Explorar el Código

feat: tree initial commit

vian hace 5 años
padre
commit
59a7f87d7a

+ 1 - 0
tree/.eslintignore

@@ -0,0 +1 @@
+/dist

+ 42 - 0
tree/.eslintrc.js

@@ -0,0 +1,42 @@
+module.exports = {
+  env: {
+    browser: true,
+    es2021: true,
+    node: true,
+  },
+  extends: ['airbnb-base', 'plugin:@typescript-eslint/recommended', 'prettier'],
+  parser: '@typescript-eslint/parser',
+  parserOptions: {
+    ecmaVersion: 12,
+    sourceType: 'module',
+  },
+  plugins: ['@typescript-eslint', 'prettier'],
+  rules: {
+    'prettier/prettier': 'error',
+    'import/extensions': [
+      'error',
+      {
+        js: 'never',
+        jsx: 'never',
+        ts: 'never',
+        tsx: 'never',
+        json: 'always',
+      },
+    ],
+    'import/no-unresolved': 'off',
+    '@typescript-eslint/no-empty-function': 'off',
+    '@typescript-eslint/no-explicit-any': 'off',
+    'no-unused-expressions': [
+      'error',
+      {
+        allowShortCircuit: true,
+      },
+    ],
+    'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
+    'no-restricted-syntax': 'off',
+    'no-shadow': 'off',
+    '@typescript-eslint/no-shadow': 'error',
+    'no-continue': 'off',
+    'no-param-reassign': 'off',
+  },
+};

+ 21 - 0
tree/.gitignore

@@ -0,0 +1,21 @@
+.DS_Store
+node_modules
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 6 - 0
tree/.huskyrc.js

@@ -0,0 +1,6 @@
+module.exports = {
+  hooks: {
+    'pre-commit': 'lint-staged',
+    'commit-msg': 'commitlint -E HUSKY_GIT_PARAMS',
+  },
+};

+ 29 - 0
tree/commitlint.config.js

@@ -0,0 +1,29 @@
+module.exports = {
+  ignores: [commit => commit.includes('init')],
+  extends: ['@commitlint/config-conventional'],
+  rules: {
+    'body-leading-blank': [2, 'always'],
+    'footer-leading-blank': [1, 'always'],
+    'header-max-length': [2, 'always', 108],
+    'subject-empty': [2, 'never'],
+    'type-empty': [2, 'never'],
+    'type-enum': [
+      2,
+      'always',
+      [
+        'feat', // 新增功能、变更需求
+        'fix', // 修复bug
+        'perf', // 优化性能
+        'refactor', // 代码重构
+        'style', // 代码格式(不影响功能,例如空格、分号等格式修正)
+        'docs', // 文档变更
+        'test', // 测试
+        'ci', // 更改持续集成软件的配置文件和package中的scripts命令,例如scopes: Travis, Circle等
+        'chore', // 变更构建流程或辅助工具(依赖更新/脚手架配置修改/webpack、gulp、npm等)
+        'revert', // 代码回退
+        'types', // ts类型定义文件更改
+        'wip', // work in process开发中
+      ],
+    ],
+  },
+};

+ 9 - 0
tree/lint-staged.config.js

@@ -0,0 +1,9 @@
+module.exports = {
+  '*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
+  '{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': [
+    'prettier --write--parser json',
+  ],
+  'package.json': ['prettier --write'],
+  '*.vue': ['prettier --write'],
+  '*.md': ['prettier --write'],
+};

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 4335 - 0
tree/package-lock.json


+ 48 - 0
tree/package.json

@@ -0,0 +1,48 @@
+{
+  "name": "@sc/tree",
+  "version": "1.0.0",
+  "description": "a template for npm package coding",
+  "main": "./dist/index.cjs.js",
+  "module": "./dist/index.esm.js",
+  "browser": "./dist/index.min.js",
+  "types": "./dist/index.d.ts",
+  "scripts": {
+    "test": "cross-env TS_NODE_COMPILER_OPTIONS={\\\"module\\\":\\\"commonjs\\\"} mocha -r ts-node/register 'tests/**/*.ts'",
+    "build": "rollup -c"
+  },
+  "keywords": [],
+  "author": "smartcost",
+  "license": "ISC",
+  "devDependencies": {
+    "@commitlint/cli": "^11.0.0",
+    "@commitlint/config-conventional": "^11.0.0",
+    "@types/chai": "^4.2.14",
+    "@types/lodash": "^4.14.165",
+    "@types/mocha": "^8.0.4",
+    "@types/ms": "^0.7.31",
+    "@typescript-eslint/eslint-plugin": "^4.4.1",
+    "@typescript-eslint/parser": "^4.4.1",
+    "chai": "^4.2.0",
+    "cross-env": "^7.0.2",
+    "eslint": "^7.11.0",
+    "eslint-config-airbnb-base": "^14.2.0",
+    "eslint-config-prettier": "^6.12.0",
+    "eslint-plugin-import": "^2.22.1",
+    "eslint-plugin-prettier": "^3.1.4",
+    "husky": "^4.3.0",
+    "lint-staged": "^10.5.0",
+    "mocha": "^8.2.1",
+    "prettier": "^2.1.2",
+    "rollup": "^2.30.0",
+    "rollup-plugin-commonjs": "^10.1.0",
+    "rollup-plugin-node-resolve": "^5.2.0",
+    "rollup-plugin-terser": "^7.0.2",
+    "rollup-plugin-typescript2": "^0.27.3",
+    "ts-node": "^9.0.0",
+    "tslib": "^2.0.3",
+    "typescript": "^4.0.3"
+  },
+  "dependencies": {
+    "lodash": "^4.17.20"
+  }
+}

+ 6 - 0
tree/prettier.config.js

@@ -0,0 +1,6 @@
+module.exports = {
+  singleQuote: true, // 单引号
+  trailingComma: 'es5', // 对象末尾以逗号结束
+  arrowParens: 'avoid', // 箭头函数只有一个参数的时候,不使用()
+  endOfLine: 'auto', // CRLF,LF都可以
+};

+ 28 - 0
tree/rollup.config.js

@@ -0,0 +1,28 @@
+// import resolve from 'rollup-plugin-node-resolve';
+// import commonjs from 'rollup-plugin-commonjs';
+import typescript from 'rollup-plugin-typescript2'; // 一定要是typescript2,如果使用typescript,没法自动生成.d.ts文件
+// import { terser } from 'rollup-plugin-terser';
+import pkg from './package.json';
+
+export default [
+  // UMD for browser
+  /* {
+    input: 'src/index.ts',
+    output: {
+      name: 'howLongUntilLunch',
+      file: pkg.browser,
+      format: 'umd',
+    },
+    plugins: [resolve(), commonjs(), typescript(), terser()], // 浏览器使用的代码文件进行简化
+  }, */
+  // CommonJS for Node and ES module for bundlers build
+  {
+    input: 'src/index.ts',
+    external: ['ms'],
+    plugins: [typescript()],
+    output: [
+      { file: pkg.main, format: 'cjs' },
+      // { file: pkg.module, format: 'es' },
+    ],
+  },
+];

+ 500 - 0
tree/src/index.ts

@@ -0,0 +1,500 @@
+import NodeContext from './nodeCtx';
+
+export interface TreeRaw {
+  ID: string;
+  parentID: string;
+  seq: number;
+  [propName: string]: any;
+}
+
+export interface TreeNode extends TreeRaw {
+  ctx: NodeContext;
+}
+
+export interface TreeIDMap {
+  [propName: string]: TreeNode;
+}
+
+export interface TreeParentMap {
+  [propName: string]: TreeNode[];
+}
+
+export interface ParentMap {
+  [propName: string]: (TreeRaw | TreeNode)[];
+}
+
+export interface UpdateItem {
+  parentID?: string;
+  seq?: number;
+  [propName: string]: any;
+}
+
+export interface UpdateData {
+  ID: string;
+  update: UpdateItem;
+}
+
+export type None = null | undefined;
+
+export class Tree {
+  // 原始数据,不经过排序的数据
+  rawData: TreeNode[];
+
+  // 按照树结构拼装好、排好序的数据。实际只是原始数据进行排序,内部元素跟原始数据内部元素的引用是一致的
+  data: TreeNode[];
+
+  readonly rootID: string = '-1';
+
+  readonly seqStartIndex = 0;
+
+  // ID与原始数据条目映射
+  IDMap: TreeIDMap;
+
+  // parentID与多条同parentID的原始数据条目映射,parentMap内部数组要始终保持正确排序
+  parentMap: TreeParentMap;
+
+  constructor(rawData: TreeRaw[]) {
+    this.rawData = this.genNodeContext(rawData);
+    this.rawData = Tree.sort(this.rawData);
+    this.data = [];
+    this.IDMap = {};
+    this.parentMap = {};
+    this.genMap();
+    this.genData();
+  }
+
+  // 根据seq进行排序
+  static sort<T extends TreeNode | TreeRaw>(nodes: T[]): T[] {
+    return nodes.sort((a, b) => a.seq - b.seq);
+  }
+
+  // 获取节点的所有parentID
+  static getParentIDList(nodes: TreeNode[]): Set<string> {
+    const parentIDList: Set<string> = new Set();
+    nodes.forEach(node => parentIDList.add(node.parentID));
+    return parentIDList;
+  }
+
+  // 生成节点ctx上下文,挂载相关节点方法,TreeRaw转换为TreeNode
+  genNodeContext(rawData: TreeRaw[]): TreeNode[] {
+    rawData.forEach(item => {
+      item.ctx = new NodeContext(item as TreeNode, this);
+    });
+    return rawData as TreeNode[];
+  }
+
+  // 生成映射表
+  genMap(): void {
+    this.rawData.forEach(raw => {
+      this.IDMap[raw.ID] = raw;
+      (
+        this.parentMap[raw.parentID] || (this.parentMap[raw.parentID] = [])
+      ).push(raw);
+    });
+  }
+
+  // 获取顶层原始数据
+  getRoots(): TreeNode[] {
+    return this.parentMap[this.rootID];
+  }
+
+  // 生成按照树结构排好序的数据
+  genData(): void {
+    // genData时不需要排序,因为rawData已经排好序
+    const roots = this.getRoots();
+    const pushNodesToData = (nodes: TreeNode[]): void => {
+      nodes.forEach(node => {
+        this.data.push(node);
+        const children = this.parentMap[node.ID];
+        if (children && children.length) {
+          pushNodesToData(children);
+        }
+      });
+    };
+    pushNodesToData(roots);
+  }
+
+  // 重新生成排序序好的数据,不改变原引用
+  reGenData(): void {
+    this.data.splice(0, this.data.length);
+    this.genData();
+  }
+
+  // 将相关parentMap内的数据、data进行重新排序。新增、删除等操作进行完后,数据需要重新排序
+  reSortData(nodes: TreeNode[]): void {
+    const toSortList = Tree.getParentIDList(nodes);
+    toSortList.forEach(parentID => {
+      if (this.parentMap[parentID] && this.parentMap[parentID].length) {
+        Tree.sort(this.parentMap[parentID]);
+      }
+    });
+    this.reGenData();
+  }
+
+  // 根据parentID对parentMap进行重新排序,并重新排序生成data
+  resortDataByID(parentIDList: string[]): void {
+    parentIDList.forEach(parentID => {
+      if (this.parentMap[parentID] && this.parentMap[parentID].length) {
+        Tree.sort(this.parentMap[parentID]);
+      }
+    });
+    this.reGenData();
+  }
+
+  // 查找ID节点
+  find(ID: string): TreeNode | None {
+    return this.IDMap[ID];
+  }
+
+  // 查找ID节点的父节点
+  findParent(ID: string): TreeNode | None {
+    const node = this.find(ID);
+    if (!node) {
+      return null;
+    }
+    return this.find(node.parentID);
+  }
+
+  // 查找ID节点的下一个节点
+  findNext(ID: string): TreeNode | None {
+    const node = this.find(ID);
+    if (!node) {
+      return null;
+    }
+    const nodes = this.parentMap[node.parentID];
+    const nodeIndex = nodes.indexOf(node);
+    if (nodeIndex < 0) {
+      return null;
+    }
+    return nodes[nodeIndex + 1];
+  }
+
+  // 查找ID节点的上一个节点
+  findPrev(ID: string): TreeNode | None {
+    const node = this.find(ID);
+    if (!node) {
+      return null;
+    }
+    const nodes = this.parentMap[node.parentID];
+    const nodeIndex = nodes.indexOf(node);
+    if (nodeIndex < 0) {
+      return null;
+    }
+    return nodes[nodeIndex - 1];
+  }
+
+  // 查询ID节点的子节点
+  findChilren(ID: string): TreeNode[] {
+    return this.parentMap[ID] || [];
+  }
+
+  updateValue(updateData: UpdateData[]): void {
+    if (updateData.length) {
+      updateData.forEach(updateItem => {
+        const node = this.find(updateItem.ID);
+        if (node) {
+          Object.assign(node, updateItem.update);
+        }
+      });
+    }
+  }
+
+  // 准备插入节点,插入节点前,计算出需要更新的数据。
+  // 可调用完此方法后,将需要更新、插入的数据提交至数据库,成功响应后调用插入节点更新缓存的方法
+  prepareInsert(rawData: TreeRaw[]): UpdateData[] {
+    const updateData: UpdateData[] = [];
+    const insertParentMap: ParentMap = {};
+    // 将相同父项的插入数据和已存在数据进行合并
+    rawData.forEach(item => {
+      if (typeof item.seq === 'undefined') {
+        item.seq = this.seqStartIndex;
+      }
+      (
+        insertParentMap[item.parentID] || (insertParentMap[item.parentID] = [])
+      ).push(item);
+    });
+    Object.entries(insertParentMap).forEach(([parentID, insertItems]) => {
+      const items = this.parentMap[parentID];
+      if (items) {
+        insertParentMap[parentID].push(...items);
+      }
+      // 重新排序
+      const combineItems = insertParentMap[parentID];
+      Tree.sort(combineItems);
+      combineItems.forEach((item, index) => {
+        // 插入数据重新赋值
+        if (insertItems.includes(item)) {
+          item.seq = index;
+        } else if (item.seq !== index) {
+          // 需要更新的原数据
+          updateData.push({
+            ID: item.ID,
+            update: {
+              seq: index,
+            },
+          });
+        }
+      });
+    });
+    return updateData;
+  }
+
+  // 插入节点数据
+  insert(items: TreeRaw[], updateData: UpdateData[] = []): TreeNode[] {
+    // 更新需要更新的节点(一般为更新seq)
+    this.updateValue(updateData);
+    // 建立映射、插入数据
+    const nodes = this.genNodeContext(items);
+    nodes.forEach(node => {
+      this.IDMap[node.ID] = node;
+      (
+        this.parentMap[node.parentID] || (this.parentMap[node.parentID] = [])
+      ).push(node);
+      this.rawData.push(node);
+    });
+    // 排序
+    this.reSortData(nodes);
+    return this.data;
+  }
+
+  // 准备删除,获取删除时,需要更新的节点
+  prepareDelete(deleteNodes: TreeNode[]): UpdateData[] {
+    const updateData: UpdateData[] = [];
+    // 获取去掉删除节点后,未删除同层节点需要更新seq的数据
+    const parentIDList = Tree.getParentIDList(deleteNodes);
+    parentIDList.forEach(parentID => {
+      const brothers = this.parentMap[parentID];
+      const tempBrothers = brothers.filter(node => !deleteNodes.includes(node));
+      Tree.sort(tempBrothers);
+      tempBrothers.forEach((node, index) => {
+        if (node.seq !== index) {
+          updateData.push({
+            ID: node.ID,
+            update: { seq: index },
+          });
+        }
+      });
+    });
+    return updateData;
+  }
+
+  /**
+   * 删除节点
+   * @param {TreeNode[]} treeNodes - 要删除的节点,不需要包含嵌套节点
+   * @param {UpdateData[]} updateData - 要更新的数据
+   * @return {TreeNode[]} 返回删除节点后,树的data
+   */
+  delete(treeNodes: TreeNode[], updateData: UpdateData[]): TreeNode[] {
+    this.updateValue(updateData);
+    const allDeletedNodes: TreeNode[] = [];
+    // 递归删除节点
+    const deleteNodes = (nodes: TreeNode[]): void => {
+      // 删除映射、删除数据
+      nodes.forEach(node => {
+        allDeletedNodes.push(node);
+        delete this.IDMap[node.ID];
+        const children = this.parentMap[node.ID];
+        delete this.parentMap[node.ID];
+        const nodesInParentMap = this.parentMap[node.parentID];
+        if (nodesInParentMap && nodesInParentMap.length) {
+          const nIndex = nodesInParentMap.indexOf(node);
+          if (nIndex >= 0) {
+            nodesInParentMap.splice(nIndex, 1);
+          }
+        }
+        const index = this.rawData.indexOf(node);
+        if (index >= 0) {
+          this.rawData.splice(index, 1);
+        }
+        if (children && children.length) {
+          deleteNodes(children);
+        }
+      });
+    };
+    deleteNodes(treeNodes);
+    // 排序
+    this.reSortData(allDeletedNodes);
+    return this.data;
+  }
+
+  prepareDeleteByID(IDList: string[]): UpdateData[] {
+    const deleteNodes: TreeNode[] = [];
+    IDList.forEach(ID => {
+      const node = this.find(ID);
+      if (node) {
+        deleteNodes.push(node);
+      }
+    });
+    return this.prepareDelete(deleteNodes);
+  }
+
+  /**
+   * 根据ID删除节点
+   * @param {string[]} IDList - 要删除的节点的ID列表
+   * @param {UpdateData[]} updateData - 要更新的数据
+   * @return {TreeNode[]} - 返回被删除的所有节点
+   */
+  deleteByID(IDList: string[], updateData: UpdateData[]): TreeNode[] {
+    const deleteNodes: TreeNode[] = [];
+    IDList.forEach(ID => {
+      const node = this.find(ID);
+      if (node) {
+        deleteNodes.push(node);
+      }
+    });
+    this.delete(deleteNodes, updateData);
+    return deleteNodes;
+  }
+
+  // 准备上移节点块(连续的兄弟节点)
+  prepareUpMove(nodes: TreeNode[]): UpdateData[] {
+    const updateData: UpdateData[] = [];
+    const firstNode = nodes[0];
+    const firstNodePrev = this.findPrev(firstNode.ID);
+    if (!firstNodePrev) {
+      return [];
+    }
+    nodes.forEach(node => {
+      updateData.push({
+        ID: node.ID,
+        update: { seq: node.seq - 1 },
+      });
+    });
+    updateData.push({
+      ID: firstNodePrev.ID,
+      update: { seq: firstNodePrev.seq + nodes.length },
+    });
+    return updateData;
+  }
+
+  // 准备下移节点块(连续的兄弟节点)
+  prepareDownMove(nodes: TreeNode[]): UpdateData[] {
+    const updateData: UpdateData[] = [];
+    const lastNode = nodes[nodes.length - 1];
+    const lastNodeNext = this.findNext(lastNode.ID);
+    if (!lastNodeNext) {
+      return [];
+    }
+    nodes.forEach(node => {
+      updateData.push({
+        ID: node.ID,
+        update: { seq: node.seq + 1 },
+      });
+    });
+    updateData.push({
+      ID: lastNodeNext.ID,
+      update: { seq: lastNodeNext.seq - nodes.length },
+    });
+    return updateData;
+  }
+
+  // 上下移
+  move(nodes: TreeNode[], updateData: UpdateData[]): void {
+    this.updateValue(updateData);
+    this.reSortData(nodes);
+  }
+
+  // 准备升级节点块(连续的兄弟节点)
+  prepareUpLevel(nodes: TreeNode[]): UpdateData[] {
+    const updateData: UpdateData[] = [];
+    const firstNode = nodes[0];
+    const lastNode = nodes[nodes.length - 1];
+    const parent = this.findParent(firstNode.ID);
+    if (!parent) {
+      return [];
+    }
+    const baseSeq = parent.seq + 1;
+    nodes.forEach((node, index) => {
+      updateData.push({
+        ID: node.ID,
+        update: { parentID: parent.parentID, seq: baseSeq + index },
+      });
+    });
+    const parentNextBrothers = parent.ctx.nextBrothers();
+    parentNextBrothers.forEach((node, index) => {
+      updateData.push({
+        ID: node.ID,
+        update: { seq: baseSeq + index + nodes.length },
+      });
+    });
+    // 最末节点的所有后兄弟节点,成为最末节点的子节点
+    const lastNodeNextBrothers = lastNode.ctx.nextBrothers();
+    lastNodeNextBrothers.forEach((node, index) => {
+      updateData.push({
+        ID: node.ID,
+        update: { parentID: lastNode.ID, seq: index },
+      });
+    });
+    return updateData;
+  }
+
+  upLevel(nodes: TreeNode[], updateData: UpdateData[]): void {
+    const firstNode = nodes[0];
+    const lastNode = nodes[nodes.length - 1];
+    if (!firstNode.ctx.canUpLevel()) {
+      return;
+    }
+    const orgParentID = firstNode.parentID;
+    const orgBrothers = this.parentMap[orgParentID];
+    const lastNodeNextBrothers = lastNode.ctx.nextBrothers();
+    orgBrothers.splice(
+      orgBrothers.indexOf(firstNode),
+      nodes.length + lastNodeNextBrothers.length
+    );
+    (this.parentMap[lastNode.ID] || (this.parentMap[lastNode.ID] = [])).push(
+      ...lastNodeNextBrothers
+    );
+    this.updateValue(updateData);
+    const newParentID = firstNode.parentID;
+    this.parentMap[newParentID].push(...nodes);
+    this.resortDataByID([orgParentID, newParentID, lastNode.ID]);
+  }
+
+  // 准备降级节点块(连续的兄弟节点)
+  prepareDownLevel(nodes: TreeNode[]): UpdateData[] {
+    const updateData: UpdateData[] = [];
+    const firstNode = nodes[0];
+    const lastNode = nodes[nodes.length - 1];
+    const prevNode = this.findPrev(firstNode.ID);
+    if (!prevNode) {
+      return [];
+    }
+    // 节点块成为前节点的子节点
+    const prevNodeLastChild = prevNode.ctx.lastChild();
+    const baseSeq = prevNodeLastChild
+      ? prevNodeLastChild.seq + 1
+      : this.seqStartIndex;
+    nodes.forEach((node, index) => {
+      updateData.push({
+        ID: node.ID,
+        update: { parentID: prevNode.ID, seq: baseSeq + index },
+      });
+    });
+    // 最末节点的后兄弟节点们,重新设置seq
+    const lastNodeNextBrothers = lastNode.ctx.nextBrothers();
+    lastNodeNextBrothers.forEach((node, index) => {
+      updateData.push({
+        ID: node.ID,
+        update: { seq: prevNode.seq + 1 + index },
+      });
+    });
+    return updateData;
+  }
+
+  downLevel(nodes: TreeNode[], updateData: UpdateData[]): void {
+    const firstNode = nodes[0];
+    if (!firstNode.ctx.canDownLevel()) {
+      return;
+    }
+    const prevNode = this.findPrev(firstNode.ID);
+    if (!prevNode) {
+      return;
+    }
+    const orgBrothers = this.parentMap[firstNode.parentID];
+    orgBrothers.splice(orgBrothers.indexOf(firstNode), nodes.length);
+    (this.parentMap[prevNode.ID] || (this.parentMap[prevNode.ID] = [])).push(
+      ...nodes
+    );
+    this.updateValue(updateData);
+  }
+}

+ 168 - 0
tree/src/nodeCtx.ts

@@ -0,0 +1,168 @@
+import { TreeNode, Tree, None } from './index';
+
+class NodeContext {
+  // 对树节点数据的引用
+  ref: TreeNode;
+
+  tree: Tree;
+
+  // 展开收起
+  expanded = false;
+
+  constructor(node: TreeNode, tree: Tree) {
+    this.ref = node;
+    this.tree = tree;
+  }
+
+  ID(): string {
+    return this.ref.ID;
+  }
+
+  parentID(): string {
+    return this.ref.parentID;
+  }
+
+  // 节点在同层数据中的顺序
+  seq(): number {
+    return this.ref.seq;
+  }
+
+  // 节点在完整、排好序的树数据中的行号
+  row(): number {
+    return this.tree.data.indexOf(this.ref);
+  }
+
+  depth(): number {
+    const parent = this.parent();
+    return parent ? parent.ctx.depth() + 1 : 0;
+  }
+
+  // 节点是否可见,根据先代节点的expanded就可以计算出来,不需要维护visible属性
+  visible(): boolean {
+    let parent = this.parent();
+    while (parent) {
+      if (parent.ctx.expanded) {
+        return false;
+      }
+      parent = parent.ctx.parent();
+    }
+    return true;
+  }
+
+  parent(): TreeNode | None {
+    return this.tree.findParent(this.ID());
+  }
+
+  next(): TreeNode | None {
+    return this.tree.findNext(this.ID());
+  }
+
+  prev(): TreeNode | None {
+    return this.tree.findPrev(this.ID());
+  }
+
+  // 获取节点子项
+  chilren(): TreeNode[] {
+    return this.tree.parentMap[this.ID()] || [];
+  }
+
+  firstChild(): TreeNode | None {
+    return this.chilren()[0] || null;
+  }
+
+  lastChild(): TreeNode | None {
+    const children = this.chilren();
+    return children[children.length - 1] || null;
+  }
+
+  // 获取节点后代(包含嵌套子项)
+  posterity(): TreeNode[] {
+    const posterity: TreeNode[] = [];
+    const getChild = (nodes: TreeNode[]): void => {
+      nodes.forEach(node => {
+        posterity.push(node);
+        const children = node.ctx.chilren();
+        if (children.length) {
+          getChild(children);
+        }
+      });
+    };
+    getChild(this.chilren());
+    return posterity;
+  }
+
+  posterityCount(): number {
+    return this.posterity().length;
+  }
+
+  // 获取节点最上层的父项(起源)
+  progenitor(): TreeNode | None {
+    let parent = this.parent();
+    while (parent && parent.ctx.parent()) {
+      parent = parent.ctx.parent();
+    }
+    return parent;
+  }
+
+  // 获取节点所有先代(包含嵌套父项)
+  ancestor(): TreeNode[] {
+    const ancestor: TreeNode[] = [];
+    let parent = this.parent();
+    while (parent) {
+      ancestor.push(parent);
+      parent = parent.ctx.parent();
+    }
+    return ancestor;
+  }
+
+  ancestorCount(): number {
+    return this.ancestor().length;
+  }
+
+  // 获取同层节点
+  brothers(includeSelf = true): TreeNode[] {
+    let nodes = this.tree.parentMap[this.parentID()] || [];
+    if (!includeSelf) {
+      nodes = nodes.filter(node => node !== this.ref);
+    }
+    return nodes;
+  }
+
+  brothersCount(includeSelf = true): number {
+    return this.brothers(includeSelf).length;
+  }
+
+  // 获取后兄弟节点们
+  nextBrothers(): TreeNode[] {
+    const nodes = this.tree.parentMap[this.parentID()] || [];
+    return nodes.filter(node => node.seq >= this.seq() && node !== this.ref);
+  }
+
+  // 获取前兄弟节点们
+  prevBrothers(): TreeNode[] {
+    const nodes = this.tree.parentMap[this.parentID()] || [];
+    return nodes.filter(node => node.seq <= this.seq() && node !== this.ref);
+  }
+
+  // 只有前节点存在才可上移
+  canUpMove(): boolean {
+    return !!this.prev();
+  }
+
+  // 只有后节点存在才可下移
+  canDownMove(): boolean {
+    return !!this.next();
+  }
+
+  // 只有父节点存在才可升级
+  canUpLevel(): boolean {
+    return !!this.parent();
+  }
+
+  // 只有前节点存在才可降级
+  canDownLevel(): boolean {
+    return !!this.prev();
+  }
+}
+
+export default NodeContext;

+ 174 - 0
tree/tests/nodeCtx.ts

@@ -0,0 +1,174 @@
+import { expect } from 'chai';
+import { Tree, TreeNode, TreeRaw } from '../src';
+
+function getIDList(nodes: TreeNode[]): string[] {
+  return nodes.map(node => node.ID);
+}
+
+describe('NodeCtx', () => {
+  const rawData: TreeRaw[] = [
+    { ID: '1', parentID: '-1', seq: 1 },
+    { ID: '2', parentID: '-1', seq: 3 },
+    { ID: '3', parentID: '-1', seq: 2 },
+    { ID: '4', parentID: '-1', seq: 4 },
+    { ID: '5', parentID: '-1', seq: 5 },
+    { ID: '6', parentID: '1', seq: 2 },
+    { ID: '7', parentID: '1', seq: 1 },
+    { ID: '8', parentID: '7', seq: 1 },
+    { ID: '9', parentID: '2', seq: 1 },
+  ];
+
+  const tree = new Tree(rawData);
+
+  it('row', () => {
+    const node = tree.find('4');
+    if (node) {
+      const row = node.ctx.row();
+      expect(row).to.equal(7);
+    }
+  });
+
+  it('depth', () => {
+    const node = tree.find('1');
+    if (node) {
+      const depth = node.ctx.depth();
+      expect(depth).to.equal(0);
+    }
+    const deepNode = tree.find('8');
+    if (deepNode) {
+      const depth = deepNode.ctx.depth();
+      expect(depth).to.equal(2);
+    }
+  });
+
+  it('visible', () => {
+    const node = tree.find('1');
+    if (node) {
+      node.ctx.expanded = true;
+      const testNode = tree.find('8');
+      if (testNode) {
+        const visibleA = testNode.ctx.visible();
+        expect(visibleA).to.equal(false);
+        node.ctx.expanded = false;
+        const visibleB = testNode.ctx.visible();
+        expect(visibleB).to.equal(true);
+      }
+    }
+  });
+
+  it('parent', () => {
+    const node = tree.find('6');
+    if (node) {
+      const parent = node.ctx.parent();
+      expect(parent).to.have.property('ID', '1');
+    }
+  });
+
+  it('parent', () => {
+    const node = tree.find('6');
+    if (node) {
+      const parent = node.ctx.parent();
+      expect(parent).to.have.property('ID', '1');
+    }
+  });
+
+  it('next', () => {
+    const node = tree.find('1');
+    if (node) {
+      const next = node.ctx.next();
+      expect(next).to.have.property('ID', '3');
+    }
+  });
+
+  it('prev', () => {
+    const node = tree.find('3');
+    if (node) {
+      const prev = node.ctx.prev();
+      expect(prev).to.have.property('ID', '1');
+    }
+  });
+
+  it('children', () => {
+    const node = tree.find('1');
+    if (node) {
+      const children = node.ctx.chilren();
+      const IDList = getIDList(children);
+      expect(IDList).to.have.ordered.members(['7', '6']);
+    }
+  });
+
+  it('firstChild', () => {
+    const node = tree.find('1');
+    if (node) {
+      const firstChild = node.ctx.firstChild();
+      expect(firstChild).to.have.property('ID', '7');
+    }
+  });
+
+  it('lastChild', () => {
+    const node = tree.find('1');
+    if (node) {
+      const lastChild = node.ctx.lastChild();
+      expect(lastChild).to.have.property('ID', '6');
+    }
+  });
+
+  it('posterity', () => {
+    const node = tree.find('1');
+    if (node) {
+      const posterity = node.ctx.posterity();
+      const IDList = getIDList(posterity);
+      expect(IDList).to.have.ordered.members(['7', '8', '6']);
+    }
+  });
+
+  it('progenitor', () => {
+    const node = tree.find('8');
+    if (node) {
+      const progenitor = node.ctx.progenitor();
+      if (progenitor) {
+        expect(progenitor).to.have.property('ID', '1');
+      }
+    }
+  });
+
+  it('ancestor', () => {
+    const node = tree.find('8');
+    if (node) {
+      const ancestor = node.ctx.ancestor();
+      const IDList = getIDList(ancestor);
+      expect(IDList).to.have.ordered.members(['7', '1']);
+    }
+  });
+
+  it('brothers', () => {
+    const node = tree.find('6');
+    if (node) {
+      const brothers = node.ctx.brothers();
+      const IDList = getIDList(brothers);
+      expect(IDList).to.have.ordered.members(['7', '6']);
+
+      const excludeBrothers = node.ctx.brothers(false);
+      const exIDList = getIDList(excludeBrothers);
+      expect(exIDList).to.have.ordered.members(['7']);
+    }
+  });
+
+  it('nextBrothers', () => {
+    const node = tree.find('2');
+    if (node) {
+      const nextBrothers = node.ctx.nextBrothers();
+      const IDList = getIDList(nextBrothers);
+      expect(IDList).to.have.ordered.members(['4', '5']);
+    }
+  });
+
+  it('prevBrothers', () => {
+    const node = tree.find('2');
+    if (node) {
+      const prevBrothers = node.ctx.prevBrothers();
+      const IDList = getIDList(prevBrothers);
+      expect(IDList).to.have.ordered.members(['1', '3']);
+    }
+  });
+});

+ 396 - 0
tree/tests/tree.ts

@@ -0,0 +1,396 @@
+import { expect } from 'chai';
+import cloneDeep from 'lodash/cloneDeep';
+import { Tree, TreeNode, TreeRaw } from '../src';
+
+function getIDList(nodes: TreeNode[]): string[] {
+  return nodes.map(node => node.ID);
+}
+
+const rawData: TreeRaw[] = [
+  { ID: '1', parentID: '-1', seq: 1 },
+  { ID: '2', parentID: '-1', seq: 3 },
+  { ID: '3', parentID: '-1', seq: 2 },
+  { ID: '4', parentID: '-1', seq: 4 },
+  { ID: '5', parentID: '-1', seq: 5 },
+  { ID: '6', parentID: '1', seq: 2 },
+  { ID: '7', parentID: '1', seq: 1 },
+  { ID: '8', parentID: '7', seq: 1 },
+  { ID: '9', parentID: '2', seq: 1 },
+];
+
+const complicatedRawData: TreeRaw[] = [
+  { ID: '1', parentID: '-1', seq: 1 },
+  { ID: '2', parentID: '-1', seq: 4 },
+  { ID: '3', parentID: '-1', seq: 3 },
+  { ID: '4', parentID: '-1', seq: 5 },
+  { ID: '5', parentID: '-1', seq: 6 },
+  { ID: '6', parentID: '1', seq: 2 },
+  { ID: '7', parentID: '1', seq: 1 },
+  { ID: '8', parentID: '7', seq: 1 },
+  { ID: '9', parentID: '2', seq: 1 },
+  { ID: '10', parentID: '1', seq: 3 },
+  { ID: '11', parentID: '1', seq: 4 },
+  { ID: '12', parentID: '1', seq: 5 },
+  { ID: '13', parentID: '1', seq: 6 },
+  { ID: '14', parentID: '-1', seq: 2 },
+];
+
+describe('Tree', () => {
+  const tree = new Tree(cloneDeep(rawData));
+
+  it('genData', () => {
+    const IDList = tree.data.map(item => item.ID);
+    expect(IDList).to.have.ordered.members([
+      '1',
+      '7',
+      '8',
+      '6',
+      '3',
+      '2',
+      '9',
+      '4',
+      '5',
+    ]);
+  });
+
+  it('find', () => {
+    const node = tree.find('3');
+    expect(node).to.have.property('ID', '3');
+  });
+
+  it('findParent', () => {
+    const node = tree.findParent('6');
+    expect(node).to.have.property('ID', '1');
+  });
+
+  it('findNext', () => {
+    const node = tree.findNext('7');
+    expect(node).to.have.property('ID', '6');
+  });
+
+  it('findPrev', () => {
+    const node = tree.findPrev('6');
+    expect(node).to.have.property('ID', '7');
+  });
+});
+
+describe('Tree change', () => {
+  it('insert', () => {
+    const tree = new Tree(cloneDeep(rawData));
+    const treeRaw: TreeRaw[] = [
+      {
+        ID: '10',
+        parentID: '2',
+        seq: 2,
+      },
+      {
+        ID: '11',
+        parentID: '7',
+        seq: 2,
+      },
+    ];
+    const updateData = tree.prepareInsert(treeRaw);
+    tree.insert(treeRaw, updateData);
+    const IDList = tree.data.map(item => item.ID);
+    expect(IDList).to.have.ordered.members([
+      '1',
+      '7',
+      '8',
+      '11',
+      '6',
+      '3',
+      '2',
+      '9',
+      '10',
+      '4',
+      '5',
+    ]);
+  });
+
+  it('complicated-insert', () => {
+    const tree = new Tree(cloneDeep(complicatedRawData));
+    const treeRaw: TreeRaw[] = [
+      {
+        ID: '15',
+        parentID: '7',
+        seq: 1,
+      },
+      {
+        ID: '16',
+        parentID: '7',
+        seq: 1,
+      },
+      {
+        ID: '17',
+        parentID: '7',
+        seq: 2,
+      },
+      {
+        ID: '18',
+        parentID: '12',
+        seq: 1,
+      },
+      {
+        ID: '19',
+        parentID: '18',
+        seq: 1,
+      },
+    ];
+    const updateData = tree.prepareInsert(treeRaw);
+    tree.insert(treeRaw, updateData);
+    const dataIDList = getIDList(tree.data);
+    expect(dataIDList).to.have.ordered.members([
+      '1',
+      '7',
+      '15',
+      '16',
+      '8',
+      '17',
+      '6',
+      '10',
+      '11',
+      '12',
+      '18',
+      '19',
+      '13',
+      '14',
+      '3',
+      '2',
+      '9',
+      '4',
+      '5',
+    ]);
+    expect(tree.parentMap).to.have.all.keys('-1', '1', '7', '12', '18', '2');
+    const parentD1 = getIDList(tree.parentMap['-1']);
+    expect(parentD1).to.have.ordered.members(['1', '14', '3', '2', '4', '5']);
+    const parent1 = getIDList(tree.parentMap['1']);
+    expect(parent1).to.have.ordered.members(['7', '6', '10', '11', '12', '13']);
+    const parent7 = getIDList(tree.parentMap['7']);
+    expect(parent7).to.have.ordered.members(['15', '16', '8', '17']);
+    const parent12 = getIDList(tree.parentMap['12']);
+    expect(parent12).to.have.ordered.members(['18']);
+    const parent18 = getIDList(tree.parentMap['18']);
+    expect(parent18).to.have.ordered.members(['19']);
+    const parent2 = getIDList(tree.parentMap['2']);
+    expect(parent2).to.have.ordered.members(['9']);
+  });
+
+  it('delete', () => {
+    const tree = new Tree(cloneDeep(rawData));
+    const deleteNodes: TreeNode[] = [tree.find('1') as TreeNode];
+    const updateData = tree.prepareDelete(deleteNodes);
+    tree.delete(deleteNodes, updateData);
+    const IDList = tree.data.map(item => item.ID);
+    expect(IDList).to.have.ordered.members(['3', '2', '9', '4', '5']);
+  });
+  it('upMove', () => {
+    const tree = new Tree(cloneDeep(rawData));
+    const nodes = [tree.find('2'), tree.find('4')];
+    const updateData = tree.prepareUpMove(nodes as TreeNode[]);
+    tree.move(nodes as TreeNode[], updateData);
+    const IDList = getIDList(tree.data);
+    expect(IDList).to.have.ordered.members([
+      '1',
+      '7',
+      '8',
+      '6',
+      '2',
+      '9',
+      '4',
+      '3',
+      '5',
+    ]);
+    expect(nodes[0]).to.have.property('seq', 2);
+    expect(nodes[1]).to.have.property('seq', 3);
+    const node3 = tree.find('3');
+    expect(node3).to.have.property('seq', 4);
+  });
+
+  it('downMove', () => {
+    const tree = new Tree(cloneDeep(rawData));
+    const nodes = [tree.find('3'), tree.find('2')];
+    const updateData = tree.prepareDownMove(nodes as TreeNode[]);
+    tree.move(nodes as TreeNode[], updateData);
+    const IDList = getIDList(tree.data);
+    expect(IDList).to.have.ordered.members([
+      '1',
+      '7',
+      '8',
+      '6',
+      '4',
+      '3',
+      '2',
+      '9',
+      '5',
+    ]);
+    expect(nodes[0]).to.have.property('seq', 3);
+    expect(nodes[1]).to.have.property('seq', 4);
+    const node4 = tree.find('4');
+    expect(node4).to.have.property('seq', 2);
+  });
+
+  it('single-upLevel', () => {
+    const tree = new Tree(cloneDeep(complicatedRawData));
+    const node10 = tree.find('10');
+    if (node10) {
+      const updateData = tree.prepareUpLevel([node10]);
+      tree.upLevel([node10], updateData);
+      const dataIDList = getIDList(tree.data);
+      expect(dataIDList).to.have.ordered.members([
+        '1',
+        '7',
+        '8',
+        '6',
+        '10',
+        '11',
+        '12',
+        '13',
+        '14',
+        '3',
+        '2',
+        '9',
+        '4',
+        '5',
+      ]);
+      expect(tree.parentMap).to.have.all.keys('-1', '1', '7', '10', '2');
+      const parentD1 = getIDList(tree.parentMap['-1']);
+      expect(parentD1).to.have.ordered.members([
+        '1',
+        '10',
+        '14',
+        '3',
+        '2',
+        '4',
+        '5',
+      ]);
+      const parent1 = getIDList(tree.parentMap['1']);
+      expect(parent1).to.have.ordered.members(['7', '6']);
+      const parent7 = getIDList(tree.parentMap['7']);
+      expect(parent7).to.have.ordered.members(['8']);
+      const parent10 = getIDList(tree.parentMap['10']);
+      expect(parent10).to.have.ordered.members(['11', '12', '13']);
+      const parent2 = getIDList(tree.parentMap['2']);
+      expect(parent2).to.have.ordered.members(['9']);
+    }
+  });
+
+  it('multi-upLevel', () => {
+    const tree = new Tree(cloneDeep(complicatedRawData));
+    const node10 = tree.find('10');
+    const node11 = tree.find('11');
+    if (node10 && node11) {
+      const updateData = tree.prepareUpLevel([node10, node11]);
+      tree.upLevel([node10, node11], updateData);
+      const dataIDList = getIDList(tree.data);
+      expect(dataIDList).to.have.ordered.members([
+        '1',
+        '7',
+        '8',
+        '6',
+        '10',
+        '11',
+        '12',
+        '13',
+        '14',
+        '3',
+        '2',
+        '9',
+        '4',
+        '5',
+      ]);
+      expect(tree.parentMap).to.have.all.keys('-1', '1', '7', '11', '2');
+      const parentD1 = getIDList(tree.parentMap['-1']);
+      expect(parentD1).to.have.ordered.members([
+        '1',
+        '10',
+        '11',
+        '14',
+        '3',
+        '2',
+        '4',
+        '5',
+      ]);
+      const parent1 = getIDList(tree.parentMap['1']);
+      expect(parent1).to.have.ordered.members(['7', '6']);
+      const parent7 = getIDList(tree.parentMap['7']);
+      expect(parent7).to.have.ordered.members(['8']);
+      const parent11 = getIDList(tree.parentMap['11']);
+      expect(parent11).to.have.ordered.members(['12', '13']);
+      const parent2 = getIDList(tree.parentMap['2']);
+      expect(parent2).to.have.ordered.members(['9']);
+    }
+  });
+
+  it('single-downLevel', () => {
+    const tree = new Tree(cloneDeep(complicatedRawData));
+    const node10 = tree.find('10');
+    if (node10) {
+      const updateData = tree.prepareDownLevel([node10]);
+      tree.downLevel([node10], updateData);
+      const dataIDList = getIDList(tree.data);
+      expect(dataIDList).to.have.ordered.members([
+        '1',
+        '7',
+        '8',
+        '6',
+        '10',
+        '11',
+        '12',
+        '13',
+        '14',
+        '3',
+        '2',
+        '9',
+        '4',
+        '5',
+      ]);
+      expect(tree.parentMap).to.have.all.keys('-1', '1', '7', '6', '2');
+      const parentD1 = getIDList(tree.parentMap['-1']);
+      expect(parentD1).to.have.ordered.members(['1', '14', '3', '2', '4', '5']);
+      const parent1 = getIDList(tree.parentMap['1']);
+      expect(parent1).to.have.ordered.members(['7', '6', '11', '12', '13']);
+      const parent7 = getIDList(tree.parentMap['7']);
+      expect(parent7).to.have.ordered.members(['8']);
+      const parent11 = getIDList(tree.parentMap['6']);
+      expect(parent11).to.have.ordered.members(['10']);
+      const parent2 = getIDList(tree.parentMap['2']);
+      expect(parent2).to.have.ordered.members(['9']);
+    }
+  });
+
+  it('multi-downLevel', () => {
+    const tree = new Tree(cloneDeep(complicatedRawData));
+    const node6 = tree.find('6');
+    const node10 = tree.find('10');
+    if (node6 && node10) {
+      const updateData = tree.prepareDownLevel([node6, node10]);
+      tree.downLevel([node6, node10], updateData);
+      const dataIDList = getIDList(tree.data);
+      expect(dataIDList).to.have.ordered.members([
+        '1',
+        '7',
+        '8',
+        '6',
+        '10',
+        '11',
+        '12',
+        '13',
+        '14',
+        '3',
+        '2',
+        '9',
+        '4',
+        '5',
+      ]);
+      expect(tree.parentMap).to.have.all.keys('-1', '1', '7', '2');
+      const parentD1 = getIDList(tree.parentMap['-1']);
+      expect(parentD1).to.have.ordered.members(['1', '14', '3', '2', '4', '5']);
+      const parent1 = getIDList(tree.parentMap['1']);
+      expect(parent1).to.have.ordered.members(['7', '11', '12', '13']);
+      const parent7 = getIDList(tree.parentMap['7']);
+      expect(parent7).to.have.ordered.members(['8', '6', '10']);
+      const parent2 = getIDList(tree.parentMap['2']);
+      expect(parent2).to.have.ordered.members(['9']);
+    }
+  });
+});

+ 17 - 0
tree/tsconfig.json

@@ -0,0 +1,17 @@
+{
+  "compilerOptions": {
+    "target": "ESNext",
+    "module": "ESNext",
+    "declaration": true,
+    "outDir": "./",
+    "strict": true,
+    "esModuleInterop": true,
+  },
+  "include": [
+    "src/**/*.ts",
+  ],
+  "exclude": [
+    "node_modules",
+    "test"
+  ]
+}