/** 用户菜单授权数据解析模型(任务 49):permission-menus 扁平项 → 嵌套树 + 用户已授权 id。 */ import { unwrap } from '../../api/envelope.ts' /** 后台 Vue 控制台菜单类型(与 /api/admin/current-user/menus 一致)。 */ export const ADMIN_MENU_TYPE = 'admin' /** 前端客户端菜单类型(桌面端功能模块,如视频/前端工具/运营工具/后勤工具)。 */ export const APP_MENU_TYPE = 'app' /** 授权用可点选菜单节点(携带后端 numeric column id,用于提交授权)。 */ export interface MenuOptionNode { id: number /** column_key,稳定权限标识;缺省用 id 字符串兜底。 */ key: string name: string sort: number parentId: number | null /** 所属菜单类型(admin 后台 / app 前端客户端),提交时按类型分区落库。 */ type: string children?: MenuOptionNode[] } function numberOrNull(value: unknown): number | null { return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : null } /** 把单条 PermissionMenuItemVo(snake_case) 归一为授权节点;缺 id 视为无效。 */ export function parsePermissionMenuItem(raw: unknown, type = ''): MenuOptionNode | 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 sortRaw = numberOrNull(record.sort_order) const columnKey = typeof record.column_key === 'string' ? record.column_key.trim() : '' return { id, key: columnKey || String(id), name: typeof record.name === 'string' && record.name.trim() ? record.name.trim() : '未命名菜单', sort: sortRaw === null ? 0 : sortRaw, parentId: parentId === null ? null : parentId, type, } } /** 归一化 permission-menus 响应(信封或裸数组)为扁平授权节点列表。 */ export function parseMenuOptionList(payload: unknown, type = ''): MenuOptionNode[] { const data = unwrap(payload) if (!Array.isArray(data)) return [] return data .map((raw) => parsePermissionMenuItem(raw, type)) .filter((node): node is MenuOptionNode => node !== null) } /** 按 parent_id 组装嵌套树;同级按 (sort,key) 升序,父缺失的节点按根处理。 */ export function buildMenuOptionTree(items: MenuOptionNode[]): MenuOptionNode[] { const nodes = new Map() for (const item of items) { nodes.set(item.id, { ...item, children: [] }) } const roots: MenuOptionNode[] = [] 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: MenuOptionNode, b: MenuOptionNode) => a.sort - b.sort || a.key.localeCompare(b.key) const sortRecursive = (nodes: MenuOptionNode[]) => { 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 } /** 解包用户已授权 columnIds(data.columnIds)→ 去重升序正整数。 */ export function parseUserGrantedColumnIds(payload: unknown): number[] { const data = unwrap(payload) const record = data && typeof data === 'object' ? (data as Record) : null if (!record || !Array.isArray(record.columnIds)) return [] return normalizeMenuIds(record.columnIds) } /** 任意输入 id 集合归一为去重升序正整数(镜像后端 normalizeColumnIds)。 */ export function normalizeMenuIds(raw: unknown): number[] { if (!Array.isArray(raw)) return [] const ids = new Set() for (const value of raw) { const id = numberOrNull(value) if (id !== null && id >= 1) ids.add(id) } return [...ids].sort((a, b) => a - b) } /** 提交授权请求体(任务 50):{columnIds} 与 Java UserColumnPermissionUpdateRequest 对齐。 */ export function buildUserColumnPermissionPayload(raw: unknown): { columnIds: number[] } { return { columnIds: normalizeMenuIds(raw) } }