simulate-menu-filter-full.ts 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. /**
  2. * 完整模拟 MenuProcessor 过滤链
  3. * 用法: npx tsx scripts/simulate-menu-filter-full.ts
  4. */
  5. import { asyncRoutes } from '../src/router/routes/asyncRoutes'
  6. import type { AppRouteRecord } from '../src/types/router'
  7. const RoutesAlias = { Layout: '/index/index' }
  8. const menuCodesFromApi = [
  9. 'bx:advanceApply:list', 'bx:advanceApply:submit', 'bx:advanceApprove:approve', 'bx:advanceApprove:list',
  10. 'bx:advanceIssue:issue', 'bx:advanceIssue:list', 'bx:expenseItem:add', 'bx:expenseItem:edit',
  11. 'bx:expenseItem:list', 'bx:expenseItem:remove', 'bx:payment:list', 'bx:payment:pay',
  12. 'bx:reimburseApply:list', 'bx:reimburseApply:submit', 'bx:reimburseApprove:list', 'bx:viewAll:list',
  13. 'bx:viewMine:list', 'bx:viewTodo:list', 'bx:workflowAdvance:list', 'bx:workflowReimburse:list',
  14. 'crm:clue:add', 'crm:clue:assign', 'crm:clue:convert', 'crm:clue:edit', 'crm:clue:export',
  15. 'crm:clue:follow', 'crm:clue:import', 'crm:clue:invalid', 'crm:clue:list', 'crm:clue:query',
  16. 'crm:clue:remove', 'crm:contract:list', 'crm:customer:add', 'crm:customer:deal', 'crm:customer:edit',
  17. 'crm:customer:list', 'crm:customer:query', 'crm:customer:remove', 'crm:customer:toPool',
  18. 'crm:customer:transfer', 'crm:customerPool:assign', 'crm:customerPool:claim', 'crm:customerPool:list',
  19. 'crm:opportunity:add', 'crm:opportunity:assign', 'crm:opportunity:edit', 'crm:opportunity:export',
  20. 'crm:opportunity:follow', 'crm:opportunity:list', 'crm:opportunity:query', 'crm:opportunity:remove',
  21. 'crm:product:list', 'crm:productCategory:list', 'crm:quote:list', 'db:dashboard:list', 'db:inventory:add',
  22. 'db:inventory:edit', 'db:inventory:list', 'db:inventory:query', 'db:inventory:remove', 'db:plan:add',
  23. 'db:plan:edit', 'db:plan:list', 'db:plan:query', 'db:plan:remove', 'db:weighing:add', 'db:weighing:edit',
  24. 'db:weighing:list', 'db:weighing:query', 'db:weighing:remove', 'db:weighing:void', 'pl:carrier:list',
  25. 'pl:document:list', 'pl:driver:list', 'pl:freight-approval:list', 'pl:freight-approval:query',
  26. 'pl:freight-disburse:list', 'pl:freight-disburse:query', 'pl:freight:approve', 'pl:freight:create',
  27. 'pl:freight:disburse', 'pl:freight:edit', 'pl:freight:list', 'pl:freight:query', 'pl:freight:submit',
  28. 'pl:freight:withdraw', 'pl:plan-archive:archive', 'pl:plan-archive:edit', 'pl:plan-archive:list',
  29. 'pl:plan-archive:query', 'pl:plan-cert:list', 'pl:plan:list', 'pl:self-vehicle:list',
  30. 'pl:transport-address:list', 'pl:vehicle-maintenance-apply:add', 'pl:vehicle-maintenance-apply:cancel',
  31. 'pl:vehicle-maintenance-apply:edit', 'pl:vehicle-maintenance-apply:list', 'pl:vehicle-maintenance-apply:query',
  32. 'pl:vehicle-maintenance-apply:remove', 'pl:vehicle-maintenance-process:add', 'pl:vehicle-maintenance-process:edit',
  33. 'pl:vehicle-maintenance-process:list', 'pl:vehicle-maintenance-process:query', 'pl:vehicle-maintenance-process:remove',
  34. 'pl:vehicle-recharge-apply:add', 'pl:vehicle-recharge-apply:cancel', 'pl:vehicle-recharge-apply:edit',
  35. 'pl:vehicle-recharge-apply:list', 'pl:vehicle-recharge-apply:query', 'pl:vehicle-recharge-apply:remove',
  36. 'pl:vehicle-recharge-record:add', 'pl:vehicle-recharge-record:edit', 'pl:vehicle-recharge-record:list',
  37. 'pl:vehicle-recharge-record:query', 'pl:vehicle-recharge-record:remove', 'pl:vehicle-urea-apply:add',
  38. 'pl:vehicle-urea-apply:cancel', 'pl:vehicle-urea-apply:edit', 'pl:vehicle-urea-apply:list',
  39. 'pl:vehicle-urea-apply:query', 'pl:vehicle-urea-apply:remove', 'pl:vehicle-urea-disburse:add',
  40. 'pl:vehicle-urea-disburse:edit', 'pl:vehicle-urea-disburse:list', 'pl:vehicle-urea-disburse:query',
  41. 'pl:vehicle-urea-disburse:remove', 'pl:vehicle:list'
  42. ]
  43. function normalizeMenuCode(input: unknown): string {
  44. const raw = String(input ?? '').trim()
  45. if (!raw) return ''
  46. return raw
  47. .replace(/[\s-]+/g, '_')
  48. .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
  49. .toLowerCase()
  50. }
  51. function filterMenuByRoles(menu: AppRouteRecord[], roles: string[]): AppRouteRecord[] {
  52. return menu.reduce((acc: AppRouteRecord[], item) => {
  53. const itemRoles = item.meta?.roles
  54. const hasPermission = !itemRoles || itemRoles.some((role) => roles?.includes(role))
  55. if (hasPermission) {
  56. const filteredItem = { ...item }
  57. if (filteredItem.children?.length) {
  58. filteredItem.children = filterMenuByRoles(filteredItem.children, roles)
  59. }
  60. acc.push(filteredItem)
  61. }
  62. return acc
  63. }, [])
  64. }
  65. function filterMenuByCodes(menu: AppRouteRecord[], codes: Set<string>): AppRouteRecord[] {
  66. return menu.reduce((acc: AppRouteRecord[], item) => {
  67. const rawCode = (item.meta as any)?.menuCode ?? item.name
  68. const code = normalizeMenuCode(rawCode)
  69. const hasSelfPermission = !!code && codes.has(code)
  70. const filteredChildren = item.children?.length
  71. ? filterMenuByCodes(item.children, codes)
  72. : item.children
  73. const shouldKeep =
  74. hasSelfPermission || (Array.isArray(filteredChildren) && filteredChildren.length > 0)
  75. if (shouldKeep) {
  76. acc.push({ ...item, children: filteredChildren })
  77. }
  78. return acc
  79. }, [])
  80. }
  81. function filterDashboardMenus(menuList: AppRouteRecord[]): AppRouteRecord[] {
  82. return menuList
  83. .filter((item) => {
  84. const path = String(item.path ?? '')
  85. const title = String(item.meta?.title ?? '')
  86. const name = String(item.name ?? '')
  87. return !(path === '/dashboard' || title === '控制台' || name === 'Dashboard')
  88. })
  89. .map((item) => {
  90. if (!item.children?.length) return item
  91. return { ...item, children: filterDashboardMenus(item.children) }
  92. })
  93. }
  94. function filterEmptyMenus(menuList: AppRouteRecord[]): AppRouteRecord[] {
  95. return menuList
  96. .map((item) => {
  97. if (item.children?.length) {
  98. return { ...item, children: filterEmptyMenus(item.children) }
  99. }
  100. return item
  101. })
  102. .filter((item) => {
  103. if (Array.isArray(item.children)) {
  104. return item.children.length > 0
  105. }
  106. if (item.meta?.isIframe === true || item.meta?.link) return true
  107. if (item.component && item.component !== '' && item.component !== RoutesAlias.Layout) {
  108. return true
  109. }
  110. return false
  111. })
  112. }
  113. function countLeaves(menu: AppRouteRecord[]): number {
  114. let n = 0
  115. for (const item of menu) {
  116. if (item.children?.length) n += countLeaves(item.children)
  117. else if (item.component && item.component !== RoutesAlias.Layout) n++
  118. }
  119. return n
  120. }
  121. function topLevelSummary(menu: AppRouteRecord[]) {
  122. return menu.map((m) => ({
  123. path: m.path,
  124. title: m.meta?.title,
  125. childGroups: m.children?.length ?? 0,
  126. leaves: m.children ? countLeaves(m.children) : 0
  127. }))
  128. }
  129. // simulate scenarios
  130. for (const roles of [['admin'], ['common'], ['R_ADMIN'], undefined as string[] | undefined]) {
  131. let menuList = [...asyncRoutes]
  132. if (roles?.length && !roles.includes('super_admin')) {
  133. menuList = filterMenuByRoles(menuList, roles)
  134. }
  135. const codeSet = new Set(menuCodesFromApi.map(normalizeMenuCode))
  136. const afterRoles = menuList.length
  137. const filtered = filterMenuByCodes(menuList, codeSet)
  138. let afterCodes = filtered
  139. if (!(filtered.length > 0 || codeSet.size === 0)) {
  140. afterCodes = menuList // fail-open
  141. }
  142. const final = filterEmptyMenus(filterDashboardMenus(afterCodes))
  143. console.log('\n=== roles:', roles, '===')
  144. console.log('after role filter top modules:', afterRoles)
  145. console.log('after code filter top modules:', filtered.length)
  146. console.log('after code filter summary:', topLevelSummary(filtered))
  147. console.log('FINAL top modules:', final.length)
  148. console.log(topLevelSummary(final))
  149. }
  150. // debug filterEmptyMenus step by step for admin
  151. {
  152. let menuList = filterMenuByRoles([...asyncRoutes], ['admin'])
  153. const codeSet = new Set(menuCodesFromApi.map(normalizeMenuCode))
  154. menuList = filterMenuByCodes(menuList, codeSet)
  155. menuList = filterDashboardMenus(menuList)
  156. console.log('\n=== DEBUG filterEmptyMenus ===')
  157. for (const top of menuList) {
  158. console.log('TOP', top.path, 'children', top.children?.length)
  159. if (top.children?.length) {
  160. for (const c of top.children) {
  161. console.log(' L2', c.path, 'component', c.component, 'children', c.children?.length)
  162. if (c.children?.length) {
  163. for (const g of c.children.slice(0, 3)) {
  164. console.log(' L3', g.path, 'component', g.component, 'menuCode', (g.meta as any)?.menuCode)
  165. }
  166. }
  167. }
  168. }
  169. }
  170. const afterEmpty = filterEmptyMenus(menuList)
  171. console.log('after filterEmptyMenus tops:', afterEmpty.length)
  172. }