2026-05-15 10:54:26 +08:00
|
|
|
export type RoleResourceTreeNode = {
|
|
|
|
|
id: string;
|
|
|
|
|
children?: RoleResourceTreeNode[] | null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type ResolveRoleMenuSubmitIdsInput = {
|
|
|
|
|
menuTree: RoleResourceTreeNode[];
|
|
|
|
|
baselineIds: string[];
|
|
|
|
|
dirtyIds: string[];
|
|
|
|
|
checkedIds: string[];
|
2026-05-15 13:16:14 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type NormalizeRoleMenuCheckedIdsInput = {
|
|
|
|
|
menuTree: RoleResourceTreeNode[];
|
|
|
|
|
checkedIds: string[];
|
2026-05-15 10:54:26 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type TreeIndex = {
|
|
|
|
|
orderedIds: string[];
|
|
|
|
|
parentById: Map<string, string | null>;
|
|
|
|
|
subtreeIdsById: Map<string, string[]>;
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-15 13:16:14 +08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 10:54:26 +08:00
|
|
|
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<string>();
|
|
|
|
|
|
|
|
|
|
baselineIds.forEach(id => {
|
|
|
|
|
if (!affectedIds.has(id)) {
|
|
|
|
|
nextIdSet.add(id);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-15 13:16:14 +08:00
|
|
|
// 半选父节点只用于树态展示,提交它会把整棵子树误当成完整授权。
|
|
|
|
|
normalizeIds(input.checkedIds).forEach(id => {
|
2026-05-15 10:54:26 +08:00
|
|
|
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<string, string | null>();
|
|
|
|
|
const subtreeIdsById = new Map<string, string[]>();
|
|
|
|
|
|
|
|
|
|
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<string>();
|
|
|
|
|
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<string, string | null>, 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<string>) {
|
|
|
|
|
const sortedIds: string[] = [];
|
|
|
|
|
|
|
|
|
|
orderedIds.forEach(id => {
|
|
|
|
|
if (idSet.has(id)) {
|
|
|
|
|
sortedIds.push(id);
|
|
|
|
|
idSet.delete(id);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
idSet.forEach(id => {
|
|
|
|
|
sortedIds.push(id);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return sortedIds;
|
|
|
|
|
}
|