/** * 树结构相关工具函数 */ /** * 树节点基础接口 */ export interface TreeNodeBase { id?: number | string parent_id?: number | string | null children?: TreeNodeBase[] [key: string]: any } /** * 构建树结构的选项 */ export interface BuildTreeOptions { /** ID 字段名,默认 'id' */ idKey?: keyof T /** 父ID 字段名,默认 'parent_id' */ parentKey?: keyof T /** 子节点字段名,默认 'children' */ childrenKey?: string /** 排序字段名,可选 */ orderKey?: keyof T } /** * 构建树结构的结果 */ export interface BuildTreeResult { /** 根节点列表 */ rootItems: T[] /** 节点映射表 (id -> node) */ itemMap: Map } /** * 将扁平数组构建为树状结构 * @param items 扁平数组 * @param options 构建选项 * @returns 包含根节点列表和节点映射的对象 */ export function buildTree( items: T[], options: BuildTreeOptions = {} ): BuildTreeResult { const { idKey = 'id' as keyof T, parentKey = 'parent_id' as keyof T, childrenKey = 'children', orderKey, } = options const itemMap = new Map() const rootItems: T[] = [] // 创建节点映射 items.forEach((item) => { const id = item[idKey] if (id !== undefined && id !== null) { // 创建带有空 children 数组的节点副本 itemMap.set(id as number | string, { ...item, [childrenKey]: [] } as T) } }) // 构建树结构 itemMap.forEach((item) => { const parentId = item[parentKey] if (parentId && itemMap.has(parentId as number | string)) { const parent = itemMap.get(parentId as number | string)! const parentChildren = parent[childrenKey] as T[] parentChildren.push(item) } else { rootItems.push(item) } }) // 排序函数 if (orderKey) { const sortByOrder = (a: T, b: T) => { const orderA = (a[orderKey] as number) || 0 const orderB = (b[orderKey] as number) || 0 return orderA - orderB } rootItems.sort(sortByOrder) itemMap.forEach((item) => { const children = item[childrenKey] as T[] if (children && children.length > 0) { children.sort(sortByOrder) } }) } return { rootItems, itemMap } } /** * 递归遍历树节点 * @param nodes 节点列表 * @param callback 回调函数 * @param childrenKey 子节点字段名 */ export function traverseTree( nodes: T[], callback: (node: T, depth: number, parent: T | null) => void | boolean, childrenKey: string = 'children', depth: number = 0, parent: T | null = null ): void { for (const node of nodes) { const result = callback(node, depth, parent) // 如果回调返回 false,停止遍历 if (result === false) return const children = node[childrenKey] as T[] if (children && children.length > 0) { traverseTree(children, callback, childrenKey, depth + 1, node) } } } /** * 在树中查找节点 * @param nodes 节点列表 * @param predicate 判断条件 * @param childrenKey 子节点字段名 * @returns 找到的节点或 null */ export function findInTree( nodes: T[], predicate: (node: T) => boolean, childrenKey: string = 'children' ): T | null { for (const node of nodes) { if (predicate(node)) { return node } const children = node[childrenKey] as T[] if (children && children.length > 0) { const found = findInTree(children, predicate, childrenKey) if (found) return found } } return null } /** * 获取节点的所有父节点路径 * @param nodeId 目标节点ID * @param itemMap 节点映射表 * @param parentKey 父ID字段名 * @returns 从根到目标节点的路径(不包含目标节点本身) */ export function getAncestors( nodeId: number | string, itemMap: Map, parentKey: keyof T = 'parent_id' as keyof T ): T[] { const ancestors: T[] = [] let current = itemMap.get(nodeId) while (current) { const parentId = current[parentKey] if (parentId && itemMap.has(parentId as number | string)) { const parent = itemMap.get(parentId as number | string)! ancestors.unshift(parent) current = parent } else { break } } return ancestors } /** * 获取节点的所有子孙节点 * @param node 目标节点 * @param childrenKey 子节点字段名 * @returns 所有子孙节点(扁平数组) */ export function getDescendants( node: T, childrenKey: string = 'children' ): T[] { const descendants: T[] = [] const children = node[childrenKey] as T[] if (children && children.length > 0) { for (const child of children) { descendants.push(child) descendants.push(...getDescendants(child, childrenKey)) } } return descendants } /** * 将树结构扁平化为数组 * @param nodes 节点列表 * @param childrenKey 子节点字段名 * @returns 扁平化后的数组(移除 children 属性) */ export function flattenTree( nodes: T[], childrenKey: string = 'children' ): Omit[] { const result: Omit[] = [] traverseTree(nodes, (node) => { const { [childrenKey]: _, ...rest } = node result.push(rest as Omit) }, childrenKey) return result }