task-53(账号/权限页面): 定义菜单管理 DTO
新增 menu-manage-model.ts 纯 DTO:parseMenuManageItem/List 归一 PermissionMenuItemVo(snake)、 MenuManageForm 工厂、validateMenuManageForm 镜像后端 NotBlank 文案、toMenuCreate/UpdateRequest camelCase 对齐 Java PermissionMenuCreate/UpdateRequest(menuType=admin 固定)。8 用例全过, 387 单测 + build 绿。
This commit is contained in:
@@ -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<string, unknown>
|
||||
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<unknown>(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)
|
||||
}
|
||||
@@ -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/)
|
||||
})
|
||||
Reference in New Issue
Block a user