67 lines
1.9 KiB
TypeScript
67 lines
1.9 KiB
TypeScript
export type ParentLinkedRow = {
|
|
id: string;
|
|
parent_id?: string | null;
|
|
};
|
|
|
|
export function buildParentById<T extends ParentLinkedRow>(rows: readonly T[]): Map<string, string | null> {
|
|
const map = new Map<string, string | null>();
|
|
for (const row of rows) {
|
|
map.set(row.id, row.parent_id ?? null);
|
|
}
|
|
return map;
|
|
}
|
|
|
|
export function isAncestorOf(
|
|
ancestorId: string,
|
|
nodeId: string,
|
|
parentById: Map<string, string | null>,
|
|
): boolean {
|
|
let current: string | null | undefined = nodeId;
|
|
while (current) {
|
|
const parentId = parentById.get(current);
|
|
if (!parentId) return false;
|
|
if (parentId === ancestorId) return true;
|
|
current = parentId;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 收集以 rootId 为根的整棵子树(包含根节点本身)。
|
|
*
|
|
* 说明:
|
|
* - 仅依赖 (id, parent_id) 字段,便于在 Convex 与前端共用同一套遍历逻辑。
|
|
* - 会做去重与环检测,避免异常数据导致死循环。
|
|
*/
|
|
export function collectSubtree<T extends ParentLinkedRow>(rows: readonly T[], rootId: string): T[] {
|
|
const byParentId = new Map<string | null, T[]>();
|
|
for (const row of rows) {
|
|
const parentId = (row.parent_id ?? null) as string | null;
|
|
const bucket = byParentId.get(parentId);
|
|
if (bucket) bucket.push(row);
|
|
else byParentId.set(parentId, [row]);
|
|
}
|
|
|
|
const root = rows.find((r) => r.id === rootId);
|
|
if (!root) return [];
|
|
|
|
const visited = new Set<string>();
|
|
const stack: T[] = [root];
|
|
const result: T[] = [];
|
|
|
|
while (stack.length > 0) {
|
|
const current = stack.pop()!;
|
|
if (visited.has(current.id)) continue;
|
|
visited.add(current.id);
|
|
result.push(current);
|
|
|
|
const children = byParentId.get(current.id) ?? [];
|
|
// 倒序入栈,保持更接近“原列表顺序”的遍历结果(不影响正确性)
|
|
for (let i = children.length - 1; i >= 0; i -= 1) {
|
|
stack.push(children[i]!);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|