From 45fdd8a294d9389058337f539f1d08ccdbb14fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Tue, 8 Sep 2026 23:51:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(software-version):=20=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E5=8C=85=E6=94=B9=E6=B5=8F=E8=A7=88=E5=99=A8=E7=9B=B4=E4=BC=A0?= =?UTF-8?q?=20MinIO=EF=BC=8C=E6=94=AF=E6=8C=81=E6=89=B9=E9=87=8F=E5=88=A0?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端新增 /api/admin/version/presign、confirm、delete 端点;直传后服务端校验对象并写 web_config - OSS 层新增软件版本对象 presign PUT/大小校验/删除与 client 桶 URL 识别 - 后台前端上传改 presign→PUT→confirm 三段式直传,带进度条;成功提示精简为"发布成功" - 版本列表加勾选/表头全选、批量删除与单行删除;操作按钮样式对齐其他子菜单 --- .../records/RecordsSoftwareVersionPage.vue | 122 +++++++++++++++--- .../src/pages/records/version-api.ts | 76 ++++++++++- .../tests/align-software-version.test.ts | 4 +- admin-frontend-vue/tests/task-128.test.ts | 6 +- admin-frontend-vue/tests/task-260.test.ts | 6 +- .../file/service/oss/OssStorageService.java | 70 +++++++++- .../SoftwareVersionAdminController.java | 33 +++++ .../service/SoftwareVersionService.java | 118 +++++++++++++++++ .../service/oss/OssStorageServiceTest.java | 35 +++++ 9 files changed, 440 insertions(+), 30 deletions(-) diff --git a/admin-frontend-vue/src/pages/records/RecordsSoftwareVersionPage.vue b/admin-frontend-vue/src/pages/records/RecordsSoftwareVersionPage.vue index a980008c..d7b80274 100644 --- a/admin-frontend-vue/src/pages/records/RecordsSoftwareVersionPage.vue +++ b/admin-frontend-vue/src/pages/records/RecordsSoftwareVersionPage.vue @@ -5,7 +5,7 @@ import { formatDateTime } from '@/utils/datetime' import { computed, onMounted, ref, watch } from 'vue' import OldPagination from '@/components/OldPagination.vue' import { ElMessage } from 'element-plus' -import { fetchSoftwareVersions, uploadSoftwareVersion } from './version-api.ts' +import { fetchSoftwareVersions, uploadSoftwareVersion, deleteSoftwareVersions } from './version-api.ts' import type { SoftwareVersionItem } from './version-dto.ts' import { isNonEmptyVersion } from './version-dto.ts' @@ -29,8 +29,53 @@ watch(filteredItems, () => { page.value = Math.min(page.value, Math.max(1, Math.ceil(filteredItems.value.length / pageSize.value))) }) +// 勾选删除:选中跨页累计;表头复选只作用于当前页,避免误选整表。 +const selectedIds = ref>(new Set()) +const selectedCount = computed(() => selectedIds.value.size) +function toggleSelect(id: number) { + const next = new Set(selectedIds.value) + if (next.has(id)) next.delete(id) + else next.add(id) + selectedIds.value = next +} +function toggleSelectAllPage() { + const pageRows = pagedVersions.value + const allOn = pageRows.length > 0 && pageRows.every((row) => selectedIds.value.has(row.id)) + const next = new Set(selectedIds.value) + if (allOn) pageRows.forEach((row) => next.delete(row.id)) + else pageRows.forEach((row) => next.add(row.id)) + selectedIds.value = next +} +async function removeSelected() { + if (!selectedCount.value) return + const rows = items.value.filter((row) => selectedIds.value.has(row.id)) + const sample = rows.slice(0, 3).map((row) => row.version).join('、') + const summary = rows.length > 3 ? `${sample} 等 ${rows.length} 个` : sample + if (!window.confirm(`确认删除选中的 ${rows.length} 个版本(${summary})?此操作不可恢复。`)) return + try { + await deleteSoftwareVersions(Array.from(selectedIds.value)) + ElMessage.success(`已删除 ${rows.length} 个版本`) + selectedIds.value = new Set() + await load() + } catch (error) { + ElMessage.error(error instanceof Error ? error.message : '删除失败') + } +} +async function removeOne(row: SoftwareVersionItem) { + if (!window.confirm(`确认删除版本 ${row.version}?此操作不可恢复。`)) return + try { + await deleteSoftwareVersions([row.id]) + ElMessage.success('删除成功') + selectedIds.value = new Set([...selectedIds.value].filter((id) => id !== row.id)) + await load() + } catch (error) { + ElMessage.error(error instanceof Error ? error.message : '删除失败') + } +} + const uploadVisible = ref(false) const uploading = ref(false) +const uploadPercent = ref(0) const newVersion = ref('') const pickedFile = ref(null) /** 弹窗内成功/失败文案(对齐 admin.js msgVersion 留驻)。 */ @@ -63,6 +108,7 @@ function openUpload() { async function submitUpload() { uploadMsg.value = '' uploadMsgOk.value = false + uploadPercent.value = 0 if (!isNonEmptyVersion(newVersion.value)) { uploadMsg.value = '请填写版本号' return @@ -75,16 +121,23 @@ async function submitUpload() { uploadMsg.value = '仅支持 .zip 格式' return } + if (pickedFile.value.size > 512 * 1024 * 1024) { + uploadMsg.value = '文件超过允许的大小限制' + return + } uploading.value = true try { - const created = await uploadSoftwareVersion(newVersion.value.trim(), pickedFile.value, pickedFile.value.name) - const version = created?.version || newVersion.value.trim() - const link = created?.fileUrl - // 对齐 admin.js:6484-6488:弹窗保持打开,成功文案(含链接)留驻可复制,仅清空输入并刷新列表。 - uploadMsg.value = link ? `发布成功。版本:${version},链接:${link}` : `发布成功。版本:${version}` + // 浏览器直传 MinIO:presign → PUT(进度条)→ confirm 落库。 + await uploadSoftwareVersion(newVersion.value.trim(), pickedFile.value, pickedFile.value.name, (p) => { + uploadPercent.value = p + uploadMsg.value = p >= 100 ? '上传完成,正在登记版本...' : `正在上传:${p}%` + }) + // 成功仅提示“发布成功”,弹窗留驻,不再展示版本号与链接。 + uploadMsg.value = '发布成功' uploadMsgOk.value = true newVersion.value = '' pickedFile.value = null + uploadPercent.value = 0 load() } catch (error) { uploadMsg.value = error instanceof Error ? error.message : '上传失败' @@ -103,6 +156,7 @@ onMounted(load)

版本列表

+
@@ -111,32 +165,39 @@ onMounted(load) - + + - - + + - + - +
版本号 + + 版本号 下载链接创建时间操作创建时间操作
加载中...加载中...
{{ versionKeyword ? '暂无匹配版本' : '暂无版本记录' }}{{ versionKeyword ? '暂无匹配版本' : '暂无版本记录' }}
@@ -157,6 +218,7 @@ onMounted(load)

仅支持 .zip 格式

{{ pickedFile.name }}
+