diff --git a/packages/devui-vue/devui/tree/src/core/README.md b/packages/devui-vue/devui/tree/src/core/README.md new file mode 100644 index 0000000000..f27589db9f --- /dev/null +++ b/packages/devui-vue/devui/tree/src/core/README.md @@ -0,0 +1,66 @@ +# Tree Factory + +Tree 组件最核心的部分,是 UI 无关的,用于树形结构的处理。 + +- 获取 / 设置整棵树 +- 获取子节点 +- 搜索节点 +- 插入 / 移除 / 编辑节点 +- 点选 / 勾选 / 展开收起节点 +- 点选 / 勾选 / 展开收起节点功能的禁用 + +## 快速开始 + +```ts +const treeData = [ + { + label: 'Parent node 1', + children: [ + { label: 'Leaf node 1-1' } + ] + }, + { label: 'Leaf node 2' } +]; + +const treeFactory = new TreeFactory(treeData); +treeFactory.getTree(); +``` + +## API + +| 名称 | 描述 | +| -- | -- | +| getTree() => ITreeNode[] | 获取整棵树 | +| setTree(tree: ITreeNode[]) => void | 设置整棵树 | +| getLevel(node: ITreeNode) => number | 获取节点层级 | +| getChildren(node: ITreeNode): ITreeNode[] | 获取子节点 | +| selectNode(node: ITreeNode): void | 点击选择 | +| checkNode(node: ITreeNode): void | 勾选 | +| uncheckNode(node: ITreeNode): void | 取消勾选 | +| expandNode(node: ITreeNode): void | 展开 | +| collapseNode(node: ITreeNode): void | 收起 | +| toggleNode(node: ITreeNode): void | 切换展开/收起状态 | +| disableSelectNode(node: ITreeNode): void | 禁用点击选择 | +| disableCheckNode(node: ITreeNode): void | 禁用勾选 | +| disableToggleNode(node: ITreeNode): void | 禁用展开/收起 | +| insertBefore(parentNode: ITreeNode, node: ITreeNode, referenceNode: ITreeNode, cut: boolean = false): void | 插入节点 | +| removeNode(node: ITreeNode): void | 移除节点 | +| editNode(node: ITreeNode, label: string): void | 编辑节点内容 | + +## ITreeNode + +```ts +interface ITreeNode { + label: string; + id?: string; + children?: ITreeNode[]; + + selected?: boolean; + checked?: boolean; + expanded?: boolean; + + disableSelect?: boolean; + disableCheck?: boolean; + disableToggle?: boolean; +} +``` diff --git a/packages/devui-vue/devui/tree/src/core/tree-factory-types.ts b/packages/devui-vue/devui/tree/src/core/tree-factory-types.ts new file mode 100644 index 0000000000..378072dada --- /dev/null +++ b/packages/devui-vue/devui/tree/src/core/tree-factory-types.ts @@ -0,0 +1,23 @@ +// 外部数据结构先只考虑嵌套结构 +export interface ITreeNode { + label: string; + id?: string; + children?: ITreeNode[]; + + selected?: boolean; + checked?: boolean; + expanded?: boolean; + + disableSelect?: boolean; + disableCheck?: boolean; + disableToggle?: boolean; +} + +// 内部数据结构使用扁平结构 +export interface IInnerTreeNode extends ITreeNode { + level: number; + idType?: 'random'; + parentId?: string; +} + +export type valueof = T[keyof T]; diff --git a/packages/devui-vue/devui/tree/src/core/tree-factory.ts b/packages/devui-vue/devui/tree/src/core/tree-factory.ts new file mode 100644 index 0000000000..33b3f592e1 --- /dev/null +++ b/packages/devui-vue/devui/tree/src/core/tree-factory.ts @@ -0,0 +1,99 @@ +import type { IInnerTreeNode, ITreeNode } from './tree-factory-types'; +import { flatToNested, generateInnerTree } from './utils'; + +export default class TreeFactory { + private _innerTree: IInnerTreeNode[] = []; + + private _getIndex(node: ITreeNode): number { + return this._innerTree.findIndex((item) => item.id === node.id); + } + + private _setNodeValue(node, key, value): void { + this._innerTree[this._getIndex(node)][key] = value; + } + + constructor(tree: ITreeNode[]) { + this.setTree(tree); + } + + getTree(flat?: boolean = false): IInnerTreeNode[] { + if (flat) { + return this._innerTree; + } else { + // TODO: 移除内部属性(level / parentId / idType) / 内部生成的id / 空children + return flatToNested(this._innerTree); + } + } + + setTree(tree: ITreeNode[]) { + this._innerTree = generateInnerTree(tree); + } + + getLevel(node: ITreeNode): number { + return this._innerTree.find((item) => item.id === node.id).level; + } + + getChildren(node: ITreeNode): IInnerTreeNode[] { + let result = []; + const startIndex = this._innerTree.findIndex((item) => item.id === node.id); + + for (let i = startIndex + 1; i < this._innerTree.length && this.getLevel(node) < this._innerTree[i].level; i++) { + result.push(this._innerTree[i]); + } + return result; + } + + selectNode(node: ITreeNode): void { + this._setNodeValue(node, 'selected', true); + } + + checkNode(node: ITreeNode): void { + this._setNodeValue(node, 'checked', true); + } + + uncheckNode(node: ITreeNode): void { + this._setNodeValue(node, 'checked', false); + } + + expandNode(node: ITreeNode): void { + this._setNodeValue(node, 'expanded', true); + } + + collapseNode(node: ITreeNode): void { + this._setNodeValue(node, 'expanded', false); + } + + toggleNode(node: ITreeNode): void { + if (node.expanded) { + this._setNodeValue(node, 'expanded', false); + } else { + this._setNodeValue(node, 'expanded', true); + } + } + + disableSelectNode(node: ITreeNode): void { + this._setNodeValue(node, 'disableSelect', true); + } + + disableCheckNode(node: ITreeNode): void { + this._setNodeValue(node, 'disableCheck', true); + } + + disableToggleNode(node: ITreeNode): void { + this._setNodeValue(node, 'disableToggle', true); + } + + insertBefore(parentNode: ITreeNode, node: ITreeNode, referenceNode: ITreeNode, cut: boolean = false): void { + // TODO + } + + removeNode(node: ITreeNode): void { + this._innerTree = this._innerTree.filter(item => { + return item.id !== node.id && !this.getChildren(node).map(nodeItem => nodeItem.id).includes(item.id); + }) + } + + editNode(node: ITreeNode, label: string): void { + this._setNodeValue(node, 'label', label); + } +} diff --git a/packages/devui-vue/devui/tree/src/core/utils.ts b/packages/devui-vue/devui/tree/src/core/utils.ts new file mode 100644 index 0000000000..36149f9cc0 --- /dev/null +++ b/packages/devui-vue/devui/tree/src/core/utils.ts @@ -0,0 +1,68 @@ +import { randomId } from '../../../anchor/src/util'; +import { IInnerTreeNode, ITreeNode } from './tree-factory-types'; + +export function flatToNested(flatTree: IInnerTreeNode[]): ITreeNode[] { + let treeMap = {}; + return flatTree.reduce((acc: ITreeNode[], cur: IInnerTreeNode) => { + const { id, parentId } = cur; + + if (!treeMap[id]) { + treeMap[id] = { + ...cur, + children: [], + }; + } + + if (!treeMap[parentId]) { + acc.push(treeMap[id]); + } else { + treeMap[parentId].children.push(treeMap[id]); + } + + return acc; + }, []); +} + +export function generateInnerTree( + tree: ITreeNode[], + key = 'children', + level: number = 0, + path: ITreeNode[] = [] +): IInnerTreeNode[] { + level++; + + return tree.reduce((acc: ITreeNode[], item: ITreeNode) => { + const newItem = Object.assign({}, item); + if (newItem.id === undefined) { + newItem.id = randomId(); + newItem.idType = 'random'; + } + + newItem.level = level; + + if (path.length > 0 && path[path.length - 1]?.level >= level) { + while (path[path.length - 1]?.level >= level) { + path.pop(); + } + } + + path.push(newItem); + + const parentNode = path[path.length - 2]; + if (parentNode) { + newItem.parentId = parentNode.id; + } + + if (!newItem[key]) { + return acc.concat(newItem); + } else { + return acc.concat(omit(newItem, 'children'), generateInnerTree(newItem[key], key, level, path)); + } + }, []); +} + +export function omit(obj: T, ...keys: Array) { + return Object.entries(obj) + .filter((item) => !keys.includes(item[0])) + .reduce((acc, item) => Object.assign({}, acc, { [item[0]]: item[1] }), {}); +}