提交
This commit is contained in:
1 parent
3520f9ce90
commit
0e60b88cba
104 files changed
+344311
-250708
No files matched your search
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* 完整模拟 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<string>): 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)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 模拟 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)
|
||||
Reference in new issue
Block a user