Files
crawler-plugin/admin-frontend-vue/src/pages/account/menu-manage-model.ts
T
huangzd1997 50a6cf12e7 task-57(账号/权限页面): 实现菜单删除前置校验
menu-manage-model.ts 增 menuDeleteReason 镜像后端仅删叶子约束(有子先删);menu-manage-api.ts
增 deleteMenu DELETE /api/admin/permission-menus/{id};MenusPage 行删除二次确认、
带子节点禁用/拦截并刷新。8 用例全过,419 单测 + build 绿。
2026-09-05 15:20:39 +08:00

215 lines
7.4 KiB
TypeScript

/** 菜单管理 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<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),
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)
}
/** 按 parentId 组装展示树;同级按 (sortOrder,name) 升序,父缺失按根处理。 */
export function buildMenuManageTree(items: MenuManageNode[]): MenuManageNode[] {
const nodes = new Map<number, MenuManageNode>()
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)
}