simulate-menu-filter.mjs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. /**
  2. * 模拟 MenuProcessor 过滤逻辑,排查菜单被滤空原因。
  3. * 用法: node scripts/simulate-menu-filter.mjs
  4. */
  5. import fs from 'fs'
  6. import path from 'path'
  7. import { fileURLToPath } from 'url'
  8. const __dirname = path.dirname(fileURLToPath(import.meta.url))
  9. function normalizeMenuCode(input) {
  10. const raw = String(input ?? '').trim()
  11. if (!raw) return ''
  12. const withUnderscore = raw
  13. .replace(/[\s-]+/g, '_')
  14. .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
  15. return withUnderscore.toLowerCase()
  16. }
  17. function filterMenuByRoles(menu, roles) {
  18. return menu.reduce((acc, item) => {
  19. const itemRoles = item.meta?.roles
  20. const hasPermission = !itemRoles || itemRoles.some((role) => roles?.includes(role))
  21. if (hasPermission) {
  22. const filteredItem = { ...item }
  23. if (filteredItem.children?.length) {
  24. filteredItem.children = filterMenuByRoles(filteredItem.children, roles)
  25. }
  26. acc.push(filteredItem)
  27. }
  28. return acc
  29. }, [])
  30. }
  31. function filterMenuByCodes(menu, codes, level = 1) {
  32. return menu.reduce((acc, item) => {
  33. const rawCode = item.meta?.menuCode ?? item.name
  34. const code = normalizeMenuCode(rawCode)
  35. const hasSelfPermission = !!code && codes.has(code)
  36. const filteredChildren = item.children?.length
  37. ? filterMenuByCodes(item.children, codes, level + 1)
  38. : item.children
  39. const shouldKeep =
  40. hasSelfPermission || (Array.isArray(filteredChildren) && filteredChildren.length > 0)
  41. if (shouldKeep) {
  42. acc.push({ ...item, children: filteredChildren })
  43. }
  44. return acc
  45. }, [])
  46. }
  47. const Layout = '/index/index'
  48. function filterEmptyMenus(menuList) {
  49. return menuList
  50. .map((item) => {
  51. if (item.children?.length) {
  52. return { ...item, children: filterEmptyMenus(item.children) }
  53. }
  54. return item
  55. })
  56. .filter((item) => {
  57. if ('children' in item) {
  58. return Array.isArray(item.children) && item.children.length > 0
  59. }
  60. if (item.meta?.isIframe === true || item.meta?.link) return true
  61. if (item.component && item.component !== '' && item.component !== Layout) return true
  62. return false
  63. })
  64. }
  65. function extractMenuCodesFromFile(content) {
  66. const re = /menuCode:\s*['"]([^'"]+)['"]/g
  67. const codes = []
  68. let m
  69. while ((m = re.exec(content))) codes.push(m[1])
  70. return codes
  71. }
  72. const menuCodesFromApi = [
  73. 'bx:advanceApply:list', 'bx:advanceApply:submit', 'bx:advanceApprove:approve', 'bx:advanceApprove:list',
  74. 'bx:advanceIssue:issue', 'bx:advanceIssue:list', 'bx:expenseItem:add', 'bx:expenseItem:edit',
  75. 'bx:expenseItem:list', 'bx:expenseItem:remove', 'bx:payment:list', 'bx:payment:pay',
  76. 'bx:reimburseApply:list', 'bx:reimburseApply:submit', 'bx:reimburseApprove:list', 'bx:viewAll:list',
  77. 'bx:viewMine:list', 'bx:viewTodo:list', 'bx:workflowAdvance:list', 'bx:workflowReimburse:list',
  78. 'crm:clue:add', 'crm:clue:assign', 'crm:clue:convert', 'crm:clue:edit', 'crm:clue:export',
  79. 'crm:clue:follow', 'crm:clue:import', 'crm:clue:invalid', 'crm:clue:list', 'crm:clue:query',
  80. 'crm:clue:remove', 'crm:contract:list', 'crm:customer:add', 'crm:customer:deal', 'crm:customer:edit',
  81. 'crm:customer:list', 'crm:customer:query', 'crm:customer:remove', 'crm:customer:toPool',
  82. 'crm:customer:transfer', 'crm:customerPool:assign', 'crm:customerPool:claim', 'crm:customerPool:list',
  83. 'crm:opportunity:add', 'crm:opportunity:assign', 'crm:opportunity:edit', 'crm:opportunity:export',
  84. 'crm:opportunity:follow', 'crm:opportunity:list', 'crm:opportunity:query', 'crm:opportunity:remove',
  85. 'crm:product:list', 'crm:productCategory:list', 'crm:quote:list', 'db:dashboard:list', 'db:inventory:add',
  86. 'db:inventory:edit', 'db:inventory:list', 'db:inventory:query', 'db:inventory:remove', 'db:plan:add',
  87. 'db:plan:edit', 'db:plan:list', 'db:plan:query', 'db:plan:remove', 'db:weighing:add', 'db:weighing:edit',
  88. 'db:weighing:list', 'db:weighing:query', 'db:weighing:remove', 'db:weighing:void', 'pl:carrier:list',
  89. 'pl:document:list', 'pl:driver:list', 'pl:freight-approval:list', 'pl:freight-approval:query',
  90. 'pl:freight-disburse:list', 'pl:freight-disburse:query', 'pl:freight:approve', 'pl:freight:create',
  91. 'pl:freight:disburse', 'pl:freight:edit', 'pl:freight:list', 'pl:freight:query', 'pl:freight:submit',
  92. 'pl:freight:withdraw', 'pl:plan-archive:archive', 'pl:plan-archive:edit', 'pl:plan-archive:list',
  93. 'pl:plan-archive:query', 'pl:plan-cert:list', 'pl:plan:list', 'pl:self-vehicle:list',
  94. 'pl:transport-address:list', 'pl:vehicle-maintenance-apply:add', 'pl:vehicle-maintenance-apply:cancel',
  95. 'pl:vehicle-maintenance-apply:edit', 'pl:vehicle-maintenance-apply:list', 'pl:vehicle-maintenance-apply:query',
  96. 'pl:vehicle-maintenance-apply:remove', 'pl:vehicle-maintenance-process:add', 'pl:vehicle-maintenance-process:edit',
  97. 'pl:vehicle-maintenance-process:list', 'pl:vehicle-maintenance-process:query', 'pl:vehicle-maintenance-process:remove',
  98. 'pl:vehicle-recharge-apply:add', 'pl:vehicle-recharge-apply:cancel', 'pl:vehicle-recharge-apply:edit',
  99. 'pl:vehicle-recharge-apply:list', 'pl:vehicle-recharge-apply:query', 'pl:vehicle-recharge-apply:remove',
  100. 'pl:vehicle-recharge-record:add', 'pl:vehicle-recharge-record:edit', 'pl:vehicle-recharge-record:list',
  101. 'pl:vehicle-recharge-record:query', 'pl:vehicle-recharge-record:remove', 'pl:vehicle-urea-apply:add',
  102. 'pl:vehicle-urea-apply:cancel', 'pl:vehicle-urea-apply:edit', 'pl:vehicle-urea-apply:list',
  103. 'pl:vehicle-urea-apply:query', 'pl:vehicle-urea-apply:remove', 'pl:vehicle-urea-disburse:add',
  104. 'pl:vehicle-urea-disburse:edit', 'pl:vehicle-urea-disburse:list', 'pl:vehicle-urea-disburse:query',
  105. 'pl:vehicle-urea-disburse:remove', 'pl:vehicle:list'
  106. ]
  107. const sqxp = fs.readFileSync(path.join(__dirname, '../src/router/modules/sqxp.ts'), 'utf8')
  108. const pl = fs.readFileSync(path.join(__dirname, '../src/router/modules/pl.ts'), 'utf8')
  109. const routeMenuCodes = [...extractMenuCodesFromFile(sqxp), ...extractMenuCodesFromFile(pl)]
  110. const apiSet = new Set(menuCodesFromApi.map(normalizeMenuCode))
  111. const missingInApi = routeMenuCodes.filter((c) => !apiSet.has(normalizeMenuCode(c)))
  112. const listPermsInApi = menuCodesFromApi.filter((c) => c.endsWith(':list'))
  113. console.log('API menuCodes count:', menuCodesFromApi.length)
  114. console.log('Route menuCode count:', routeMenuCodes.length)
  115. console.log('Route codes NOT in API (normalized):', missingInApi.length)
  116. missingInApi.slice(0, 30).forEach((c) => console.log(' -', c, '->', normalizeMenuCode(c)))
  117. // Build minimal tree from route files is hard; import compiled - skip, use dynamic import of ts not available
  118. // Instead eval route structure counts from grep
  119. console.log('\nList perms in API:', listPermsInApi.length)