/** 菜单管理 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 children?: MenuManageNode[] } 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), 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) } /** 按 parentId 组装展示树;同级按 (sortOrder,name) 升序,父缺失按根处理。 */ export function buildMenuManageTree(items: MenuManageNode[]): MenuManageNode[] { const nodes = new Map() for (const item of items) { nodes.set(item.id, { ...item, children: [] }) } const roots: MenuManageNode[] = [] for (const item of items) { const node = nodes.get(item.id) if (!node) continue const parent = item.parentId === null ? undefined : nodes.get(item.parentId) if (parent) { parent.children!.push(node) } else { roots.push(node) } } const order = (a: MenuManageNode, b: MenuManageNode) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name) const sortRecursive = (nodes: MenuManageNode[]) => { nodes.sort(order) for (const node of nodes) node.children?.length && sortRecursive(node.children) } sortRecursive(roots) for (const item of items) { const node = nodes.get(item.id) if (node && !node.children?.length) delete node.children } return roots } /** 展平树为列表(前序),供父级下拉/统计复用。 */ export function flattenMenuNodes(nodes: MenuManageNode[]): MenuManageNode[] { const out: MenuManageNode[] = [] const walk = (list: MenuManageNode[]) => { for (const node of list) { out.push(node) if (node.children?.length) walk(node.children) } } walk(nodes) return out } /** 同级建议排序:该父下最大 sortOrder + 1;无同级返回 0。 */ export function nextSiblingSort(items: MenuManageNode[], parentId: number | null): number { let max = -1 for (const item of items) { if (item.parentId === parentId && item.sortOrder > max) max = item.sortOrder } return max + 1 } /** 新建菜单表单工厂:指定父节点与建议排序,其余默认(menuType=admin)。 */ export function createMenuForm(parentId: number | null, sortOrder: number): MenuManageForm { return { ...emptyMenuManageForm(), parentId: typeof parentId === 'number' ? parentId : null, sortOrder: normalizeMenuSort(sortOrder), } } /** 编辑表单回填:从可编辑节点还原表单字段。 */ export function menuFormFromNode(node: MenuManageNode): MenuManageForm { return { name: node.name, columnKey: node.columnKey, parentId: node.parentId, menuType: node.menuType, routePath: node.routePath, sortOrder: node.sortOrder, } } /** 编辑时父节点防环校验:不可选自身或其子孙为父;返回可操作原因,否则 undefined。 */ export function menuParentChangeReason(node: MenuManageNode, parentId: number | null): string | undefined { if (parentId === null || parentId === undefined) return undefined if (parentId === node.id) return '不能选择自身作为父菜单' const descendants = flattenMenuNodes(node.children ?? []) if (descendants.some((item) => item.id === parentId)) return '不能选择自己的子菜单作为父菜单' return undefined } /** 删除前置校验(镜像后端:仅叶子可删,避免孤儿层级);可删返回 undefined。 */ export function menuDeleteReason(node: MenuManageNode | null | undefined): string | undefined { const children = node?.children if (Array.isArray(children) && children.length > 0) return '请先删除该菜单下的子菜单' return undefined } /** 新建/编辑表单空值工厂:默认菜单类型为 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) }