/** * 完整模拟 MenuProcessor 过滤链 * 用法: npx tsx scripts/simulate-menu-filter-full.ts */ import { asyncRoutes } from '../src/router/routes/asyncRoutes' import type { AppRouteRecord } from '../src/types/router' const RoutesAlias = { Layout: '/index/index' } const menuCodesFromApi = [ 'bx:advanceApply:list', 'bx:advanceApply:submit', 'bx:advanceApprove:approve', 'bx:advanceApprove:list', 'bx:advanceIssue:issue', 'bx:advanceIssue:list', 'bx:expenseItem:add', 'bx:expenseItem:edit', 'bx:expenseItem:list', 'bx:expenseItem:remove', 'bx:payment:list', 'bx:payment:pay', 'bx:reimburseApply:list', 'bx:reimburseApply:submit', 'bx:reimburseApprove:list', 'bx:viewAll:list', 'bx:viewMine:list', 'bx:viewTodo:list', 'bx:workflowAdvance:list', 'bx:workflowReimburse:list', 'crm:clue:add', 'crm:clue:assign', 'crm:clue:convert', 'crm:clue:edit', 'crm:clue:export', 'crm:clue:follow', 'crm:clue:import', 'crm:clue:invalid', 'crm:clue:list', 'crm:clue:query', 'crm:clue:remove', 'crm:contract:list', 'crm:customer:add', 'crm:customer:deal', 'crm:customer:edit', 'crm:customer:list', 'crm:customer:query', 'crm:customer:remove', 'crm:customer:toPool', 'crm:customer:transfer', 'crm:customerPool:assign', 'crm:customerPool:claim', 'crm:customerPool:list', 'crm:opportunity:add', 'crm:opportunity:assign', 'crm:opportunity:edit', 'crm:opportunity:export', 'crm:opportunity:follow', 'crm:opportunity:list', 'crm:opportunity:query', 'crm:opportunity:remove', 'crm:product:list', 'crm:productCategory:list', 'crm:quote:list', 'db:dashboard:list', 'db:inventory:add', 'db:inventory:edit', 'db:inventory:list', 'db:inventory:query', 'db:inventory:remove', 'db:plan:add', 'db:plan:edit', 'db:plan:list', 'db:plan:query', 'db:plan:remove', 'db:weighing:add', 'db:weighing:edit', 'db:weighing:list', 'db:weighing:query', 'db:weighing:remove', 'db:weighing:void', 'pl:carrier:list', 'pl:document:list', 'pl:driver:list', 'pl:freight-approval:list', 'pl:freight-approval:query', 'pl:freight-disburse:list', 'pl:freight-disburse:query', 'pl:freight:approve', 'pl:freight:create', 'pl:freight:disburse', 'pl:freight:edit', 'pl:freight:list', 'pl:freight:query', 'pl:freight:submit', 'pl:freight:withdraw', 'pl:plan-archive:archive', 'pl:plan-archive:edit', 'pl:plan-archive:list', 'pl:plan-archive:query', 'pl:plan-cert:list', 'pl:plan:list', 'pl:self-vehicle:list', 'pl:transport-address:list', 'pl:vehicle-maintenance-apply:add', 'pl:vehicle-maintenance-apply:cancel', 'pl:vehicle-maintenance-apply:edit', 'pl:vehicle-maintenance-apply:list', 'pl:vehicle-maintenance-apply:query', 'pl:vehicle-maintenance-apply:remove', 'pl:vehicle-maintenance-process:add', 'pl:vehicle-maintenance-process:edit', 'pl:vehicle-maintenance-process:list', 'pl:vehicle-maintenance-process:query', 'pl:vehicle-maintenance-process:remove', 'pl:vehicle-recharge-apply:add', 'pl:vehicle-recharge-apply:cancel', 'pl:vehicle-recharge-apply:edit', 'pl:vehicle-recharge-apply:list', 'pl:vehicle-recharge-apply:query', 'pl:vehicle-recharge-apply:remove', 'pl:vehicle-recharge-record:add', 'pl:vehicle-recharge-record:edit', 'pl:vehicle-recharge-record:list', 'pl:vehicle-recharge-record:query', 'pl:vehicle-recharge-record:remove', 'pl:vehicle-urea-apply:add', 'pl:vehicle-urea-apply:cancel', 'pl:vehicle-urea-apply:edit', 'pl:vehicle-urea-apply:list', 'pl:vehicle-urea-apply:query', 'pl:vehicle-urea-apply:remove', 'pl:vehicle-urea-disburse:add', 'pl:vehicle-urea-disburse:edit', 'pl:vehicle-urea-disburse:list', 'pl:vehicle-urea-disburse:query', 'pl:vehicle-urea-disburse:remove', 'pl:vehicle:list' ] function normalizeMenuCode(input: unknown): string { const raw = String(input ?? '').trim() if (!raw) return '' return raw .replace(/[\s-]+/g, '_') .replace(/([a-z0-9])([A-Z])/g, '$1_$2') .toLowerCase() } function filterMenuByRoles(menu: AppRouteRecord[], roles: string[]): AppRouteRecord[] { return menu.reduce((acc: AppRouteRecord[], item) => { const itemRoles = item.meta?.roles const hasPermission = !itemRoles || itemRoles.some((role) => roles?.includes(role)) if (hasPermission) { const filteredItem = { ...item } if (filteredItem.children?.length) { filteredItem.children = filterMenuByRoles(filteredItem.children, roles) } acc.push(filteredItem) } return acc }, []) } function filterMenuByCodes(menu: AppRouteRecord[], codes: Set): AppRouteRecord[] { return menu.reduce((acc: AppRouteRecord[], item) => { const rawCode = (item.meta as any)?.menuCode ?? item.name const code = normalizeMenuCode(rawCode) const hasSelfPermission = !!code && codes.has(code) const filteredChildren = item.children?.length ? filterMenuByCodes(item.children, codes) : item.children const shouldKeep = hasSelfPermission || (Array.isArray(filteredChildren) && filteredChildren.length > 0) if (shouldKeep) { acc.push({ ...item, children: filteredChildren }) } return acc }, []) } function filterDashboardMenus(menuList: AppRouteRecord[]): AppRouteRecord[] { return menuList .filter((item) => { const path = String(item.path ?? '') const title = String(item.meta?.title ?? '') const name = String(item.name ?? '') return !(path === '/dashboard' || title === '控制台' || name === 'Dashboard') }) .map((item) => { if (!item.children?.length) return item return { ...item, children: filterDashboardMenus(item.children) } }) } function filterEmptyMenus(menuList: AppRouteRecord[]): AppRouteRecord[] { return menuList .map((item) => { if (item.children?.length) { return { ...item, children: filterEmptyMenus(item.children) } } return item }) .filter((item) => { if (Array.isArray(item.children)) { return item.children.length > 0 } if (item.meta?.isIframe === true || item.meta?.link) return true if (item.component && item.component !== '' && item.component !== RoutesAlias.Layout) { return true } return false }) } function countLeaves(menu: AppRouteRecord[]): number { let n = 0 for (const item of menu) { if (item.children?.length) n += countLeaves(item.children) else if (item.component && item.component !== RoutesAlias.Layout) n++ } return n } function topLevelSummary(menu: AppRouteRecord[]) { return menu.map((m) => ({ path: m.path, title: m.meta?.title, childGroups: m.children?.length ?? 0, leaves: m.children ? countLeaves(m.children) : 0 })) } // simulate scenarios for (const roles of [['admin'], ['common'], ['R_ADMIN'], undefined as string[] | undefined]) { let menuList = [...asyncRoutes] if (roles?.length && !roles.includes('super_admin')) { menuList = filterMenuByRoles(menuList, roles) } const codeSet = new Set(menuCodesFromApi.map(normalizeMenuCode)) const afterRoles = menuList.length const filtered = filterMenuByCodes(menuList, codeSet) let afterCodes = filtered if (!(filtered.length > 0 || codeSet.size === 0)) { afterCodes = menuList // fail-open } const final = filterEmptyMenus(filterDashboardMenus(afterCodes)) console.log('\n=== roles:', roles, '===') console.log('after role filter top modules:', afterRoles) console.log('after code filter top modules:', filtered.length) console.log('after code filter summary:', topLevelSummary(filtered)) console.log('FINAL top modules:', final.length) console.log(topLevelSummary(final)) } // debug filterEmptyMenus step by step for admin { let menuList = filterMenuByRoles([...asyncRoutes], ['admin']) const codeSet = new Set(menuCodesFromApi.map(normalizeMenuCode)) menuList = filterMenuByCodes(menuList, codeSet) menuList = filterDashboardMenus(menuList) console.log('\n=== DEBUG filterEmptyMenus ===') for (const top of menuList) { console.log('TOP', top.path, 'children', top.children?.length) if (top.children?.length) { for (const c of top.children) { console.log(' L2', c.path, 'component', c.component, 'children', c.children?.length) if (c.children?.length) { for (const g of c.children.slice(0, 3)) { console.log(' L3', g.path, 'component', g.component, 'menuCode', (g.meta as any)?.menuCode) } } } } } const afterEmpty = filterEmptyMenus(menuList) console.log('after filterEmptyMenus tops:', afterEmpty.length) }