224 lines
5.4 KiB
TypeScript
224 lines
5.4 KiB
TypeScript
|
|
/**
|
|||
|
|
* 树结构相关工具函数
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 树节点基础接口
|
|||
|
|
*/
|
|||
|
|
export interface TreeNodeBase {
|
|||
|
|
id?: number | string
|
|||
|
|
parent_id?: number | string | null
|
|||
|
|
children?: TreeNodeBase[]
|
|||
|
|
[key: string]: any
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 构建树结构的选项
|
|||
|
|
*/
|
|||
|
|
export interface BuildTreeOptions<T extends TreeNodeBase> {
|
|||
|
|
/** ID 字段名,默认 'id' */
|
|||
|
|
idKey?: keyof T
|
|||
|
|
/** 父ID 字段名,默认 'parent_id' */
|
|||
|
|
parentKey?: keyof T
|
|||
|
|
/** 子节点字段名,默认 'children' */
|
|||
|
|
childrenKey?: string
|
|||
|
|
/** 排序字段名,可选 */
|
|||
|
|
orderKey?: keyof T
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 构建树结构的结果
|
|||
|
|
*/
|
|||
|
|
export interface BuildTreeResult<T extends TreeNodeBase> {
|
|||
|
|
/** 根节点列表 */
|
|||
|
|
rootItems: T[]
|
|||
|
|
/** 节点映射表 (id -> node) */
|
|||
|
|
itemMap: Map<number | string, T>
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 将扁平数组构建为树状结构
|
|||
|
|
* @param items 扁平数组
|
|||
|
|
* @param options 构建选项
|
|||
|
|
* @returns 包含根节点列表和节点映射的对象
|
|||
|
|
*/
|
|||
|
|
export function buildTree<T extends TreeNodeBase>(
|
|||
|
|
items: T[],
|
|||
|
|
options: BuildTreeOptions<T> = {}
|
|||
|
|
): BuildTreeResult<T> {
|
|||
|
|
const {
|
|||
|
|
idKey = 'id' as keyof T,
|
|||
|
|
parentKey = 'parent_id' as keyof T,
|
|||
|
|
childrenKey = 'children',
|
|||
|
|
orderKey,
|
|||
|
|
} = options
|
|||
|
|
|
|||
|
|
const itemMap = new Map<number | string, T>()
|
|||
|
|
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<T extends TreeNodeBase>(
|
|||
|
|
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<T extends TreeNodeBase>(
|
|||
|
|
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<T extends TreeNodeBase>(
|
|||
|
|
nodeId: number | string,
|
|||
|
|
itemMap: Map<number | string, T>,
|
|||
|
|
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<T extends TreeNodeBase>(
|
|||
|
|
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<T extends TreeNodeBase>(
|
|||
|
|
nodes: T[],
|
|||
|
|
childrenKey: string = 'children'
|
|||
|
|
): Omit<T, 'children'>[] {
|
|||
|
|
const result: Omit<T, 'children'>[] = []
|
|||
|
|
|
|||
|
|
traverseTree(nodes, (node) => {
|
|||
|
|
const { [childrenKey]: _, ...rest } = node
|
|||
|
|
result.push(rest as Omit<T, 'children'>)
|
|||
|
|
}, childrenKey)
|
|||
|
|
|
|||
|
|
return result
|
|||
|
|
}
|