diff --git a/admin-frontend-vue/src/pages/records/RecordsSoftwareVersionPage.vue b/admin-frontend-vue/src/pages/records/RecordsSoftwareVersionPage.vue
index ea43e35e..f6d799b5 100644
--- a/admin-frontend-vue/src/pages/records/RecordsSoftwareVersionPage.vue
+++ b/admin-frontend-vue/src/pages/records/RecordsSoftwareVersionPage.vue
@@ -32,23 +32,29 @@ function onFileChange(file: File) {
async function submitUpload() {
if (!isNonEmptyVersion(newVersion.value)) {
- ElMessage.warning('版本号不能为空')
+ ElMessage.warning('请填写版本号')
return
}
if (!pickedFile.value) {
- ElMessage.warning('请选择程序压缩包')
+ ElMessage.warning('请选择 zip 压缩包')
+ return
+ }
+ if (!/\.zip$/i.test(pickedFile.value.name)) {
+ ElMessage.warning('仅支持 .zip 格式')
return
}
uploading.value = true
try {
- await uploadSoftwareVersion(newVersion.value.trim(), pickedFile.value, pickedFile.value.name)
- ElMessage.success('上传成功')
+ const created = await uploadSoftwareVersion(newVersion.value.trim(), pickedFile.value, pickedFile.value.name)
+ const version = created?.version || newVersion.value.trim()
+ const link = created?.fileUrl
+ ElMessage.success(link ? `发布成功。版本:${version},链接:${link}` : `发布成功。版本:${version}`)
uploadVisible.value = false
newVersion.value = ''
pickedFile.value = null
load()
} catch (error) {
- ElMessage.error(error instanceof Error ? error.message : '上传失败')
+ ElMessage.error(error instanceof Error ? error.message : '发布失败')
} finally {
uploading.value = false
}
@@ -70,9 +76,9 @@ onMounted(load)
-
+
-
+
{{ (row as SoftwareVersionItem).fileUrl }}
—
@@ -81,6 +87,12 @@ onMounted(load)
{{ formatDateTime(row.createdAt) }}
+
+
+ 下载
+ —
+
+
@@ -90,7 +102,7 @@ onMounted(load)
-
+
选择文件
{{ pickedFile.name }}
diff --git a/admin-frontend-vue/src/pages/records/version-api.ts b/admin-frontend-vue/src/pages/records/version-api.ts
index 2200c131..ba40e0fd 100644
--- a/admin-frontend-vue/src/pages/records/version-api.ts
+++ b/admin-frontend-vue/src/pages/records/version-api.ts
@@ -1,7 +1,7 @@
/** 软件版本列表加载适配(任务 127):GET /api/admin/versions。 */
import { http } from '@/api/http'
-import { parseSoftwareVersionList } from './version-model.ts'
-import type { SoftwareVersionList } from './version-dto.ts'
+import { parseSoftwareVersionList, parseSoftwareVersionUpload } from './version-model.ts'
+import type { SoftwareVersionItem, SoftwareVersionList } from './version-dto.ts'
export const SOFTWARE_VERSIONS_ENDPOINT = '/api/admin/versions'
export const SOFTWARE_VERSION_UPLOAD_ENDPOINT = '/api/admin/version'
@@ -11,10 +11,11 @@ export async function fetchSoftwareVersions(): Promise {
return parseSoftwareVersionList(data)
}
-/** 上传客户端软件版本(multipart version + file)。 */
-export async function uploadSoftwareVersion(version: string, file: Blob, filename = ''): Promise {
+/** 上传客户端软件版本(multipart version + file);返回新版本行(含下载链接)。 */
+export async function uploadSoftwareVersion(version: string, file: Blob, filename = ''): Promise {
const form = new FormData()
form.append('version', version)
form.append('file', file, filename || (file instanceof File ? file.name : 'version.zip'))
- await http.post(SOFTWARE_VERSION_UPLOAD_ENDPOINT, form)
+ const { data } = await http.post(SOFTWARE_VERSION_UPLOAD_ENDPOINT, form)
+ return parseSoftwareVersionUpload(data)
}
diff --git a/admin-frontend-vue/src/pages/records/version-model.ts b/admin-frontend-vue/src/pages/records/version-model.ts
index a30a6894..034a727e 100644
--- a/admin-frontend-vue/src/pages/records/version-model.ts
+++ b/admin-frontend-vue/src/pages/records/version-model.ts
@@ -33,3 +33,11 @@ export function parseSoftwareVersionList(payload: unknown): SoftwareVersionList
: []
return { items }
}
+
+/** 上传成功后返回的新版本行(data.item);无 item 回 null。 */
+export function parseSoftwareVersionUpload(payload: unknown): SoftwareVersionItem | null {
+ const data = unwrap(payload)
+ if (!data || typeof data !== 'object') return null
+ const item = (data as Record).item
+ return item ? toSoftwareVersionItem(item) : null
+}
diff --git a/admin-frontend-vue/src/pages/records/version-upload-model.ts b/admin-frontend-vue/src/pages/records/version-upload-model.ts
index 93748289..ea8e6c29 100644
--- a/admin-frontend-vue/src/pages/records/version-upload-model.ts
+++ b/admin-frontend-vue/src/pages/records/version-upload-model.ts
@@ -10,14 +10,17 @@ export interface VersionUploadErrors {
file?: string
}
-/** 校验上传表单(版本号必填非空、文件已选且非空);返回逐字段错误。 */
+/** 校验上传表单(版本号必填非空、zip 已选且非空);文案对齐 admin.js。 */
export function validateSoftwareVersionUpload(form: VersionUploadForm): { valid: boolean; errors: VersionUploadErrors } {
const errors: VersionUploadErrors = {}
const version = (form.version || '').trim()
- if (!version) errors.version = '版本号不能为空'
+ if (!version) errors.version = '请填写版本号'
const file = form.file
- if (!file) errors.file = '请选择安装包文件'
- else if (typeof file.size === 'number' && file.size <= 0) errors.file = '安装包为空或无效'
+ if (!file) errors.file = '请选择 zip 压缩包'
+ else {
+ if (typeof file.size === 'number' && file.size <= 0) errors.file = '安装包为空或无效'
+ else if (file.name && !/\.zip$/i.test(file.name)) errors.file = '仅支持 .zip 格式'
+ }
return { valid: Object.keys(errors).length === 0, errors }
}
diff --git a/admin-frontend-vue/tests/task-128.test.ts b/admin-frontend-vue/tests/task-128.test.ts
index 05d27ff9..433d8a23 100644
--- a/admin-frontend-vue/tests/task-128.test.ts
+++ b/admin-frontend-vue/tests/task-128.test.ts
@@ -29,16 +29,16 @@ test('test_task_128_version_upload_repeated_is_idempotent', () => {
})
test('test_task_128_version_upload_boundary_empty_input', () => {
- // 边界空值:空版本/无文件校验失败。
+ // 边界空值:空版本/无文件校验失败(文案对齐 admin.js)。
const { errors } = validateSoftwareVersionUpload({ version: '', file: null })
- assert.equal(errors.version, '版本号不能为空')
- assert.equal(errors.file, '请选择安装包文件')
+ assert.equal(errors.version, '请填写版本号')
+ assert.equal(errors.file, '请选择 zip 压缩包')
})
test('test_task_128_version_upload_boundary_single_item', () => {
// 边界单元素:仅版本号合法但缺文件报错。
const { errors } = validateSoftwareVersionUpload({ version: '1.0.0', file: null })
- assert.equal(errors.file, '请选择安装包文件')
+ assert.equal(errors.file, '请选择 zip 压缩包')
})
test('test_task_128_version_upload_boundary_limit_or_missing_field', () => {
@@ -48,7 +48,9 @@ test('test_task_128_version_upload_boundary_limit_or_missing_field', () => {
})
test('test_task_128_version_upload_invalid_input_rejected', () => {
- // 异常输入:非法版本字符仅去空白,不做语义强校验。
+ // 异常输入:非 zip 文件被拒;非法版本字符仅去空白,不做语义强校验。
+ const { errors } = validateSoftwareVersionUpload({ version: '1.0.0', file: { name: 'x.exe', size: 5 } })
+ assert.equal(errors.file, '仅支持 .zip 格式')
const request = buildSoftwareVersionUploadRequest({ version: ' v1 ', file: { name: 'x.zip', size: 1 } })
assert.equal(request.version, 'v1')
})
diff --git a/admin-frontend-vue/tests/task-260.test.ts b/admin-frontend-vue/tests/task-260.test.ts
new file mode 100644
index 00000000..57b94579
--- /dev/null
+++ b/admin-frontend-vue/tests/task-260.test.ts
@@ -0,0 +1,72 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { readSource } from './helpers.ts'
+import { validateSoftwareVersionUpload } from '../src/pages/records/version-upload-model.ts'
+import { parseSoftwareVersionUpload } from '../src/pages/records/version-model.ts'
+import { formatFileSize } from '../src/pages/records/version-format.ts'
+
+// module 13 task 260:软件版本页对齐 admin panel-version(zip 校验文案/发布成功含链接/列表+下载/空态)。
+
+test('test_task_260_version_normal_primary_path', () => {
+ // 正常主路径:上传表单校验通过;发布成功解析返回新版本行。
+ const { valid, errors } = validateSoftwareVersionUpload({ version: '1.0.2', file: { name: 'a.zip', size: 2048 } })
+ assert.equal(valid, true)
+ assert.deepEqual(errors, {})
+ const item = parseSoftwareVersionUpload({
+ success: true,
+ data: { item: { id: 1, version: '1.0.2', file_url: 'https://oss/x/1.0.2.zip', created_at: '2026-09-05T10:00:00' } },
+ })
+ assert.equal(item?.version, '1.0.2')
+ assert.equal(item?.fileUrl, 'https://oss/x/1.0.2.zip')
+})
+
+test('test_task_260_version_normal_variant_input', () => {
+ // 正常变体:上传无 item 时回 null,不抛。
+ assert.equal(parseSoftwareVersionUpload({ success: true, data: {} }), null)
+ assert.equal(parseSoftwareVersionUpload({ success: true }), null)
+})
+
+test('test_task_260_version_normal_repeated_operation_is_idempotent', () => {
+ assert.equal(parseSoftwareVersionUpload({ success: true, data: { item: { id: 1, version: 'v' } } })?.version, 'v')
+ assert.equal(parseSoftwareVersionUpload({ success: true, data: { item: { id: 1, version: 'v' } } })?.version, 'v')
+})
+
+test('test_task_260_version_boundary_empty_input', () => {
+ // 边界:空版本/空文件报对应文案(对齐 admin.js)。
+ const { errors } = validateSoftwareVersionUpload({ version: '', file: null })
+ assert.equal(errors.version, '请填写版本号')
+ assert.equal(errors.file, '请选择 zip 压缩包')
+})
+
+test('test_task_260_version_boundary_single_item', () => {
+ // 边界:非 zip 扩展名被拒。
+ const { errors } = validateSoftwareVersionUpload({ version: '1.0.0', file: { name: 'x.exe', size: 100 } })
+ assert.equal(errors.file, '仅支持 .zip 格式')
+ const ok = validateSoftwareVersionUpload({ version: '1.0.0', file: { name: 'X.ZIP', size: 100 } })
+ assert.equal(ok.valid, true, '扩展名大小写不敏感')
+})
+
+test('test_task_260_version_boundary_limit_or_missing_field', () => {
+ // 边界上限:文件字节格式化仍可用。
+ assert.equal(formatFileSize(1536), '1.5 KB')
+})
+
+test('test_task_260_version_invalid_input_rejected', () => {
+ // 异常:发布失败有兜底文案、页面上传 accept 限 .zip。
+ const page = readSource('src/pages/records/RecordsSoftwareVersionPage.vue')
+ assert.match(page, /请填写版本号/)
+ assert.match(page, /请选择 zip 压缩包/)
+ assert.match(page, /仅支持 \.zip 格式/)
+ assert.match(page, /accept="\.zip"/)
+ assert.match(page, /发布失败/)
+})
+
+test('test_task_260_version_dependency_failure_returns_actionable_message', () => {
+ // 依赖失败:成功文案含版本与链接;上传经 API 适配并解析新版本;空态兜底。
+ const page = readSource('src/pages/records/RecordsSoftwareVersionPage.vue')
+ assert.match(page, /发布成功。版本:/, '成功文案需含版本')
+ assert.match(page, /,链接:/, '成功文案需含链接')
+ assert.match(page, /暂无版本/, '空态文案')
+ const api = readSource('src/pages/records/version-api.ts')
+ assert.match(api, /parseSoftwareVersionUpload/, '上传适配解析新版本行')
+})