feat: 版本管理支持多入口文件、安装选项(桌面快捷方式/开始菜单/自动运行/默认安装目录)

This commit is contained in:
Trae AI
2026-06-18 18:15:51 +08:00
parent 9ea0ef4536
commit 77a2dd468f
6 changed files with 364 additions and 176 deletions
+5
View File
@@ -227,6 +227,11 @@ type Version struct {
FileSize int64 `json:"file_size"`
FileHash string `gorm:"size:64" json:"file_hash"`
EntryFile string `gorm:"size:255" json:"entry_file"`
EntryFiles string `gorm:"type:text" json:"entry_files"` // JSON数组,多入口文件
CreateDesktopShortcut bool `gorm:"default:false" json:"create_desktop_shortcut"` // 是否创建桌面快捷方式
CreateStartMenu bool `gorm:"default:false" json:"create_start_menu"` // 是否创建开始菜单
AutoRun bool `gorm:"default:false" json:"auto_run"` // 是否自动运行
DefaultInstallDir string `gorm:"size:500" json:"default_install_dir"` // 默认安装目录
UpdateStrategy string `gorm:"size:20;default:optional" json:"update_strategy"`
UpdateType string `gorm:"size:20;default:full" json:"update_type"`
UpdateMethod string `gorm:"size:20;default:auto" json:"update_method"`
+20
View File
@@ -266,6 +266,11 @@ type CreateVersionRequest struct {
FileSize int64 `json:"file_size"`
FileHash string `json:"file_hash"`
EntryFile string `json:"entry_file"`
EntryFiles string `json:"entry_files"`
CreateDesktopShortcut bool `json:"create_desktop_shortcut"`
CreateStartMenu bool `json:"create_start_menu"`
AutoRun bool `json:"auto_run"`
DefaultInstallDir string `json:"default_install_dir"`
UpdateStrategy string `json:"update_strategy"`
UpdateType string `json:"update_type"`
UpdateMethod string `json:"update_method"`
@@ -318,6 +323,11 @@ func handleCreateVersionGlobal(c *gin.Context) {
FileSize: req.FileSize,
FileHash: req.FileHash,
EntryFile: req.EntryFile,
EntryFiles: req.EntryFiles,
CreateDesktopShortcut: req.CreateDesktopShortcut,
CreateStartMenu: req.CreateStartMenu,
AutoRun: req.AutoRun,
DefaultInstallDir: req.DefaultInstallDir,
UpdateStrategy: req.UpdateStrategy,
UpdateType: req.UpdateType,
UpdateMethod: req.UpdateMethod,
@@ -460,6 +470,11 @@ type UpdateVersionRequest struct {
FileSize int64 `json:"file_size"`
FileHash string `json:"file_hash"`
EntryFile string `json:"entry_file"`
EntryFiles string `json:"entry_files"`
CreateDesktopShortcut bool `json:"create_desktop_shortcut"`
CreateStartMenu bool `json:"create_start_menu"`
AutoRun bool `json:"auto_run"`
DefaultInstallDir string `json:"default_install_dir"`
UpdateStrategy string `json:"update_strategy"`
UpdateType string `json:"update_type"`
UpdateMethod string `json:"update_method"`
@@ -525,6 +540,11 @@ func handleUpdateVersionGlobal(c *gin.Context) {
updates["file_hash"] = req.FileHash
}
updates["entry_file"] = req.EntryFile
updates["entry_files"] = req.EntryFiles
updates["create_desktop_shortcut"] = req.CreateDesktopShortcut
updates["create_start_menu"] = req.CreateStartMenu
updates["auto_run"] = req.AutoRun
updates["default_install_dir"] = req.DefaultInstallDir
if req.UpdateStrategy != "" {
updates["update_strategy"] = req.UpdateStrategy
}
+10
View File
@@ -120,6 +120,11 @@ func handleAppCheckUpdate(c *gin.Context) {
"file_size": downloadSize,
"file_hash": downloadHash,
"entry_file": latestVersion.EntryFile,
"entry_files": latestVersion.EntryFiles,
"create_desktop_shortcut": latestVersion.CreateDesktopShortcut,
"create_start_menu": latestVersion.CreateStartMenu,
"auto_run": latestVersion.AutoRun,
"default_install_dir": latestVersion.DefaultInstallDir,
"update_notes": latestVersion.Description,
"update_strategy": latestVersion.UpdateStrategy,
"update_type": latestVersion.UpdateType,
@@ -257,6 +262,11 @@ func handleAppCheckUpdate(c *gin.Context) {
"file_size": downloadSize,
"file_hash": downloadHash,
"entry_file": latestVersion.EntryFile,
"entry_files": latestVersion.EntryFiles,
"create_desktop_shortcut": latestVersion.CreateDesktopShortcut,
"create_start_menu": latestVersion.CreateStartMenu,
"auto_run": latestVersion.AutoRun,
"default_install_dir": latestVersion.DefaultInstallDir,
"update_notes": latestVersion.Description,
"update_strategy": updateStrategy,
"update_type": latestVersion.UpdateType,
+131 -54
View File
@@ -53,6 +53,11 @@ const formData = ref({
custom_download_url: '',
changelog: '',
entry_file: '',
entry_files: [] as string[],
create_desktop_shortcut: false,
create_start_menu: false,
auto_run: false,
default_install_dir: '',
file: null as File | null,
base_version_id: '' as string,
})
@@ -92,6 +97,16 @@ function formatFileSize(size: number): string {
return `${(size / (1024 * 1024)).toFixed(2)} MB`
}
function toggleEntryFile(filePath: string) {
const index = formData.value.entry_files.indexOf(filePath)
if (index >= 0) {
formData.value.entry_files.splice(index, 1)
} else {
formData.value.entry_files.push(filePath)
}
formData.value.entry_file = formData.value.entry_files.length > 0 ? formData.value.entry_files[0] : ''
}
async function fetchVersion() {
loading.value = true
try {
@@ -107,6 +122,16 @@ async function fetchVersion() {
formData.value.changelog = ver.changelog || ''
formData.value.entry_file = ver.entry_file || ''
formData.value.base_version_id = ver.base_version_id ? String(ver.base_version_id) : ''
formData.value.create_desktop_shortcut = ver.create_desktop_shortcut || false
formData.value.create_start_menu = ver.create_start_menu || false
formData.value.auto_run = ver.auto_run || false
formData.value.default_install_dir = ver.default_install_dir || ''
// 解析 entry_files JSON
try {
formData.value.entry_files = ver.entry_files ? JSON.parse(ver.entry_files) : []
} catch {
formData.value.entry_files = []
}
// 加载已有文件信息
if (ver.file_path && ver.file_size) {
@@ -275,6 +300,11 @@ async function handleSubmit() {
custom_download_url: formData.value.custom_download_url,
changelog: formData.value.changelog,
entry_file: formData.value.entry_file,
entry_files: JSON.stringify(formData.value.entry_files),
create_desktop_shortcut: formData.value.create_desktop_shortcut,
create_start_menu: formData.value.create_start_menu,
auto_run: formData.value.auto_run,
default_install_dir: formData.value.default_install_dir,
}
// 如果是补丁版本,添加基础版本ID
@@ -385,6 +415,49 @@ onMounted(() => {
/>
</div>
<!-- 更新类型选择上传前选择 -->
<div class="space-y-2">
<UiLabel>{{ t('admin.versions.createForm.updateType') }} <span class="text-destructive">*</span></UiLabel>
<UiSelect v-model="formData.update_type" :disabled="saving">
<UiSelectTrigger>
<UiSelectValue />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem value="full">
{{ t('admin.versions.types.full') }}
</UiSelectItem>
<UiSelectItem value="patch">
{{ t('admin.versions.types.patch') }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
</div>
<!-- 基础版本选择仅补丁类型上传前选择 -->
<div v-if="formData.update_type === 'patch'" class="space-y-2">
<UiLabel>{{ t('admin.versions.createForm.baseVersion') }} <span class="text-destructive">*</span></UiLabel>
<p class="text-sm text-muted-foreground">
{{ t('admin.versions.createForm.baseVersionDesc') }}
</p>
<UiSelect v-model="formData.base_version_id" :disabled="saving || loadingBaseVersions">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('admin.versions.createForm.baseVersionPlaceholder')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem
v-for="v in baseVersions"
:key="v.id"
:value="String(v.id)"
>
{{ v.version }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
<p v-if="baseVersions.length === 0 && !loadingBaseVersions" class="text-sm text-destructive">
{{ t('admin.versions.createForm.noBaseVersion') }}
</p>
</div>
<div class="space-y-2">
<UiLabel>{{ t('admin.versions.createForm.updateFile') }}</UiLabel>
<div class="border-2 border-dashed rounded-lg p-4">
@@ -534,22 +607,68 @@ onMounted(() => {
</div>
</div>
<!-- 多选入口文件 -->
<div v-if="executableFiles.length > 0" class="space-y-2">
<UiLabel>{{ t('admin.versions.createForm.entryFile') }}</UiLabel>
<UiSelect v-model="formData.entry_file" :disabled="saving">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('admin.versions.createForm.entryFilePlaceholder')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem
<UiLabel>{{ t('admin.versions.createForm.entryFiles') }}</UiLabel>
<p class="text-sm text-muted-foreground">
{{ t('admin.versions.createForm.entryFilesDesc') }}
</p>
<div class="space-y-2">
<div
v-for="file in executableFiles"
:key="file.file_path"
:value="file.file_path"
class="flex items-center gap-3 rounded-lg border p-3"
>
{{ file.file_name }} ({{ formatFileSize(file.file_size) }})
</UiSelectItem>
</UiSelectContent>
</UiSelect>
<input
type="checkbox"
:checked="formData.entry_files.includes(file.file_path)"
class="h-4 w-4 rounded border-gray-300"
@change="toggleEntryFile(file.file_path)"
>
<div class="flex-1">
<p class="text-sm font-medium">{{ file.file_name }}</p>
<p class="text-xs text-muted-foreground">{{ file.file_path }} ({{ formatFileSize(file.file_size) }})</p>
</div>
</div>
</div>
</div>
<!-- 安装选项 -->
<div class="space-y-4 rounded-lg border p-4">
<h4 class="font-medium">{{ t('admin.versions.createForm.installOptions') }}</h4>
<div class="flex items-center justify-between">
<div class="space-y-0.5">
<UiLabel class="text-sm">{{ t('admin.versions.createForm.createDesktopShortcut') }}</UiLabel>
<p class="text-xs text-muted-foreground">{{ t('admin.versions.createForm.createDesktopShortcutDesc') }}</p>
</div>
<UiSwitch v-model="formData.create_desktop_shortcut" :disabled="saving" />
</div>
<div class="flex items-center justify-between">
<div class="space-y-0.5">
<UiLabel class="text-sm">{{ t('admin.versions.createForm.createStartMenu') }}</UiLabel>
<p class="text-xs text-muted-foreground">{{ t('admin.versions.createForm.createStartMenuDesc') }}</p>
</div>
<UiSwitch v-model="formData.create_start_menu" :disabled="saving" />
</div>
<div class="flex items-center justify-between">
<div class="space-y-0.5">
<UiLabel class="text-sm">{{ t('admin.versions.createForm.autoRun') }}</UiLabel>
<p class="text-xs text-muted-foreground">{{ t('admin.versions.createForm.autoRunDesc') }}</p>
</div>
<UiSwitch v-model="formData.auto_run" :disabled="saving" />
</div>
<div class="space-y-2">
<UiLabel class="text-sm">{{ t('admin.versions.createForm.defaultInstallDir') }}</UiLabel>
<UiInput
v-model="formData.default_install_dir"
:placeholder="t('admin.versions.createForm.defaultInstallDirPlaceholder')"
:disabled="saving"
/>
</div>
</div>
<div class="flex items-center justify-between rounded-lg border p-4">
@@ -567,48 +686,6 @@ onMounted(() => {
/>
</div>
<div class="space-y-2">
<UiLabel>{{ t('admin.versions.createForm.updateType') }}</UiLabel>
<UiSelect v-model="formData.update_type" :disabled="saving">
<UiSelectTrigger>
<UiSelectValue />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem value="full">
{{ t('admin.versions.types.full') }}
</UiSelectItem>
<UiSelectItem value="patch">
{{ t('admin.versions.types.patch') }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
</div>
<!-- 基础版本选择仅补丁类型 -->
<div v-if="formData.update_type === 'patch'" class="space-y-2">
<UiLabel>{{ t('admin.versions.createForm.baseVersion') }}</UiLabel>
<p class="text-sm text-muted-foreground">
{{ t('admin.versions.createForm.baseVersionDesc') }}
</p>
<UiSelect v-model="formData.base_version_id" :disabled="saving || loadingBaseVersions">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('admin.versions.createForm.baseVersionPlaceholder')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem
v-for="v in baseVersions"
:key="v.id"
:value="String(v.id)"
>
{{ v.version }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
<p v-if="baseVersions.length === 0 && !loadingBaseVersions" class="text-sm text-destructive">
{{ t('admin.versions.createForm.noBaseVersion') }}
</p>
</div>
<div class="space-y-2">
<UiLabel>{{ t('admin.versions.createForm.updateMethod') }}</UiLabel>
<UiSelect v-model="formData.update_method" :disabled="saving">
+120 -55
View File
@@ -52,6 +52,11 @@ const formData = ref({
custom_download_url: '',
changelog: '',
entry_file: '',
entry_files: [] as string[],
create_desktop_shortcut: false,
create_start_menu: false,
auto_run: false,
default_install_dir: '',
file: null as File | null,
base_version_id: '' as string,
})
@@ -92,6 +97,17 @@ function formatFileSize(size: number): string {
return `${(size / (1024 * 1024)).toFixed(2)} MB`
}
function toggleEntryFile(filePath: string) {
const index = formData.value.entry_files.indexOf(filePath)
if (index >= 0) {
formData.value.entry_files.splice(index, 1)
} else {
formData.value.entry_files.push(filePath)
}
// 兼容旧字段:第一个选中的作为主入口
formData.value.entry_file = formData.value.entry_files.length > 0 ? formData.value.entry_files[0] : ''
}
const typeLabel = computed(() => {
const map: Record<string, string> = {
full: t('admin.versions.types.full'),
@@ -260,6 +276,11 @@ async function handleSubmit() {
custom_download_url: formData.value.custom_download_url,
changelog: formData.value.changelog,
entry_file: formData.value.entry_file,
entry_files: JSON.stringify(formData.value.entry_files),
create_desktop_shortcut: formData.value.create_desktop_shortcut,
create_start_menu: formData.value.create_start_menu,
auto_run: formData.value.auto_run,
default_install_dir: formData.value.default_install_dir,
file_path: uploadedData.value.file_path,
file_size: uploadedData.value.file_size,
file_hash: uploadedData.value.file_hash,
@@ -351,6 +372,49 @@ onMounted(() => {
/>
</div>
<!-- 更新类型选择上传前选择 -->
<div class="space-y-2">
<UiLabel>{{ t('admin.versions.createForm.updateType') }} <span class="text-destructive">*</span></UiLabel>
<UiSelect v-model="formData.update_type" :disabled="saving">
<UiSelectTrigger>
<UiSelectValue />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem value="full">
{{ t('admin.versions.types.full') }}
</UiSelectItem>
<UiSelectItem value="patch">
{{ t('admin.versions.types.patch') }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
</div>
<!-- 基础版本选择仅补丁类型上传前选择 -->
<div v-if="formData.update_type === 'patch'" class="space-y-2">
<UiLabel>{{ t('admin.versions.createForm.baseVersion') }} <span class="text-destructive">*</span></UiLabel>
<p class="text-sm text-muted-foreground">
{{ t('admin.versions.createForm.baseVersionDesc') }}
</p>
<UiSelect v-model="formData.base_version_id" :disabled="saving || loadingBaseVersions">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('admin.versions.createForm.baseVersionPlaceholder')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem
v-for="v in baseVersions"
:key="v.id"
:value="String(v.id)"
>
{{ v.version }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
<p v-if="baseVersions.length === 0 && !loadingBaseVersions" class="text-sm text-destructive">
{{ t('admin.versions.createForm.noBaseVersion') }}
</p>
</div>
<div class="space-y-2">
<UiLabel>{{ t('admin.versions.createForm.updateFile') }}</UiLabel>
<div class="border-2 border-dashed rounded-lg p-4">
@@ -473,25 +537,68 @@ onMounted(() => {
</div>
</div>
<!-- 多选入口文件 -->
<div v-if="executableFiles.length > 0" class="space-y-2">
<UiLabel>{{ t('admin.versions.createForm.entryFile') }}</UiLabel>
<UiLabel>{{ t('admin.versions.createForm.entryFiles') }}</UiLabel>
<p class="text-sm text-muted-foreground">
{{ t('admin.versions.createForm.entryFileDesc') }}
{{ t('admin.versions.createForm.entryFilesDesc') }}
</p>
<UiSelect v-model="formData.entry_file" :disabled="saving">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('admin.versions.createForm.entryFilePlaceholder')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem
<div class="space-y-2">
<div
v-for="file in executableFiles"
:key="file.file_path"
:value="file.file_path"
class="flex items-center gap-3 rounded-lg border p-3"
>
{{ file.file_name }} ({{ formatFileSize(file.file_size) }})
</UiSelectItem>
</UiSelectContent>
</UiSelect>
<input
type="checkbox"
:checked="formData.entry_files.includes(file.file_path)"
class="h-4 w-4 rounded border-gray-300"
@change="toggleEntryFile(file.file_path)"
>
<div class="flex-1">
<p class="text-sm font-medium">{{ file.file_name }}</p>
<p class="text-xs text-muted-foreground">{{ file.file_path }} ({{ formatFileSize(file.file_size) }})</p>
</div>
</div>
</div>
</div>
<!-- 安装选项 -->
<div class="space-y-4 rounded-lg border p-4">
<h4 class="font-medium">{{ t('admin.versions.createForm.installOptions') }}</h4>
<div class="flex items-center justify-between">
<div class="space-y-0.5">
<UiLabel class="text-sm">{{ t('admin.versions.createForm.createDesktopShortcut') }}</UiLabel>
<p class="text-xs text-muted-foreground">{{ t('admin.versions.createForm.createDesktopShortcutDesc') }}</p>
</div>
<UiSwitch v-model="formData.create_desktop_shortcut" :disabled="saving" />
</div>
<div class="flex items-center justify-between">
<div class="space-y-0.5">
<UiLabel class="text-sm">{{ t('admin.versions.createForm.createStartMenu') }}</UiLabel>
<p class="text-xs text-muted-foreground">{{ t('admin.versions.createForm.createStartMenuDesc') }}</p>
</div>
<UiSwitch v-model="formData.create_start_menu" :disabled="saving" />
</div>
<div class="flex items-center justify-between">
<div class="space-y-0.5">
<UiLabel class="text-sm">{{ t('admin.versions.createForm.autoRun') }}</UiLabel>
<p class="text-xs text-muted-foreground">{{ t('admin.versions.createForm.autoRunDesc') }}</p>
</div>
<UiSwitch v-model="formData.auto_run" :disabled="saving" />
</div>
<div class="space-y-2">
<UiLabel class="text-sm">{{ t('admin.versions.createForm.defaultInstallDir') }}</UiLabel>
<UiInput
v-model="formData.default_install_dir"
:placeholder="t('admin.versions.createForm.defaultInstallDirPlaceholder')"
:disabled="saving"
/>
</div>
</div>
<div class="flex items-center justify-between rounded-lg border p-4">
@@ -509,48 +616,6 @@ onMounted(() => {
/>
</div>
<div class="space-y-2">
<UiLabel>{{ t('admin.versions.createForm.updateType') }}</UiLabel>
<UiSelect v-model="formData.update_type" :disabled="saving">
<UiSelectTrigger>
<UiSelectValue />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem value="full">
{{ t('admin.versions.types.full') }}
</UiSelectItem>
<UiSelectItem value="patch">
{{ t('admin.versions.types.patch') }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
</div>
<!-- 基础版本选择仅补丁类型 -->
<div v-if="formData.update_type === 'patch'" class="space-y-2">
<UiLabel>{{ t('admin.versions.createForm.baseVersion') }}</UiLabel>
<p class="text-sm text-muted-foreground">
{{ t('admin.versions.createForm.baseVersionDesc') }}
</p>
<UiSelect v-model="formData.base_version_id" :disabled="saving || loadingBaseVersions">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('admin.versions.createForm.baseVersionPlaceholder')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem
v-for="v in baseVersions"
:key="v.id"
:value="String(v.id)"
>
{{ v.version }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
<p v-if="baseVersions.length === 0 && !loadingBaseVersions" class="text-sm text-destructive">
{{ t('admin.versions.createForm.noBaseVersion') }}
</p>
</div>
<div class="space-y-2">
<UiLabel>{{ t('admin.versions.createForm.updateMethod') }}</UiLabel>
<UiSelect v-model="formData.update_method" :disabled="saving">
+11
View File
@@ -1495,6 +1495,17 @@
"entryFile": "入口文件",
"entryFileDesc": "选择更新完成后要启动的主程序文件(可选)",
"entryFilePlaceholder": "选择入口文件(可选)",
"entryFiles": "入口文件(多选)",
"entryFilesDesc": "勾选更新完成后可以启动的程序文件",
"installOptions": "安装选项",
"createDesktopShortcut": "创建桌面快捷方式",
"createDesktopShortcutDesc": "安装时在桌面创建快捷方式",
"createStartMenu": "创建开始菜单",
"createStartMenuDesc": "安装时在开始菜单创建快捷方式",
"autoRun": "安装后自动运行",
"autoRunDesc": "安装完成后自动启动主程序",
"defaultInstallDir": "默认安装目录",
"defaultInstallDirPlaceholder": "例如: C:\\Program Files\\MyApp",
"forcedUpdate": "强制更新",
"forcedUpdateDesc": "启用后用户必须更新到此版本才能继续使用",
"updateType": "更新类型",