diff --git a/backend/internal/model/models.go b/backend/internal/model/models.go index a83b0d9..6611ca8 100644 --- a/backend/internal/model/models.go +++ b/backend/internal/model/models.go @@ -227,8 +227,8 @@ 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"` // 是否创建桌面快捷方式 + EntryFiles string `gorm:"type:text" json:"entry_files"` // JSON数组,可启动的入口文件 + DesktopShortcutFiles string `gorm:"type:text" json:"desktop_shortcut_files"` // JSON数组,需要创建桌面快捷方式的文件 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"` // 默认安装目录 diff --git a/backend/internal/router/admin/versions.go b/backend/internal/router/admin/versions.go index 97f4cf8..4d5d3bf 100644 --- a/backend/internal/router/admin/versions.go +++ b/backend/internal/router/admin/versions.go @@ -249,36 +249,45 @@ func handleGetVersionByID(c *gin.Context) { "changelog": version.Changelog, "status": version.PublishStatus, "publish_status": version.PublishStatus, - "superseded_by": version.SupersededBy, - "published_at": version.PublishedAt, - "deprecated_at": version.DeprecatedAt, - "base_version_id": version.BaseVersionID, - "files": version.Files, - "created_at": version.CreatedAt, - "updated_at": version.UpdatedAt, + "superseded_by": version.SupersededBy, + "published_at": version.PublishedAt, + "deprecated_at": version.DeprecatedAt, + "base_version_id": version.BaseVersionID, + "entry_file": version.EntryFile, + "entry_files": version.EntryFiles, + "desktop_shortcut_files": version.DesktopShortcutFiles, + "create_start_menu": version.CreateStartMenu, + "auto_run": version.AutoRun, + "default_install_dir": version.DefaultInstallDir, + "full_package_path": version.FullPackagePath, + "full_package_size": version.FullPackageSize, + "full_package_hash": version.FullPackageHash, + "files": version.Files, + "created_at": version.CreatedAt, + "updated_at": version.UpdatedAt, }) } type CreateVersionRequest struct { - ApplicationID uint `json:"application_id"` - Version string `json:"version"` - FilePath string `json:"file_path"` - 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"` - CustomDownloadURL string `json:"custom_download_url"` - Description string `json:"description"` - Changelog string `json:"changelog"` - BaseVersionID *uint `json:"base_version_id"` - Files []VersionFileInput `json:"files"` + ApplicationID uint `json:"application_id"` + Version string `json:"version"` + FilePath string `json:"file_path"` + FileSize int64 `json:"file_size"` + FileHash string `json:"file_hash"` + EntryFile string `json:"entry_file"` + EntryFiles string `json:"entry_files"` + DesktopShortcutFiles string `json:"desktop_shortcut_files"` + 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"` + CustomDownloadURL string `json:"custom_download_url"` + Description string `json:"description"` + Changelog string `json:"changelog"` + BaseVersionID *uint `json:"base_version_id"` + Files []VersionFileInput `json:"files"` } type VersionFileInput struct { @@ -317,24 +326,24 @@ func handleCreateVersionGlobal(c *gin.Context) { } version := model.Version{ - ApplicationID: req.ApplicationID, - Version: req.Version, - FilePath: req.FilePath, - 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, - CustomDownloadURL: req.CustomDownloadURL, - Description: req.Description, - Changelog: req.Changelog, - PublishStatus: "draft", + ApplicationID: req.ApplicationID, + Version: req.Version, + FilePath: req.FilePath, + FileSize: req.FileSize, + FileHash: req.FileHash, + EntryFile: req.EntryFile, + EntryFiles: req.EntryFiles, + DesktopShortcutFiles: req.DesktopShortcutFiles, + CreateStartMenu: req.CreateStartMenu, + AutoRun: req.AutoRun, + DefaultInstallDir: req.DefaultInstallDir, + UpdateStrategy: req.UpdateStrategy, + UpdateType: req.UpdateType, + UpdateMethod: req.UpdateMethod, + CustomDownloadURL: req.CustomDownloadURL, + Description: req.Description, + Changelog: req.Changelog, + PublishStatus: "draft", } if version.UpdateStrategy == "" { @@ -465,24 +474,24 @@ func handleCreateVersionGlobal(c *gin.Context) { } type UpdateVersionRequest struct { - Version string `json:"version"` - FilePath string `json:"file_path"` - 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"` - CustomDownloadURL string `json:"custom_download_url"` - Description string `json:"description"` - Changelog string `json:"changelog"` - BaseVersionID *uint `json:"base_version_id"` - Files []VersionFileInput `json:"files"` + Version string `json:"version"` + FilePath string `json:"file_path"` + FileSize int64 `json:"file_size"` + FileHash string `json:"file_hash"` + EntryFile string `json:"entry_file"` + EntryFiles string `json:"entry_files"` + DesktopShortcutFiles string `json:"desktop_shortcut_files"` + 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"` + CustomDownloadURL string `json:"custom_download_url"` + Description string `json:"description"` + Changelog string `json:"changelog"` + BaseVersionID *uint `json:"base_version_id"` + Files []VersionFileInput `json:"files"` } func handleUpdateVersionGlobal(c *gin.Context) { @@ -541,7 +550,7 @@ func handleUpdateVersionGlobal(c *gin.Context) { } updates["entry_file"] = req.EntryFile updates["entry_files"] = req.EntryFiles - updates["create_desktop_shortcut"] = req.CreateDesktopShortcut + updates["desktop_shortcut_files"] = req.DesktopShortcutFiles updates["create_start_menu"] = req.CreateStartMenu updates["auto_run"] = req.AutoRun updates["default_install_dir"] = req.DefaultInstallDir diff --git a/backend/internal/router/app/info.go b/backend/internal/router/app/info.go index 82a0480..084febb 100644 --- a/backend/internal/router/app/info.go +++ b/backend/internal/router/app/info.go @@ -121,7 +121,7 @@ func handleAppCheckUpdate(c *gin.Context) { "file_hash": downloadHash, "entry_file": latestVersion.EntryFile, "entry_files": latestVersion.EntryFiles, - "create_desktop_shortcut": latestVersion.CreateDesktopShortcut, + "desktop_shortcut_files": latestVersion.DesktopShortcutFiles, "create_start_menu": latestVersion.CreateStartMenu, "auto_run": latestVersion.AutoRun, "default_install_dir": latestVersion.DefaultInstallDir, @@ -263,10 +263,10 @@ func handleAppCheckUpdate(c *gin.Context) { "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, + "desktop_shortcut_files": latestVersion.DesktopShortcutFiles, + "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, diff --git a/frontend/src/pages/admin/versions/[id].vue b/frontend/src/pages/admin/versions/[id].vue index 7952817..ab7f1b8 100644 --- a/frontend/src/pages/admin/versions/[id].vue +++ b/frontend/src/pages/admin/versions/[id].vue @@ -54,7 +54,7 @@ const formData = ref({ changelog: '', entry_file: '', entry_files: [] as string[], - create_desktop_shortcut: false, + desktop_shortcut_files: [] as string[], create_start_menu: false, auto_run: false, default_install_dir: '', @@ -69,10 +69,26 @@ const selectedApplication = computed(() => { return applications.value.find(app => String(app.id) === formData.value.application_id) }) +const baseVersionFiles = ref([]) + +const mergedFiles = computed(() => { + if (!uploadedData.value) return [] + // 全量更新:直接显示上传的文件 + if (formData.value.update_type !== 'patch') return uploadedData.value.files + // 补丁更新:合并基础版本文件 + 补丁文件(补丁覆盖同名文件) + if (baseVersionFiles.value.length === 0) return uploadedData.value.files + const map = new Map() + for (const f of baseVersionFiles.value) { + map.set(f.file_path, f) + } + for (const f of uploadedData.value.files) { + map.set(f.file_path, f) + } + return Array.from(map.values()) +}) + const executableFiles = computed(() => { - if (!uploadedData.value) - return [] - return uploadedData.value.files.filter(f => f.file_type === 'executable') + return mergedFiles.value.filter(f => f.file_type === 'executable') }) const fileSize = computed(() => { @@ -107,6 +123,15 @@ function toggleEntryFile(filePath: string) { formData.value.entry_file = formData.value.entry_files.length > 0 ? formData.value.entry_files[0] : '' } +function toggleDesktopShortcut(filePath: string) { + const index = formData.value.desktop_shortcut_files.indexOf(filePath) + if (index >= 0) { + formData.value.desktop_shortcut_files.splice(index, 1) + } else { + formData.value.desktop_shortcut_files.push(filePath) + } +} + async function fetchVersion() { loading.value = true try { @@ -122,7 +147,6 @@ 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 || '' @@ -132,6 +156,12 @@ async function fetchVersion() { } catch { formData.value.entry_files = [] } + // 解析 desktop_shortcut_files JSON + try { + formData.value.desktop_shortcut_files = ver.desktop_shortcut_files ? JSON.parse(ver.desktop_shortcut_files) : [] + } catch { + formData.value.desktop_shortcut_files = [] + } // 加载已有文件信息 if (ver.file_path && ver.file_size) { @@ -144,6 +174,11 @@ async function fetchVersion() { // 保存原始数据 originalUploadedData.value = { ...uploadedData.value } } + + // 如果是补丁版本且有基础版本,加载基础版本文件列表 + if (ver.update_type === 'patch' && ver.base_version_id) { + fetchBaseVersionFiles(String(ver.base_version_id)) + } } } catch (error) { @@ -168,6 +203,7 @@ async function fetchApplications() { async function fetchBaseVersions(appId: string) { if (!appId) { baseVersions.value = [] + baseVersionFiles.value = [] return } loadingBaseVersions.value = true @@ -188,6 +224,27 @@ async function fetchBaseVersions(appId: string) { } } +async function fetchBaseVersionFiles(versionId: string) { + if (!versionId) { + baseVersionFiles.value = [] + return + } + try { + const data = await api.get(`/dev/versions/${versionId}`) + baseVersionFiles.value = (data?.files || []).map((f: any) => ({ + file_path: f.file_path, + file_name: f.file_name, + file_size: f.file_size, + file_hash: f.file_hash, + file_type: f.file_type, + })) + } + catch (error) { + console.error('获取基础版本文件列表失败:', error) + baseVersionFiles.value = [] + } +} + // 监听应用变化 watch( () => formData.value.application_id, @@ -196,6 +253,7 @@ watch( fetchBaseVersions(newAppId) } else if (!newAppId) { baseVersions.value = [] + baseVersionFiles.value = [] formData.value.base_version_id = '' } }, @@ -210,12 +268,25 @@ watch( fetchBaseVersions(formData.value.application_id) } else if (newType !== 'patch') { baseVersions.value = [] + baseVersionFiles.value = [] formData.value.base_version_id = '' } }, { immediate: true } ) +// 监听基础版本变化,获取基础版本文件列表 +watch( + () => formData.value.base_version_id, + (newBaseId) => { + if (newBaseId && formData.value.update_type === 'patch') { + fetchBaseVersionFiles(newBaseId) + } else { + baseVersionFiles.value = [] + } + } +) + function handleFileSelect(event: Event) { const target = event.target as HTMLInputElement if (target.files && target.files.length > 0) { @@ -301,7 +372,7 @@ async function handleSubmit() { 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, + desktop_shortcut_files: JSON.stringify(formData.value.desktop_shortcut_files), create_start_menu: formData.value.create_start_menu, auto_run: formData.value.auto_run, default_install_dir: formData.value.default_install_dir, @@ -561,9 +632,9 @@ onMounted(() => { -
+
{{ t('admin.versions.createForm.fileList') }} -
+
@@ -576,14 +647,17 @@ onMounted(() => { - + @@ -596,10 +670,21 @@ onMounted(() => { - + @@ -607,44 +692,10 @@ onMounted(() => { - -
- {{ t('admin.versions.createForm.entryFiles') }} -

- {{ t('admin.versions.createForm.entryFilesDesc') }} -

-
-
- -
-

{{ file.file_name }}

-

{{ file.file_path }} ({{ formatFileSize(file.file_size) }})

-
-
-
-
-

{{ t('admin.versions.createForm.installOptions') }}

-
-
- {{ t('admin.versions.createForm.createDesktopShortcut') }} -

{{ t('admin.versions.createForm.createDesktopShortcutDesc') }}

-
- -
-
{{ t('admin.versions.createForm.createStartMenu') }} diff --git a/frontend/src/pages/admin/versions/create.vue b/frontend/src/pages/admin/versions/create.vue index 42cf97f..5bb23a5 100644 --- a/frontend/src/pages/admin/versions/create.vue +++ b/frontend/src/pages/admin/versions/create.vue @@ -53,7 +53,7 @@ const formData = ref({ changelog: '', entry_file: '', entry_files: [] as string[], - create_desktop_shortcut: false, + desktop_shortcut_files: [] as string[], create_start_menu: false, auto_run: false, default_install_dir: '', @@ -68,10 +68,26 @@ const selectedApplication = computed(() => { return applications.value.find(app => String(app.id) === formData.value.application_id) }) +const baseVersionFiles = ref([]) + +const mergedFiles = computed(() => { + if (!uploadedData.value) return [] + // 全量更新:直接显示上传的文件 + if (formData.value.update_type !== 'patch') return uploadedData.value.files + // 补丁更新:合并基础版本文件 + 补丁文件(补丁覆盖同名文件) + if (baseVersionFiles.value.length === 0) return uploadedData.value.files + const map = new Map() + for (const f of baseVersionFiles.value) { + map.set(f.file_path, f) + } + for (const f of uploadedData.value.files) { + map.set(f.file_path, f) + } + return Array.from(map.values()) +}) + const executableFiles = computed(() => { - if (!uploadedData.value) - return [] - return uploadedData.value.files.filter(f => f.file_type === 'executable') + return mergedFiles.value.filter(f => f.file_type === 'executable') }) const fileSize = computed(() => { @@ -104,10 +120,18 @@ function toggleEntryFile(filePath: string) { } else { formData.value.entry_files.push(filePath) } - // 兼容旧字段:第一个选中的作为主入口 formData.value.entry_file = formData.value.entry_files.length > 0 ? formData.value.entry_files[0] : '' } +function toggleDesktopShortcut(filePath: string) { + const index = formData.value.desktop_shortcut_files.indexOf(filePath) + if (index >= 0) { + formData.value.desktop_shortcut_files.splice(index, 1) + } else { + formData.value.desktop_shortcut_files.push(filePath) + } +} + const typeLabel = computed(() => { const map: Record = { full: t('admin.versions.types.full'), @@ -145,6 +169,7 @@ async function fetchApplications() { async function fetchBaseVersions(appId: string) { if (!appId) { baseVersions.value = [] + baseVersionFiles.value = [] return } loadingBaseVersions.value = true @@ -165,6 +190,27 @@ async function fetchBaseVersions(appId: string) { } } +async function fetchBaseVersionFiles(versionId: string) { + if (!versionId) { + baseVersionFiles.value = [] + return + } + try { + const data = await api.get(`/dev/versions/${versionId}`) + baseVersionFiles.value = (data?.files || []).map((f: any) => ({ + file_path: f.file_path, + file_name: f.file_name, + file_size: f.file_size, + file_hash: f.file_hash, + file_type: f.file_type, + })) + } + catch (error) { + console.error('获取基础版本文件列表失败:', error) + baseVersionFiles.value = [] + } +} + // 监听应用变化 watch( () => formData.value.application_id, @@ -174,6 +220,7 @@ watch( } else if (!newAppId) { // 只有当应用被清空时才清空基础版本 baseVersions.value = [] + baseVersionFiles.value = [] formData.value.base_version_id = '' } }, @@ -188,12 +235,25 @@ watch( fetchBaseVersions(formData.value.application_id) } else if (newType !== 'patch') { baseVersions.value = [] + baseVersionFiles.value = [] formData.value.base_version_id = '' } }, { immediate: true } ) +// 监听基础版本变化,获取基础版本文件列表 +watch( + () => formData.value.base_version_id, + (newBaseId) => { + if (newBaseId && formData.value.update_type === 'patch') { + fetchBaseVersionFiles(newBaseId) + } else { + baseVersionFiles.value = [] + } + } +) + function handleFileSelect(event: Event) { const target = event.target as HTMLInputElement if (target.files && target.files.length > 0) { @@ -277,7 +337,7 @@ async function handleSubmit() { 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, + desktop_shortcut_files: JSON.stringify(formData.value.desktop_shortcut_files), create_start_menu: formData.value.create_start_menu, auto_run: formData.value.auto_run, default_install_dir: formData.value.default_install_dir, @@ -491,9 +551,9 @@ onMounted(() => {
-
+
{{ t('admin.versions.createForm.fileList') }} -
+
{{ t('admin.versions.createForm.fileSize') }} - {{ t('admin.versions.createForm.fileType') }} + + {{ t('admin.versions.createForm.colLaunch') }} + + {{ t('admin.versions.createForm.colDesktop') }}
{{ formatFileSize(file.file_size) }} - - {{ file.file_type }} - + + + +
@@ -506,14 +566,17 @@ onMounted(() => { - + @@ -526,10 +589,21 @@ onMounted(() => { - + @@ -537,44 +611,10 @@ onMounted(() => { - -
- {{ t('admin.versions.createForm.entryFiles') }} -

- {{ t('admin.versions.createForm.entryFilesDesc') }} -

-
-
- -
-

{{ file.file_name }}

-

{{ file.file_path }} ({{ formatFileSize(file.file_size) }})

-
-
-
-
-

{{ t('admin.versions.createForm.installOptions') }}

-
-
- {{ t('admin.versions.createForm.createDesktopShortcut') }} -

{{ t('admin.versions.createForm.createDesktopShortcutDesc') }}

-
- -
-
{{ t('admin.versions.createForm.createStartMenu') }} @@ -689,7 +729,7 @@ onMounted(() => {
{{ t('admin.versions.createForm.previewFileCount') }} - {{ uploadedData ? uploadedData.files.length : '-' }} + {{ uploadedData ? mergedFiles.length : '-' }}
{{ t('admin.versions.createForm.previewEntryFile') }} diff --git a/frontend/src/plugins/i18n/zh.json b/frontend/src/plugins/i18n/zh.json index 7619302..8670954 100644 --- a/frontend/src/plugins/i18n/zh.json +++ b/frontend/src/plugins/i18n/zh.json @@ -1497,9 +1497,9 @@ "entryFilePlaceholder": "选择入口文件(可选)", "entryFiles": "入口文件(多选)", "entryFilesDesc": "勾选更新完成后可以启动的程序文件", + "colLaunch": "启动", + "colDesktop": "桌面快捷方式", "installOptions": "安装选项", - "createDesktopShortcut": "创建桌面快捷方式", - "createDesktopShortcutDesc": "安装时在桌面创建快捷方式", "createStartMenu": "创建开始菜单", "createStartMenuDesc": "安装时在开始菜单创建快捷方式", "autoRun": "安装后自动运行",
{{ t('admin.versions.createForm.fileSize') }} - {{ t('admin.versions.createForm.fileType') }} + + {{ t('admin.versions.createForm.colLaunch') }} + + {{ t('admin.versions.createForm.colDesktop') }}
{{ formatFileSize(file.file_size) }} - - {{ file.file_type }} - + + + +