/** * 模拟 MenuProcessor 过滤逻辑,排查菜单被滤空原因。 * 用法: node scripts/simulate-menu-filter.mjs */ import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' const __dirname = path.dirname(fileURLToPath(import.meta.url)) function normalizeMenuCode(input) { const raw = String(input ?? '').trim() if (!raw) return '' const withUnderscore = raw .replace(/[\s-]+/g, '_') .replace(/([a-z0-9])([A-Z])/g, '$1_$2') return withUnderscore.toLowerCase() } function filterMenuByRoles(menu, roles) { return menu.reduce((acc, 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, codes, level = 1) { return menu.reduce((acc, item) => { const rawCode = item.meta?.menuCode ?? item.name const code = normalizeMenuCode(rawCode) const hasSelfPermission = !!code && codes.has(code) const filteredChildren = item.children?.length ? filterMenuByCodes(item.children, codes, level + 1) : item.children const shouldKeep = hasSelfPermission || (Array.isArray(filteredChildren) && filteredChildren.length > 0) if (shouldKeep) { acc.push({ ...item, children: filteredChildren }) } return acc }, []) } const Layout = '/index/index' function filterEmptyMenus(menuList) { return menuList .map((item) => { if (item.children?.length) { return { ...item, children: filterEmptyMenus(item.children) } } return item }) .filter((item) => { if ('children' in item) { return Array.isArray(item.children) && item.children.length > 0 } if (item.meta?.isIframe === true || item.meta?.link) return true if (item.component && item.component !== '' && item.component !== Layout) return true return false }) } function extractMenuCodesFromFile(content) { const re = /menuCode:\s*['"]([^'"]+)['"]/g const codes = [] let m while ((m = re.exec(content))) codes.push(m[1]) return codes } 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' ] const sqxp = fs.readFileSync(path.join(__dirname, '../src/router/modules/sqxp.ts'), 'utf8') const pl = fs.readFileSync(path.join(__dirname, '../src/router/modules/pl.ts'), 'utf8') const routeMenuCodes = [...extractMenuCodesFromFile(sqxp), ...extractMenuCodesFromFile(pl)] const apiSet = new Set(menuCodesFromApi.map(normalizeMenuCode)) const missingInApi = routeMenuCodes.filter((c) => !apiSet.has(normalizeMenuCode(c))) const listPermsInApi = menuCodesFromApi.filter((c) => c.endsWith(':list')) console.log('API menuCodes count:', menuCodesFromApi.length) console.log('Route menuCode count:', routeMenuCodes.length) console.log('Route codes NOT in API (normalized):', missingInApi.length) missingInApi.slice(0, 30).forEach((c) => console.log(' -', c, '->', normalizeMenuCode(c))) // Build minimal tree from route files is hard; import compiled - skip, use dynamic import of ts not available // Instead eval route structure counts from grep console.log('\nList perms in API:', listPermsInApi.length)