diff --git a/admin-frontend-vue/src/pages/records/version-api.ts b/admin-frontend-vue/src/pages/records/version-api.ts new file mode 100644 index 00000000..a992fe08 --- /dev/null +++ b/admin-frontend-vue/src/pages/records/version-api.ts @@ -0,0 +1,11 @@ +/** 软件版本列表加载适配(任务 127):GET /api/admin/versions。 */ +import { http } from '@/api/http' +import { parseSoftwareVersionList } from './version-model.ts' +import type { SoftwareVersionList } from './version-dto.ts' + +export const SOFTWARE_VERSIONS_ENDPOINT = '/api/admin/versions' + +export async function fetchSoftwareVersions(): Promise { + const { data } = await http.get(SOFTWARE_VERSIONS_ENDPOINT) + return parseSoftwareVersionList(data) +} diff --git a/admin-frontend-vue/src/pages/records/version-model.ts b/admin-frontend-vue/src/pages/records/version-model.ts new file mode 100644 index 00000000..a30a6894 --- /dev/null +++ b/admin-frontend-vue/src/pages/records/version-model.ts @@ -0,0 +1,35 @@ +/** 软件版本列表加载模型(任务 127):解析 /api/admin/versions data.items(snake);纯逻辑。 */ +import { unwrap } from '../../api/envelope.ts' +import { type SoftwareVersionItem, type SoftwareVersionList } from './version-dto.ts' + +function text(value: unknown): string { + return typeof value === 'string' ? value.trim() : '' +} + +function numberOrNull(value: unknown): number | null { + return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : null +} + +/** 解析单条软件版本行;缺 id 视为无效。 */ +export function toSoftwareVersionItem(raw: unknown): SoftwareVersionItem | null { + if (!raw || typeof raw !== 'object') return null + const r = raw as Record + const id = numberOrNull(r.id) + if (id === null) return null + return { + id, + version: text(r.version), + fileUrl: text(r.file_url ?? r.fileUrl), + createdAt: text(r.created_at ?? r.createdAt), + } +} + +/** 归一化版本列表负载为前端结果;缺省回空列表。 */ +export function parseSoftwareVersionList(payload: unknown): SoftwareVersionList { + const data = unwrap(payload) + const record = data && typeof data === 'object' ? (data as Record) : {} + const items = Array.isArray(record.items) + ? record.items.map((raw) => toSoftwareVersionItem(raw)).filter((item): item is SoftwareVersionItem => item !== null) + : [] + return { items } +} diff --git a/admin-frontend-vue/tests/task-127.test.ts b/admin-frontend-vue/tests/task-127.test.ts new file mode 100644 index 00000000..e60d08c6 --- /dev/null +++ b/admin-frontend-vue/tests/task-127.test.ts @@ -0,0 +1,58 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { parseSoftwareVersionList } from '../src/pages/records/version-model.ts' + +test('test_task_127_version_list_load_normal_primary_path', () => { + // 正常主路径:snake 版本列表负载解析。 + const list = parseSoftwareVersionList({ + success: true, + data: { items: [{ id: 1, version: '1.0.46', file_url: 'https://oss/a.zip', created_at: '2026-01-01 10:00' }] }, + }) + assert.equal(list.items.length, 1) + assert.equal(list.items[0].version, '1.0.46') + assert.equal(list.items[0].fileUrl, 'https://oss/a.zip') +}) + +test('test_task_127_version_list_load_normal_variant_input', () => { + // 正常变体:缺 file_url 行字段默认空。 + const list = parseSoftwareVersionList({ data: { items: [{ id: 2, version: '1.0.0' }] } }) + assert.equal(list.items[0].fileUrl, '') +}) + +test('test_task_127_version_list_load_repeated_is_idempotent', () => { + // 正常重复:解析稳定、不改输入。 + const payload = { data: { items: [{ id: 1, version: '1' }] } } + assert.deepEqual(parseSoftwareVersionList(payload), parseSoftwareVersionList(payload)) +}) + +test('test_task_127_version_list_load_boundary_empty_input', () => { + // 边界空值:空列表负载。 + assert.deepEqual(parseSoftwareVersionList({ data: { items: [] } }).items, []) +}) + +test('test_task_127_version_list_load_boundary_single_item', () => { + // 边界单元素:单版本;缺 id 行过滤。 + const list = parseSoftwareVersionList({ data: { items: [{ id: 9, version: 'v' }, { version: 'no-id' }] } }) + assert.equal(list.items.length, 1) +}) + +test('test_task_127_version_list_load_boundary_limit_or_missing_field', () => { + // 边界上限/缺字段:非对象版本行过滤。 + const list = parseSoftwareVersionList({ data: { items: ['bad'] } }) + assert.deepEqual(list.items, []) +}) + +test('test_task_127_version_list_load_invalid_input_rejected', () => { + // 异常输入:success=false 抛后端 message。 + assert.throws(() => parseSoftwareVersionList({ success: false, message: '无权访问版本管理' }), /无权访问/) +}) + +test('test_task_127_version_list_load_dependency_failure_returns_actionable_message', () => { + // 依赖失败/加载走 adapter:GET /api/admin/versions。 + const api = readSource('src/pages/records/version-api.ts') + assert.match(api, /\/api\/admin\/versions/) + assert.match(api, /fetchSoftwareVersions/) + const model = readSource('src/pages/records/version-model.ts') + assert.equal(/axios|http\./.test(model), false, '版本解析保持纯逻辑') +})