This commit is contained in:
2026-09-11 11:15:04 +08:00
parent f702a819fa
commit 23e991722c
8 changed files with 115 additions and 29 deletions
+22 -1
View File
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import type { Permission } from '@/modules/iam/types'
import { buildPermissionTree, getDescendantIds, summarizePermissionSelection } from '@/modules/iam/utils/permissionTree'
import { buildNavigationPermissionTree, buildPermissionTree, getDescendantIds, summarizePermissionSelection } from '@/modules/iam/utils/permissionTree'
import { adminNavigation } from '@/core/navigation/adminNavigation'
const permissions: Permission[] = [
{ id: 'action-b', parentId: 'menu', name: '操作 B', code: 'demo:b', type: '按钮', path: '', method: '-', status: '启用', sort: 20 },
@@ -52,4 +53,24 @@ describe('permissionTree', () => {
actionCount: 1,
})
})
it('groups assignable permissions by the current admin navigation and hides technical containers', () => {
const rolePermissions: Permission[] = [
{ id: 'supplier', parentId: null, name: '供应商旧名称', code: 'platform:supplier:list', type: '菜单', path: '/suppliers', method: 'GET', status: '启用', sort: 10 },
{ id: 'supplier-create', parentId: 'supplier', name: '新增供应商', code: 'platform:supplier:create', type: '按钮', path: '', method: 'POST', status: '启用', sort: 10 },
{ id: 'express', parentId: null, name: '快递公司', code: 'platform:express:list', type: '菜单', path: '/master-data/express-companies', method: 'GET', status: '启用', sort: 20 },
{ id: 'counterparty', parentId: null, name: '往来单位', code: 'platform:counterparty:list', type: '菜单', path: '/master-data/counterparties', method: 'GET', status: '启用', sort: 30 },
{ id: 'technical', parentId: null, name: '服务接口', code: 'platform:technical:invoke', type: '接口', path: '/internal', method: 'POST', status: '启用', sort: 40 },
]
const tree = buildNavigationPermissionTree(rolePermissions, adminNavigation)
const general = tree.find((node) => node.name === '通用')
expect(tree.some((node) => node.name === '供应商中心')).toBe(false)
expect(general?.permissionId).toBeUndefined()
expect(general?.children?.map((node) => node.name)).toEqual(['供应商列表', '快递公司', '往来单位'])
expect(general?.children?.[0]?.children?.map((node) => node.name)).toEqual(['供应商旧名称', '新增供应商'])
expect(tree.some((node) => node.name === '其他权限')).toBe(false)
expect(JSON.stringify(tree)).not.toContain('platform:technical:invoke')
})
})
+66
View File
@@ -1,7 +1,15 @@
import type { Permission } from '@/modules/iam/types'
import type { AdminNavigationNode } from '@/core/navigation/adminNavigation'
export type PermissionTreeNode = Permission & { children?: PermissionTreeNode[] }
export type PermissionMatcher = (permission: Permission) => boolean
export interface RolePermissionTreeNode {
id: string
permissionId?: string
name: string
code: string
children?: RolePermissionTreeNode[]
}
function groupByParent(permissions: Permission[]) {
const childrenByParent = new Map<string | null, Permission[]>()
@@ -73,3 +81,61 @@ export function summarizePermissionSelection(permissions: Permission[], selected
actionCount: selectedPermissions.filter((permission) => ['按钮', '接口'].includes(permission.type)).length,
}
}
export function buildNavigationPermissionTree(
permissions: Permission[],
navigation: AdminNavigationNode[],
): RolePermissionTreeNode[] {
const childrenByParent = groupByParent(permissions)
const permissionByCode = new Map(permissions.map((permission) => [permission.code, permission]))
const usedPermissionIds = new Set<string>()
function permissionLeaves(permission: Permission, ancestors: Set<string>): RolePermissionTreeNode[] {
if (ancestors.has(permission.id)) return []
usedPermissionIds.add(permission.id)
const nextAncestors = new Set(ancestors).add(permission.id)
const children = (childrenByParent.get(permission.id) ?? [])
.flatMap((child) => permissionLeaves(child, nextAncestors))
return [{ id: permission.id, permissionId: permission.id, name: permission.name, code: permission.code }, ...children]
}
function navigationNode(node: AdminNavigationNode): RolePermissionTreeNode | null {
if (node.hidden) return null
if (node.children?.length) {
const children = node.children
.map(navigationNode)
.filter((child): child is RolePermissionTreeNode => child !== null)
if (!children.length) return null
return {
id: `navigation-group:${node.key}`,
name: node.title,
code: '',
children,
}
}
const codes = [...new Set([node.permission, ...(node.permissionAnyOf ?? [])].filter(Boolean))] as string[]
const permissionNodes = codes
.map((code) => permissionByCode.get(code))
.filter((permission): permission is Permission => permission !== undefined)
.flatMap((permission) => usedPermissionIds.has(permission.id) ? [] : permissionLeaves(permission, new Set()))
if (permissionNodes.length) {
return {
id: `navigation-menu:${node.key}`,
name: node.title,
code: '',
children: permissionNodes,
}
}
return null
}
const navigationGroups = navigation
.map(navigationNode)
.filter((node): node is RolePermissionTreeNode => node !== null)
return navigationGroups
}