Files
front/src/utils/tree.ts

224 lines
5.4 KiB
TypeScript
Raw Normal View History

2026-03-07 20:11:25 +08:00
/**
*
*/
/**
*
*/
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
}