export type RoleResourceTreeNode = { id: string; children?: RoleResourceTreeNode[] | null; }; type ResolveRoleMenuSubmitIdsInput = { menuTree: RoleResourceTreeNode[]; baselineIds: string[]; dirtyIds: string[]; checkedIds: string[]; }; type NormalizeRoleMenuCheckedIdsInput = { menuTree: RoleResourceTreeNode[]; checkedIds: string[]; }; type TreeIndex = { orderedIds: string[]; parentById: Map; subtreeIdsById: Map; }; export function normalizeRoleMenuCheckedIds(input: NormalizeRoleMenuCheckedIdsInput) { const checkedIds = normalizeIds(input.checkedIds); if (!checkedIds.length) { return checkedIds; } const treeIndex = buildTreeIndex(input.menuTree); const normalizedIdSet = new Set(checkedIds); treeIndex.orderedIds.forEach(id => { if (!normalizedIdSet.has(id)) { return; } const descendantIds = (treeIndex.subtreeIdsById.get(id) ?? [id]).slice(1); if (!descendantIds.length) { return; } const selectedDescendantCount = descendantIds.reduce((count, descendantId) => { return normalizedIdSet.has(descendantId) ? count + 1 : count; }, 0); if (selectedDescendantCount > 0 && selectedDescendantCount < descendantIds.length) { normalizedIdSet.delete(id); } }); return sortIdsByTreeOrder(treeIndex.orderedIds, normalizedIdSet); } export function resolveRoleMenuSubmitIds(input: ResolveRoleMenuSubmitIdsInput) { const baselineIds = normalizeIds(input.baselineIds); if (!input.dirtyIds.length) { return baselineIds; } const treeIndex = buildTreeIndex(input.menuTree); const affectedIds = collectAffectedIds(treeIndex, baselineIds, normalizeIds(input.dirtyIds)); if (!affectedIds.size) { return baselineIds; } const nextIdSet = new Set(); baselineIds.forEach(id => { if (!affectedIds.has(id)) { nextIdSet.add(id); } }); // 半选父节点只用于树态展示,提交它会把整棵子树误当成完整授权。 normalizeIds(input.checkedIds).forEach(id => { if (affectedIds.has(id)) { nextIdSet.add(id); } }); return sortIdsByTreeOrder(treeIndex.orderedIds, nextIdSet); } function normalizeIds(ids: string[]) { return [...new Set(ids.map(id => String(id).trim()).filter(Boolean))]; } function buildTreeIndex(nodes: RoleResourceTreeNode[]) { const orderedIds: string[] = []; const parentById = new Map(); const subtreeIdsById = new Map(); const walk = (items: RoleResourceTreeNode[], parentId: string | null) => { items.forEach(item => { orderedIds.push(item.id); parentById.set(item.id, parentId); const childIds = item.children?.length ? walk(item.children, item.id) : []; subtreeIdsById.set(item.id, [item.id, ...childIds]); }); return items.flatMap(item => subtreeIdsById.get(item.id) ?? [item.id]); }; walk(nodes, null); return { orderedIds, parentById, subtreeIdsById } satisfies TreeIndex; } function collectAffectedIds(treeIndex: TreeIndex, baselineIds: string[], dirtyIds: string[]) { const affectedIds = new Set(); const baselineIdSet = new Set(baselineIds); dirtyIds.forEach(dirtyId => { const subtreeIds = treeIndex.subtreeIdsById.get(dirtyId) ?? [dirtyId]; subtreeIds.forEach(id => affectedIds.add(id)); const ancestors = collectAncestorIds(treeIndex.parentById, dirtyId); ancestors.forEach(ancestorId => { if (!baselineIdSet.has(ancestorId)) { return; } const ancestorSubtreeIds = treeIndex.subtreeIdsById.get(ancestorId) ?? [ancestorId]; ancestorSubtreeIds.forEach(id => affectedIds.add(id)); }); }); return affectedIds; } function collectAncestorIds(parentById: Map, nodeId: string) { const ancestorIds: string[] = []; let currentId: string | null | undefined = nodeId; while (currentId) { ancestorIds.push(currentId); currentId = parentById.get(currentId) ?? null; } return ancestorIds; } function sortIdsByTreeOrder(orderedIds: string[], idSet: Set) { const sortedIds: string[] = []; orderedIds.forEach(id => { if (idSet.has(id)) { sortedIds.push(id); idSet.delete(id); } }); idSet.forEach(id => { sortedIds.push(id); }); return sortedIds; }