From 5d9019a40d4a918585888cf08e86ede08e694a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Sat, 5 Sep 2026 15:15:08 +0800 Subject: [PATCH] =?UTF-8?q?task-53(=E8=B4=A6=E5=8F=B7/=E6=9D=83=E9=99=90?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2):=20=E5=AE=9A=E4=B9=89=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E7=AE=A1=E7=90=86=20DTO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 menu-manage-model.ts 纯 DTO:parseMenuManageItem/List 归一 PermissionMenuItemVo(snake)、 MenuManageForm 工厂、validateMenuManageForm 镜像后端 NotBlank 文案、toMenuCreate/UpdateRequest camelCase 对齐 Java PermissionMenuCreate/UpdateRequest(menuType=admin 固定)。8 用例全过, 387 单测 + build 绿。 --- .../src/pages/account/menu-manage-model.ts | 123 ++++++++++++++++++ admin-frontend-vue/tests/task-53.test.ts | 94 +++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 admin-frontend-vue/src/pages/account/menu-manage-model.ts create mode 100644 admin-frontend-vue/tests/task-53.test.ts diff --git a/admin-frontend-vue/src/pages/account/menu-manage-model.ts b/admin-frontend-vue/src/pages/account/menu-manage-model.ts new file mode 100644 index 00000000..6887d677 --- /dev/null +++ b/admin-frontend-vue/src/pages/account/menu-manage-model.ts @@ -0,0 +1,123 @@ +/** 菜单管理 DTO/模型(任务 53):扁平 PermissionMenuItemVo → 可编辑节点 + 创建/更新请求体。 */ +import { unwrap } from '../../api/envelope.ts' + +/** 后台 Vue 控制台菜单类型。 */ +export const ADMIN_MENU_TYPE = 'admin' + +export interface MenuManageNode { + id: number + name: string + /** column_key,稳定权限标识。 */ + columnKey: string + parentId: number | null + rootColumnKey?: string + menuType: string + routePath: string + sortOrder: number +} + +export interface MenuManageForm { + name: string + columnKey: string + parentId: number | null + menuType: string + routePath: string + sortOrder: number +} + +/** 请求体字段与 Java PermissionMenuCreate/UpdateRequest 对齐(camelCase)。 */ +export interface MenuManagePayload { + name: string + columnKey: string + parentId: number | null + menuType: string + routePath: string + sortOrder: number +} + +export interface MenuManageFormErrors { + name?: string + columnKey?: string + menuType?: string + routePath?: string +} + +function numberOrNull(value: unknown): number | null { + return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : null +} + +function text(value: unknown): string { + return typeof value === 'string' ? value.trim() : '' +} + +/** 排序归一:非负整数,非法回 0。 */ +export function normalizeMenuSort(value: unknown): number { + const n = numberOrNull(value) + return n !== null && n >= 0 ? n : 0 +} + +/** 把单条 PermissionMenuItemVo(snake_case) 归一为可编辑节点;缺 id 视为无效。 */ +export function parseMenuManageItem(raw: unknown): MenuManageNode | null { + if (!raw || typeof raw !== 'object') return null + const record = raw as Record + const id = numberOrNull(record.id) + if (id === null) return null + const parentId = numberOrNull(record.parent_id) + const node: MenuManageNode = { + id, + name: text(record.name), + columnKey: text(record.column_key) || String(id), + parentId: parentId === null ? null : parentId, + menuType: text(record.menu_type) || ADMIN_MENU_TYPE, + routePath: text(record.route_path), + sortOrder: normalizeMenuSort(record.sort_order), + } + const rootColumnKey = text(record.root_column_key) + if (rootColumnKey) node.rootColumnKey = rootColumnKey + return node +} + +/** 归一化 permission-menus 响应(信封或裸数组)为可编辑节点列表。 */ +export function parseMenuManageList(payload: unknown): MenuManageNode[] { + const data = unwrap(payload) + if (!Array.isArray(data)) return [] + return data + .map((raw) => parseMenuManageItem(raw)) + .filter((node): node is MenuManageNode => node !== null) +} + +/** 新建/编辑表单空值工厂:默认菜单类型为 admin、排序 0、根父节点。 */ +export function emptyMenuManageForm(): MenuManageForm { + return { name: '', columnKey: '', parentId: null, menuType: ADMIN_MENU_TYPE, routePath: '', sortOrder: 0 } +} + +/** 表单校验(镜像后端 NotBlank 文案);返回逐字段错误。 */ +export function validateMenuManageForm(form: MenuManageForm): { valid: boolean; errors: MenuManageFormErrors } { + const errors: MenuManageFormErrors = {} + if (!text(form.name)) errors.name = '菜单名称不能为空' + if (!text(form.columnKey)) errors.columnKey = '菜单标识不能为空' + if (!text(form.menuType)) errors.menuType = '菜单类型不能为空' + if (!text(form.routePath)) errors.routePath = '菜单路由不能为空' + return { valid: Object.keys(errors).length === 0, errors } +} + +function toMenuManagePayload(form: MenuManageForm): MenuManagePayload { + return { + name: text(form.name), + columnKey: text(form.columnKey), + parentId: typeof form.parentId === 'number' ? form.parentId : null, + menuType: text(form.menuType), + routePath: text(form.routePath), + sortOrder: normalizeMenuSort(form.sortOrder), + } +} + +/** 表单 → POST /api/admin/permission-menus 请求体。 */ +export function toMenuCreateRequest(form: MenuManageForm): MenuManagePayload { + return toMenuManagePayload(form) +} + +/** 表单 → PUT /api/admin/permission-menus/{id} 请求体。 */ +export function toMenuUpdateRequest(form: MenuManageForm): MenuManagePayload { + return toMenuManagePayload(form) +} diff --git a/admin-frontend-vue/tests/task-53.test.ts b/admin-frontend-vue/tests/task-53.test.ts new file mode 100644 index 00000000..3202e263 --- /dev/null +++ b/admin-frontend-vue/tests/task-53.test.ts @@ -0,0 +1,94 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { + ADMIN_MENU_TYPE, + emptyMenuManageForm, + parseMenuManageItem, + parseMenuManageList, + toMenuCreateRequest, + toMenuUpdateRequest, + validateMenuManageForm, +} from '../src/pages/account/menu-manage-model.ts' + +test('test_task_053_menu_manage_dto_normal_primary_path', () => { + // 正常主路径:扁平 PermissionMenuItemVo(snake) 解析为可编辑菜单节点。 + const node = parseMenuManageItem({ + id: 12, + name: '用户管理', + column_key: 'account_users', + parent_id: 1, + root_column_key: 'account', + menu_type: 'admin', + route_path: '/account/users', + sort_order: 3, + }) + assert.equal(node?.id, 12) + assert.equal(node?.columnKey, 'account_users') + assert.equal(node?.parentId, 1) + assert.equal(node?.rootColumnKey, 'account') + assert.equal(node?.menuType, 'admin') + assert.equal(node?.routePath, '/account/users') + assert.equal(node?.sortOrder, 3) +}) + +test('test_task_053_menu_manage_dto_normal_variant_input', () => { + // 正常变体:分组节点缺 route_path/sort 用默认;缺 id 行被过滤。 + const node = parseMenuManageItem({ id: 20, name: '账号', column_key: 'account', parent_id: null }) + assert.equal(node?.routePath, '') + assert.equal(node?.sortOrder, 0) + const list = parseMenuManageList([{ id: 21, name: 'x', column_key: 'k' }, { name: 'no-id' }]) + assert.equal(list.length, 1) +}) + +test('test_task_053_menu_manage_dto_normal_repeated_operation_is_idempotent', () => { + // 正常重复:解析与序列化不改输入、结果稳定。 + const raw = { id: 3, name: 'A', column_key: 'a', parent_id: null, sort_order: 1 } + assert.deepEqual(toMenuCreateRequest(emptyMenuManageForm()), toMenuCreateRequest(emptyMenuManageForm())) + const node = parseMenuManageItem(raw) + assert.equal(node?.name, 'A') + assert.equal((raw as { name: string }).name, 'A') +}) + +test('test_task_053_menu_manage_dto_boundary_empty_input', () => { + // 边界空值:空负载回空列表;缺省表单带 admin 类型与默认排序。 + assert.deepEqual(parseMenuManageList({}), []) + const form = emptyMenuManageForm() + assert.equal(form.menuType, ADMIN_MENU_TYPE) + assert.equal(form.sortOrder, 0) + assert.equal(form.parentId, null) +}) + +test('test_task_053_menu_manage_dto_boundary_single_item', () => { + // 边界单元素:最小 id+name 可解析。 + assert.equal(parseMenuManageItem({ id: 9, name: 'Only' })?.id, 9) +}) + +test('test_task_053_menu_manage_dto_boundary_limit_or_missing_field', () => { + // 边界上限/缺字段:请求体字段与 Java 对齐(必填含 menuType/routePath);排序取整。 + const req = toMenuUpdateRequest({ ...emptyMenuManageForm(), name: 'n', columnKey: 'c', routePath: '/r', sortOrder: 2.7 }) + assert.equal(req.sortOrder, 2) + assert.ok('menuType' in req) + assert.ok('routePath' in req) + assert.equal(req.columnKey, 'c') +}) + +test('test_task_053_menu_manage_dto_invalid_input_rejected', () => { + // 异常输入:必填缺失/空白给出与后端一致的校验文案。 + const r = validateMenuManageForm(emptyMenuManageForm()) + assert.equal(r.valid, false) + assert.ok(r.errors.name) + assert.ok(r.errors.columnKey) + assert.ok(r.errors.routePath) + assert.equal(parseMenuManageItem('garbage'), null) +}) + +test('test_task_053_menu_manage_dto_dependency_failure_returns_actionable_message', () => { + // 依赖失败/可操作:DTO 纯逻辑、字段与 Java create/update 请求一致、不内联 http。 + const mod = readSource('src/pages/account/menu-manage-model.ts') + assert.equal(/axios|http\./.test(mod), false, '菜单管理 DTO 保持纯逻辑') + assert.match(mod, /columnKey/) + assert.match(mod, /parentId/) + assert.match(mod, /routePath/) + assert.match(mod, /menuType/) +})