From 618cadd65e97b8e902ee58674148b794d57c80e6 Mon Sep 17 00:00:00 2001 From: Kagol Date: Wed, 30 Mar 2022 19:42:39 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(tree):=20=E5=A2=9E=E5=8A=A0=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E7=89=88=E6=9C=ACTreeFactory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devui-vue/devui/tree/src/core/data.ts | 62 +++++++++++ .../tree/src/core/tree-factory-interface.ts | 20 ++++ .../devui/tree/src/core/tree-factory-types.ts | 39 +++++++ .../devui/tree/src/core/tree-factory.ts | 101 ++++++++++++++++++ .../devui-vue/devui/tree/src/core/use-tree.ts | 17 +++ 5 files changed, 239 insertions(+) create mode 100644 packages/devui-vue/devui/tree/src/core/data.ts create mode 100644 packages/devui-vue/devui/tree/src/core/tree-factory-interface.ts create mode 100644 packages/devui-vue/devui/tree/src/core/tree-factory-types.ts create mode 100644 packages/devui-vue/devui/tree/src/core/tree-factory.ts create mode 100644 packages/devui-vue/devui/tree/src/core/use-tree.ts diff --git a/packages/devui-vue/devui/tree/src/core/data.ts b/packages/devui-vue/devui/tree/src/core/data.ts new file mode 100644 index 0000000000..4bb9afd04f --- /dev/null +++ b/packages/devui-vue/devui/tree/src/core/data.ts @@ -0,0 +1,62 @@ +import { ITree } from "./tree-factory-types"; + +export const treeData: ITree = [ + { + label: 'Parent node 1', + expanded: true, + children: [ + { + label: 'Parent node 1-1', + children: [ + { + label: 'Leaf node 1-1-1', + } + ] + }, + { + label: 'Leaf node 1-2', + }, + { + label: 'Leaf node 1-3', + }, + ] + }, + { + label: 'Leaf node 2', + } +]; + +export const treeDataId: ITree = [ + { + label: 'Parent node 1', + id: 'node-1', + expanded: true, + children: [ + { + label: 'Parent node 1-1', + id: 'node-1-1', + children: [ + { + label: 'Leaf node 1-1-1', + id: 'node-1-1-1', + } + ] + }, + { + label: 'Leaf node 1-2', + id: 'node-1-2', + }, + { + label: 'Leaf node 1-3', + id: 'node-1-3', + }, + ] + }, + { + label: 'Leaf node 2', + id: 'node-2', + }, + { + label: 'Leaf node 3', + } +]; diff --git a/packages/devui-vue/devui/tree/src/core/tree-factory-interface.ts b/packages/devui-vue/devui/tree/src/core/tree-factory-interface.ts new file mode 100644 index 0000000000..044907c9d5 --- /dev/null +++ b/packages/devui-vue/devui/tree/src/core/tree-factory-interface.ts @@ -0,0 +1,20 @@ +import type { valueof } from './tree-factory-types'; + +export interface ITreeFactory { + /** + * 1. 点选 + * 2. 勾选 + * 3. 展开/收起 + * 4. 插入节点 + * 5. 编辑节点 + * 6. 删除节点 + * 7. 改变节点顺序 + * 8. 禁用 + */ + + getTree(): T[]; + + setTree(tree: T[]): void; + + getTreeNode(value: valueof, key: keyof T): T[]; +}; 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..d10585c22f --- /dev/null +++ b/packages/devui-vue/devui/tree/src/core/tree-factory-types.ts @@ -0,0 +1,39 @@ +interface IBaseTreeNode { + label: string; + id?: string; + + selected?: boolean; + checked?: boolean; + expanded?: boolean; + + disableSelect?: boolean; + disableCheck?: boolean; + disableToggle?: boolean; +} + +interface IFlatTreeNode extends IBaseTreeNode { + parentId?: string; +} + +interface INestedTreeNode extends IBaseTreeNode { + children?: INestedTreeNode[]; +} + +// 外部数据结构先只考虑嵌套结构 +export type ITreeNode = INestedTreeNode; + +type IFlatTree = IFlatTreeNode[]; + +type INestedTree = INestedTreeNode[]; + +export type ITree = INestedTree; + +// 内部树节点使用扁平结构 +export interface IInnerTreeNode extends IFlatTreeNode { + level: number; + idType: 'random'; +} + +export type IInnerTree = IInnerTreeNode[]; + +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..3e6453fdc1 --- /dev/null +++ b/packages/devui-vue/devui/tree/src/core/tree-factory.ts @@ -0,0 +1,101 @@ +import { randomId } from '../../../anchor/src/util'; +import { omit } from '../util'; +import { ITreeFactory } from './tree-factory-interface'; +import type { ITree, ITreeNode, IInnerTree, IInnerTreeNode, valueof } from './tree-factory-types'; + +export default class TreeFactory { + private _innerTree: IInnerTree = []; + + constructor(tree: ITree) { + console.log('tree:', tree); + this._innerTree = traverseTree(tree); + } + + getTree() { + return convertInnerTree(this._innerTree); + } + + setTree(tree: ITree) { + this._innerTree = traverseTree(tree); + } + + getNodes(value: valueof, key: keyof ITreeNode = 'id', inclusive: boolean = false): ITree { + return convertInnerTree(this._innerTree.filter(item => item[key].indexOf(value) > -1)); + } + + getChildren(treeNode: ITreeNode): ITree { + return []; + } + + selectNode() {} + + checkNode() {} + + expandNode() {} + + collapseNode() {} + + toggleNode() {} + + disableSelectNode() {} + + disableCheckNode() {} + + disableToggleNode() {} + + insertBefore(parentNode: ITreeNode, node: ITreeNode, referenceNode: ITreeNode, cut: boolean = false) { + + } + + removeNode(node: ITreeNode) {} + + editNode(node: ITreeNode) {} +} + +function traverseTree(tree: ITree, key = 'children', level: number = 0, path: ITree = []): IInnerTree { + level++; + + return tree.reduce((acc: ITree, item: ITreeNode) => { + if (item.id === undefined) { + item.id = randomId(); + item.idType = 'random'; + } + + item.level = level; + + if (path.length > 0 && path[path.length - 1]?.level >= level) { + while (path[path.length - 1]?.level >= level) { + path.pop(); + } + } + + path.push(item); + + const parentNode = path[path.length - 2]; + if (parentNode) { + item.parentId = parentNode.id; + } + + if (!item[key]) { + return acc.concat(item); + } else { + return acc.concat(omit(item, 'children'), traverseTree(item[key], key, level, path)); + } + }, []); +} + +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] }), {}); +}; + +function convertInnerTree(innerTree: IInnerTree) { + return innerTree.map(item => { + const omitKeys = ['level', 'parentId', 'idType']; + if (item.idType === 'random') { + omitKeys.push('id') + } + return omit(item, ...omitKeys) + }); +} diff --git a/packages/devui-vue/devui/tree/src/core/use-tree.ts b/packages/devui-vue/devui/tree/src/core/use-tree.ts new file mode 100644 index 0000000000..ad66fb80b2 --- /dev/null +++ b/packages/devui-vue/devui/tree/src/core/use-tree.ts @@ -0,0 +1,17 @@ +import { ref } from 'vue'; +import { treeData, treeDataId } from './data'; +import TreeFactory from './tree-factory'; + +export default function useTree() { + const treeFactory = new TreeFactory(treeDataId); + console.log('treeFactory:', treeFactory, treeFactory._innerTree); + console.log('tree:', treeFactory.getTree()); + + // treeFactory._innerTree = [1, 2, 3]; + // console.log('treeFactory._innerTree:', treeFactory._innerTree); + console.log(treeFactory.getNodes('node-1')); + console.log(treeFactory.getNodes('Leaf', 'label')); + console.log(treeFactory.getNodes('node-1')); + + return {}; +} From dd54f6af61fa5b95ff6d8766cfa1cf51091d5476 Mon Sep 17 00:00:00 2001 From: Kagol Date: Thu, 31 Mar 2022 21:16:40 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(tree):=20=E5=A2=9E=E5=8A=A0=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E7=89=88TreeFactory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devui-vue/devui/tree/src/core/README.md | 66 ++++++++ .../devui-vue/devui/tree/src/core/data.ts | 62 -------- .../tree/src/core/tree-factory-interface.ts | 20 --- .../devui/tree/src/core/tree-factory-types.ts | 30 +--- .../devui/tree/src/core/tree-factory.ts | 142 +++++++++--------- .../devui-vue/devui/tree/src/core/use-tree.ts | 17 --- .../devui-vue/devui/tree/src/core/utils.ts | 68 +++++++++ 7 files changed, 211 insertions(+), 194 deletions(-) create mode 100644 packages/devui-vue/devui/tree/src/core/README.md delete mode 100644 packages/devui-vue/devui/tree/src/core/data.ts delete mode 100644 packages/devui-vue/devui/tree/src/core/tree-factory-interface.ts delete mode 100644 packages/devui-vue/devui/tree/src/core/use-tree.ts create mode 100644 packages/devui-vue/devui/tree/src/core/utils.ts 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/data.ts b/packages/devui-vue/devui/tree/src/core/data.ts deleted file mode 100644 index 4bb9afd04f..0000000000 --- a/packages/devui-vue/devui/tree/src/core/data.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { ITree } from "./tree-factory-types"; - -export const treeData: ITree = [ - { - label: 'Parent node 1', - expanded: true, - children: [ - { - label: 'Parent node 1-1', - children: [ - { - label: 'Leaf node 1-1-1', - } - ] - }, - { - label: 'Leaf node 1-2', - }, - { - label: 'Leaf node 1-3', - }, - ] - }, - { - label: 'Leaf node 2', - } -]; - -export const treeDataId: ITree = [ - { - label: 'Parent node 1', - id: 'node-1', - expanded: true, - children: [ - { - label: 'Parent node 1-1', - id: 'node-1-1', - children: [ - { - label: 'Leaf node 1-1-1', - id: 'node-1-1-1', - } - ] - }, - { - label: 'Leaf node 1-2', - id: 'node-1-2', - }, - { - label: 'Leaf node 1-3', - id: 'node-1-3', - }, - ] - }, - { - label: 'Leaf node 2', - id: 'node-2', - }, - { - label: 'Leaf node 3', - } -]; diff --git a/packages/devui-vue/devui/tree/src/core/tree-factory-interface.ts b/packages/devui-vue/devui/tree/src/core/tree-factory-interface.ts deleted file mode 100644 index 044907c9d5..0000000000 --- a/packages/devui-vue/devui/tree/src/core/tree-factory-interface.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { valueof } from './tree-factory-types'; - -export interface ITreeFactory { - /** - * 1. 点选 - * 2. 勾选 - * 3. 展开/收起 - * 4. 插入节点 - * 5. 编辑节点 - * 6. 删除节点 - * 7. 改变节点顺序 - * 8. 禁用 - */ - - getTree(): T[]; - - setTree(tree: T[]): void; - - getTreeNode(value: valueof, key: keyof T): T[]; -}; 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 index d10585c22f..378072dada 100644 --- a/packages/devui-vue/devui/tree/src/core/tree-factory-types.ts +++ b/packages/devui-vue/devui/tree/src/core/tree-factory-types.ts @@ -1,6 +1,8 @@ -interface IBaseTreeNode { +// 外部数据结构先只考虑嵌套结构 +export interface ITreeNode { label: string; id?: string; + children?: ITreeNode[]; selected?: boolean; checked?: boolean; @@ -11,29 +13,11 @@ interface IBaseTreeNode { disableToggle?: boolean; } -interface IFlatTreeNode extends IBaseTreeNode { - parentId?: string; -} - -interface INestedTreeNode extends IBaseTreeNode { - children?: INestedTreeNode[]; -} - -// 外部数据结构先只考虑嵌套结构 -export type ITreeNode = INestedTreeNode; - -type IFlatTree = IFlatTreeNode[]; - -type INestedTree = INestedTreeNode[]; - -export type ITree = INestedTree; - -// 内部树节点使用扁平结构 -export interface IInnerTreeNode extends IFlatTreeNode { +// 内部数据结构使用扁平结构 +export interface IInnerTreeNode extends ITreeNode { level: number; - idType: 'random'; + idType?: 'random'; + parentId?: string; } -export type IInnerTree = IInnerTreeNode[]; - 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 index 3e6453fdc1..33b3f592e1 100644 --- a/packages/devui-vue/devui/tree/src/core/tree-factory.ts +++ b/packages/devui-vue/devui/tree/src/core/tree-factory.ts @@ -1,101 +1,99 @@ -import { randomId } from '../../../anchor/src/util'; -import { omit } from '../util'; -import { ITreeFactory } from './tree-factory-interface'; -import type { ITree, ITreeNode, IInnerTree, IInnerTreeNode, valueof } from './tree-factory-types'; +import type { IInnerTreeNode, ITreeNode } from './tree-factory-types'; +import { flatToNested, generateInnerTree } from './utils'; export default class TreeFactory { - private _innerTree: IInnerTree = []; + private _innerTree: IInnerTreeNode[] = []; - constructor(tree: ITree) { - console.log('tree:', tree); - this._innerTree = traverseTree(tree); + private _getIndex(node: ITreeNode): number { + return this._innerTree.findIndex((item) => item.id === node.id); } - getTree() { - return convertInnerTree(this._innerTree); + private _setNodeValue(node, key, value): void { + this._innerTree[this._getIndex(node)][key] = value; } - setTree(tree: ITree) { - this._innerTree = traverseTree(tree); + constructor(tree: ITreeNode[]) { + this.setTree(tree); } - getNodes(value: valueof, key: keyof ITreeNode = 'id', inclusive: boolean = false): ITree { - return convertInnerTree(this._innerTree.filter(item => item[key].indexOf(value) > -1)); + getTree(flat?: boolean = false): IInnerTreeNode[] { + if (flat) { + return this._innerTree; + } else { + // TODO: 移除内部属性(level / parentId / idType) / 内部生成的id / 空children + return flatToNested(this._innerTree); + } } - getChildren(treeNode: ITreeNode): ITree { - return []; + setTree(tree: ITreeNode[]) { + this._innerTree = generateInnerTree(tree); } - selectNode() {} - - checkNode() {} - - expandNode() {} - - collapseNode() {} + getLevel(node: ITreeNode): number { + return this._innerTree.find((item) => item.id === node.id).level; + } - toggleNode() {} + getChildren(node: ITreeNode): IInnerTreeNode[] { + let result = []; + const startIndex = this._innerTree.findIndex((item) => item.id === node.id); - disableSelectNode() {} + for (let i = startIndex + 1; i < this._innerTree.length && this.getLevel(node) < this._innerTree[i].level; i++) { + result.push(this._innerTree[i]); + } + return result; + } - disableCheckNode() {} + selectNode(node: ITreeNode): void { + this._setNodeValue(node, 'selected', true); + } - disableToggleNode() {} + checkNode(node: ITreeNode): void { + this._setNodeValue(node, 'checked', true); + } - insertBefore(parentNode: ITreeNode, node: ITreeNode, referenceNode: ITreeNode, cut: boolean = false) { + uncheckNode(node: ITreeNode): void { + this._setNodeValue(node, 'checked', false); + } + expandNode(node: ITreeNode): void { + this._setNodeValue(node, 'expanded', true); } - removeNode(node: ITreeNode) {} + collapseNode(node: ITreeNode): void { + this._setNodeValue(node, 'expanded', false); + } - editNode(node: ITreeNode) {} -} + toggleNode(node: ITreeNode): void { + if (node.expanded) { + this._setNodeValue(node, 'expanded', false); + } else { + this._setNodeValue(node, 'expanded', true); + } + } -function traverseTree(tree: ITree, key = 'children', level: number = 0, path: ITree = []): IInnerTree { - level++; + disableSelectNode(node: ITreeNode): void { + this._setNodeValue(node, 'disableSelect', true); + } - return tree.reduce((acc: ITree, item: ITreeNode) => { - if (item.id === undefined) { - item.id = randomId(); - item.idType = 'random'; - } + disableCheckNode(node: ITreeNode): void { + this._setNodeValue(node, 'disableCheck', true); + } - item.level = level; + disableToggleNode(node: ITreeNode): void { + this._setNodeValue(node, 'disableToggle', true); + } - if (path.length > 0 && path[path.length - 1]?.level >= level) { - while (path[path.length - 1]?.level >= level) { - path.pop(); - } - } + insertBefore(parentNode: ITreeNode, node: ITreeNode, referenceNode: ITreeNode, cut: boolean = false): void { + // TODO + } - path.push(item); - - const parentNode = path[path.length - 2]; - if (parentNode) { - item.parentId = parentNode.id; - } - - if (!item[key]) { - return acc.concat(item); - } else { - return acc.concat(omit(item, 'children'), traverseTree(item[key], key, level, path)); - } - }, []); -} + 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); + }) + } -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] }), {}); -}; - -function convertInnerTree(innerTree: IInnerTree) { - return innerTree.map(item => { - const omitKeys = ['level', 'parentId', 'idType']; - if (item.idType === 'random') { - omitKeys.push('id') - } - return omit(item, ...omitKeys) - }); + editNode(node: ITreeNode, label: string): void { + this._setNodeValue(node, 'label', label); + } } diff --git a/packages/devui-vue/devui/tree/src/core/use-tree.ts b/packages/devui-vue/devui/tree/src/core/use-tree.ts deleted file mode 100644 index ad66fb80b2..0000000000 --- a/packages/devui-vue/devui/tree/src/core/use-tree.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ref } from 'vue'; -import { treeData, treeDataId } from './data'; -import TreeFactory from './tree-factory'; - -export default function useTree() { - const treeFactory = new TreeFactory(treeDataId); - console.log('treeFactory:', treeFactory, treeFactory._innerTree); - console.log('tree:', treeFactory.getTree()); - - // treeFactory._innerTree = [1, 2, 3]; - // console.log('treeFactory._innerTree:', treeFactory._innerTree); - console.log(treeFactory.getNodes('node-1')); - console.log(treeFactory.getNodes('Leaf', 'label')); - console.log(treeFactory.getNodes('node-1')); - - return {}; -} 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] }), {}); +}