优化构架
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { Permission } from '@/modules/iam/types'
|
||||
import { buildPermissionTree, getDescendantIds, summarizePermissionSelection } from '@/modules/iam/utils/permissionTree'
|
||||
|
||||
const permissions: Permission[] = [
|
||||
{ id: 'action-b', parentId: 'menu', name: '操作 B', code: 'demo:b', type: '按钮', path: '', method: '-', status: '启用', sort: 20 },
|
||||
{ id: 'menu', parentId: 'directory', name: '页面', code: 'demo:list', type: '菜单', path: '/demo', method: '-', status: '启用', sort: 10 },
|
||||
{ id: 'directory', parentId: null, name: '目录', code: 'demo', type: '目录', path: '', method: '-', status: '启用', sort: 10 },
|
||||
{ id: 'action-a', parentId: 'menu', name: '操作 A', code: 'demo:a', type: '按钮', path: '', method: '-', status: '启用', sort: 10 },
|
||||
]
|
||||
|
||||
describe('permissionTree', () => {
|
||||
it('builds arbitrary-depth trees and sorts siblings', () => {
|
||||
const tree = buildPermissionTree(permissions)
|
||||
|
||||
expect(tree[0]?.children?.[0]?.children?.map((node) => node.id)).toEqual(['action-a', 'action-b'])
|
||||
})
|
||||
|
||||
it('keeps the ancestor chain when a deep descendant matches', () => {
|
||||
const tree = buildPermissionTree(permissions, (item) => item.code === 'demo:a')
|
||||
|
||||
expect(tree.map((node) => node.id)).toEqual(['directory'])
|
||||
expect(tree[0]?.children?.map((node) => node.id)).toEqual(['menu'])
|
||||
expect(tree[0]?.children?.[0]?.children?.map((node) => node.id)).toEqual(['action-a'])
|
||||
})
|
||||
|
||||
it('stops building a branch when malformed data repeats an ancestor id', () => {
|
||||
const malformed = [
|
||||
...permissions,
|
||||
{ id: 'loop', parentId: null, name: '循环目录', code: 'loop', type: '目录', path: '', method: '-', status: '启用', sort: 20 },
|
||||
{ id: 'loop', parentId: 'loop', name: '循环下级', code: 'loop:child', type: '目录', path: '', method: '-', status: '启用', sort: 10 },
|
||||
] satisfies Permission[]
|
||||
|
||||
const tree = buildPermissionTree(malformed)
|
||||
|
||||
expect(tree.find((node) => node.code === 'loop')).not.toHaveProperty('children')
|
||||
})
|
||||
|
||||
it('collects descendants once when malformed data contains a cycle', () => {
|
||||
const malformed = [
|
||||
...permissions,
|
||||
{ id: 'cycle-a', parentId: 'cycle-b', name: 'A', code: 'cycle:a', type: '目录', path: '', method: '-', status: '启用', sort: 1 },
|
||||
{ id: 'cycle-b', parentId: 'cycle-a', name: 'B', code: 'cycle:b', type: '目录', path: '', method: '-', status: '启用', sort: 2 },
|
||||
] satisfies Permission[]
|
||||
|
||||
expect(getDescendantIds(malformed, 'cycle-a')).toEqual(['cycle-b'])
|
||||
})
|
||||
|
||||
it('counts page menus and operations without treating directories as menus', () => {
|
||||
expect(summarizePermissionSelection(permissions, ['directory', 'menu', 'action-a'])).toEqual({
|
||||
menuCount: 1,
|
||||
actionCount: 1,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { Permission } from '@/modules/iam/types'
|
||||
|
||||
export type PermissionTreeNode = Permission & { children?: PermissionTreeNode[] }
|
||||
export type PermissionMatcher = (permission: Permission) => boolean
|
||||
|
||||
function groupByParent(permissions: Permission[]) {
|
||||
const childrenByParent = new Map<string | null, Permission[]>()
|
||||
|
||||
permissions.forEach((permission) => {
|
||||
const siblings = childrenByParent.get(permission.parentId) ?? []
|
||||
siblings.push(permission)
|
||||
childrenByParent.set(permission.parentId, siblings)
|
||||
})
|
||||
|
||||
childrenByParent.forEach((siblings) => {
|
||||
siblings.sort((left, right) => left.sort - right.sort)
|
||||
})
|
||||
|
||||
return childrenByParent
|
||||
}
|
||||
|
||||
export function buildPermissionTree(
|
||||
permissions: Permission[],
|
||||
matches: PermissionMatcher = () => true,
|
||||
): PermissionTreeNode[] {
|
||||
const childrenByParent = groupByParent(permissions)
|
||||
|
||||
function buildNode(permission: Permission, ancestors: Set<string>): PermissionTreeNode | null {
|
||||
if (ancestors.has(permission.id)) return null
|
||||
|
||||
const nextAncestors = new Set(ancestors).add(permission.id)
|
||||
const children = (childrenByParent.get(permission.id) ?? [])
|
||||
.map((child) => buildNode(child, nextAncestors))
|
||||
.filter((child): child is PermissionTreeNode => child !== null)
|
||||
|
||||
if (!matches(permission) && children.length === 0) return null
|
||||
|
||||
return {
|
||||
...permission,
|
||||
...(children.length ? { children } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
return (childrenByParent.get(null) ?? [])
|
||||
.map((permission) => buildNode(permission, new Set()))
|
||||
.filter((node): node is PermissionTreeNode => node !== null)
|
||||
}
|
||||
|
||||
export function getDescendantIds(permissions: Permission[], permissionId: string): string[] {
|
||||
const childrenByParent = groupByParent(permissions)
|
||||
const visited = new Set([permissionId])
|
||||
const descendants: string[] = []
|
||||
const queue = [...(childrenByParent.get(permissionId) ?? [])]
|
||||
|
||||
while (queue.length) {
|
||||
const current = queue.shift()
|
||||
if (!current || visited.has(current.id)) continue
|
||||
|
||||
visited.add(current.id)
|
||||
descendants.push(current.id)
|
||||
queue.push(...(childrenByParent.get(current.id) ?? []))
|
||||
}
|
||||
|
||||
return descendants
|
||||
}
|
||||
|
||||
export function summarizePermissionSelection(permissions: Permission[], selectedIds: string[]) {
|
||||
const selected = new Set(selectedIds)
|
||||
const selectedPermissions = permissions.filter((permission) => selected.has(permission.id))
|
||||
|
||||
return {
|
||||
menuCount: selectedPermissions.filter((permission) => permission.type === '菜单').length,
|
||||
actionCount: selectedPermissions.filter((permission) => ['按钮', '接口'].includes(permission.type)).length,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { mapPlatformPermission } from '@/modules/iam/utils/platformPermission'
|
||||
|
||||
describe('mapPlatformPermission', () => {
|
||||
it('treats the string root parent as a root node and preserves long ids', () => {
|
||||
expect(mapPlatformPermission({
|
||||
id: '2082813911786024961',
|
||||
tenantId: '0',
|
||||
appCode: 'PLATFORM',
|
||||
permissionType: 'MENU',
|
||||
parentId: '0',
|
||||
permissionCode: 'platform:audit',
|
||||
permissionName: '审计管理',
|
||||
routePath: '/audit',
|
||||
httpMethod: null,
|
||||
sortNo: 50,
|
||||
status: 'ACTIVE',
|
||||
})).toMatchObject({ id: '2082813911786024961', parentId: null, code: 'platform:audit' })
|
||||
})
|
||||
|
||||
it('preserves a long string parent id for child resources', () => {
|
||||
expect(mapPlatformPermission({
|
||||
id: '2082813911786024962',
|
||||
tenantId: '0',
|
||||
appCode: 'PLATFORM',
|
||||
permissionType: 'BUTTON',
|
||||
parentId: '2082813911786024961',
|
||||
permissionCode: 'platform:audit:list',
|
||||
permissionName: '操作日志',
|
||||
routePath: null,
|
||||
httpMethod: null,
|
||||
sortNo: 10,
|
||||
status: 'ACTIVE',
|
||||
})).toMatchObject({ id: '2082813911786024962', parentId: '2082813911786024961', type: '按钮' })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { PlatformPermissionResource } from '@/modules/iam/api/platformIam'
|
||||
import type { Permission } from '@/modules/iam/types'
|
||||
|
||||
export function mapPlatformPermission(item: PlatformPermissionResource): Permission {
|
||||
const parentId = String(item.parentId)
|
||||
return {
|
||||
id: String(item.id),
|
||||
parentId: parentId === '0' ? null : parentId,
|
||||
name: item.permissionName,
|
||||
code: item.permissionCode,
|
||||
type: item.permissionType === 'BUTTON' ? '按钮' : '菜单',
|
||||
path: item.routePath ?? '',
|
||||
method: item.httpMethod ?? '-',
|
||||
status: item.status === 'ACTIVE' ? '启用' : '停用',
|
||||
sort: item.sortNo,
|
||||
builtIn: Boolean(item.builtIn),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user