feat: file list shows full package files, desktop shortcut per-file setting

This commit is contained in:
Trae AI
2026-06-18 18:59:50 +08:00
parent 77a2dd468f
commit 2751b24e18
6 changed files with 271 additions and 171 deletions
+2 -2
View File
@@ -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"` // 默认安装目录
+13 -4
View File
@@ -253,6 +253,15 @@ func handleGetVersionByID(c *gin.Context) {
"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,
@@ -267,7 +276,7 @@ type CreateVersionRequest struct {
FileHash string `json:"file_hash"`
EntryFile string `json:"entry_file"`
EntryFiles string `json:"entry_files"`
CreateDesktopShortcut bool `json:"create_desktop_shortcut"`
DesktopShortcutFiles string `json:"desktop_shortcut_files"`
CreateStartMenu bool `json:"create_start_menu"`
AutoRun bool `json:"auto_run"`
DefaultInstallDir string `json:"default_install_dir"`
@@ -324,7 +333,7 @@ func handleCreateVersionGlobal(c *gin.Context) {
FileHash: req.FileHash,
EntryFile: req.EntryFile,
EntryFiles: req.EntryFiles,
CreateDesktopShortcut: req.CreateDesktopShortcut,
DesktopShortcutFiles: req.DesktopShortcutFiles,
CreateStartMenu: req.CreateStartMenu,
AutoRun: req.AutoRun,
DefaultInstallDir: req.DefaultInstallDir,
@@ -471,7 +480,7 @@ type UpdateVersionRequest struct {
FileHash string `json:"file_hash"`
EntryFile string `json:"entry_file"`
EntryFiles string `json:"entry_files"`
CreateDesktopShortcut bool `json:"create_desktop_shortcut"`
DesktopShortcutFiles string `json:"desktop_shortcut_files"`
CreateStartMenu bool `json:"create_start_menu"`
AutoRun bool `json:"auto_run"`
DefaultInstallDir string `json:"default_install_dir"`
@@ -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
+2 -2
View File
@@ -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,7 +263,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,
+100 -49
View File
@@ -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<VersionFileInfo[]>([])
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<string, VersionFileInfo>()
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<any>(`/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(() => {
</div>
</div>
<div v-if="uploadedData && uploadedData.files.length > 0" class="space-y-2">
<div v-if="mergedFiles.length > 0" class="space-y-2">
<UiLabel>{{ t('admin.versions.createForm.fileList') }}</UiLabel>
<div class="border rounded-lg max-h-[300px] overflow-auto">
<div class="border rounded-lg max-h-[400px] overflow-auto">
<table class="w-full text-sm">
<thead class="bg-muted/50 sticky top-0">
<tr>
@@ -576,14 +647,17 @@ onMounted(() => {
<th class="text-right p-2 font-medium">
{{ t('admin.versions.createForm.fileSize') }}
</th>
<th class="text-left p-2 font-medium">
{{ t('admin.versions.createForm.fileType') }}
<th class="text-center p-2 font-medium w-20">
{{ t('admin.versions.createForm.colLaunch') }}
</th>
<th class="text-center p-2 font-medium w-20">
{{ t('admin.versions.createForm.colDesktop') }}
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(file, index) in uploadedData.files"
v-for="(file, index) in mergedFiles"
:key="index"
class="border-t"
>
@@ -596,10 +670,21 @@ onMounted(() => {
<td class="p-2 text-right text-muted-foreground">
{{ formatFileSize(file.file_size) }}
</td>
<td class="p-2">
<span class="px-2 py-0.5 rounded text-xs bg-muted">
{{ file.file_type }}
</span>
<td class="p-2 text-center">
<input
type="checkbox"
:checked="formData.entry_files.includes(file.file_path)"
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
@change="toggleEntryFile(file.file_path)"
>
</td>
<td class="p-2 text-center">
<input
type="checkbox"
:checked="formData.desktop_shortcut_files.includes(file.file_path)"
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
@change="toggleDesktopShortcut(file.file_path)"
>
</td>
</tr>
</tbody>
@@ -607,44 +692,10 @@ onMounted(() => {
</div>
</div>
<!-- 多选入口文件 -->
<div v-if="executableFiles.length > 0" class="space-y-2">
<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"
class="flex items-center gap-3 rounded-lg border p-3"
>
<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>
+90 -50
View File
@@ -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<VersionFileInfo[]>([])
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<string, VersionFileInfo>()
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<string, string> = {
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<any>(`/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(() => {
</div>
</div>
<div v-if="uploadedData && uploadedData.files.length > 0" class="space-y-2">
<div v-if="mergedFiles.length > 0" class="space-y-2">
<UiLabel>{{ t('admin.versions.createForm.fileList') }}</UiLabel>
<div class="border rounded-lg max-h-[300px] overflow-auto">
<div class="border rounded-lg max-h-[400px] overflow-auto">
<table class="w-full text-sm">
<thead class="bg-muted/50 sticky top-0">
<tr>
@@ -506,14 +566,17 @@ onMounted(() => {
<th class="text-right p-2 font-medium">
{{ t('admin.versions.createForm.fileSize') }}
</th>
<th class="text-left p-2 font-medium">
{{ t('admin.versions.createForm.fileType') }}
<th class="text-center p-2 font-medium w-20">
{{ t('admin.versions.createForm.colLaunch') }}
</th>
<th class="text-center p-2 font-medium w-20">
{{ t('admin.versions.createForm.colDesktop') }}
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(file, index) in uploadedData.files"
v-for="(file, index) in mergedFiles"
:key="index"
class="border-t"
>
@@ -526,10 +589,21 @@ onMounted(() => {
<td class="p-2 text-right text-muted-foreground">
{{ formatFileSize(file.file_size) }}
</td>
<td class="p-2">
<span class="px-2 py-0.5 rounded text-xs bg-muted">
{{ file.file_type }}
</span>
<td class="p-2 text-center">
<input
type="checkbox"
:checked="formData.entry_files.includes(file.file_path)"
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
@change="toggleEntryFile(file.file_path)"
>
</td>
<td class="p-2 text-center">
<input
type="checkbox"
:checked="formData.desktop_shortcut_files.includes(file.file_path)"
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
@change="toggleDesktopShortcut(file.file_path)"
>
</td>
</tr>
</tbody>
@@ -537,44 +611,10 @@ onMounted(() => {
</div>
</div>
<!-- 多选入口文件 -->
<div v-if="executableFiles.length > 0" class="space-y-2">
<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"
class="flex items-center gap-3 rounded-lg border p-3"
>
<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>
@@ -689,7 +729,7 @@ onMounted(() => {
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewFileCount') }}</span>
<span>{{ uploadedData ? uploadedData.files.length : '-' }}</span>
<span>{{ uploadedData ? mergedFiles.length : '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewEntryFile') }}</span>
+2 -2
View File
@@ -1497,9 +1497,9 @@
"entryFilePlaceholder": "选择入口文件(可选)",
"entryFiles": "入口文件(多选)",
"entryFilesDesc": "勾选更新完成后可以启动的程序文件",
"colLaunch": "启动",
"colDesktop": "桌面快捷方式",
"installOptions": "安装选项",
"createDesktopShortcut": "创建桌面快捷方式",
"createDesktopShortcutDesc": "安装时在桌面创建快捷方式",
"createStartMenu": "创建开始菜单",
"createStartMenuDesc": "安装时在开始菜单创建快捷方式",
"autoRun": "安装后自动运行",