Преглед изворни кода

feat: 完成树的节点的添加与删除逻辑,完善树节点与连线的渲染逻辑,修复了一个可能导致引用不存在的节点的bug

Jinyao пре 5 година
родитељ
комит
c99e3a42f1

+ 4 - 1
src/api/index.ts

@@ -1,6 +1,9 @@
 import * as UserAPI from './user';
 import * as ProjectAPI from './project';
+import * as RequirementAPI from './requirement';
 
 export {
-  UserAPI, ProjectAPI,
+  UserAPI,
+  ProjectAPI,
+  RequirementAPI,
 };

+ 7 - 0
src/api/requirement.ts

@@ -0,0 +1,7 @@
+import axios from '@/utils/axios';
+
+export const addChild = (parentId: number) => axios.post('/requirement', {
+  parentId,
+});
+
+export const deleteNode = (id: number) => axios.delete(`/requirement/${id}`);

+ 1 - 0
src/assets/icon/add.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1615360924760" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1836" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M896 544H128c-17.6 0-32-14.4-32-32s14.4-32 32-32h768c17.6 0 32 14.4 32 32s-14.4 32-32 32z" p-id="1837"></path><path d="M512 928c-17.6 0-32-14.4-32-32V128c0-17.6 14.4-32 32-32s32 14.4 32 32v768c0 17.6-14.4 32-32 32z" p-id="1838"></path></svg>

+ 1 - 0
src/assets/icon/delete.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1615360913868" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2268" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M768 960H256c-52.8 0-96-43.2-96-96V288c0-17.6 14.4-32 32-32s32 14.4 32 32v576c0 17.6 14.4 32 32 32h512c17.6 0 32-14.4 32-32V288c0-17.6 14.4-32 32-32s32 14.4 32 32v576c0 52.8-43.2 96-96 96zM896 192H672c-8 0-16-3.2-22.4-9.6l-44.8-44.8c-6.4-6.4-14.4-9.6-24-9.6h-139.2c-8 0-16 3.2-22.4 9.6l-44.8 44.8c-6.4 6.4-14.4 9.6-22.4 9.6H128c-17.6 0-32-14.4-32-32s14.4-32 32-32h211.2l35.2-35.2C392 73.6 417.6 64 443.2 64h139.2c25.6 0 49.6 9.6 67.2 28.8L684.8 128H896c17.6 0 32 14.4 32 32s-14.4 32-32 32z" p-id="2269"></path><path d="M400 832c-17.6 0-32-14.4-32-32V288c0-17.6 14.4-32 32-32s32 14.4 32 32v512c0 17.6-14.4 32-32 32zM624 832c-17.6 0-32-14.4-32-32V288c0-17.6 14.4-32 32-32s32 14.4 32 32v512c0 17.6-14.4 32-32 32z" p-id="2270"></path></svg>

+ 60 - 30
src/components/Tree.vue

@@ -6,6 +6,8 @@
       :key="requirement.id"
       v-bind="requirement"
       :ref="`treenode-${requirement.id}`"
+      @addChild="addChild"
+      @deleteNode="deleteNode"
     />
   </div>
   <canvas id="line-drawer" :height="HEIGHT" :width="WIDTH"></canvas>
@@ -32,35 +34,53 @@ export default {
       parent2children: {},
       depth2node: {},
       layout: {},
+      ctx: null,
     };
   },
   mounted() {
-    for (let i = 0; i < this.data.length; i += 1) {
-      const parentid = this.data[i].parent;
-      if (!Object.prototype.hasOwnProperty.call(this.parent2children, parentid)) {
-        this.parent2children[parentid] = [];
-      }
-      this.parent2children[parentid].push(this.data[i]);
-    }
-    let depth = 1;
-    let parentList = [null];
-    for (;depth < 5; depth += 1) {
+    this.initCanvasCtx();
+    this.regenerateTree();
+  },
+  methods: {
+    initCanvasCtx() {
+      this.ctx = document.getElementById('line-drawer').getContext('2d');
+
+      this.ctx.lineWidth = 1;
+      this.ctx.strokeStyle = '#bbbbbb';
+    },
+    regenerateTree() {
+      this.initParent2Children();
+      this.initDepth2Node();
+      this.calculateLayout();
+      this.locateAndDraw();
+    },
+    initParent2Children() {
+      this.parent2children = {};
       for (let i = 0; i < this.data.length; i += 1) {
-        if (parentList.includes(this.data[i].parent)) {
-          if (!Object.prototype.hasOwnProperty.call(this.depth2node, depth)) {
-            this.depth2node[depth] = [];
+        const parentid = this.data[i].parent;
+        if (!Object.prototype.hasOwnProperty.call(this.parent2children, parentid)) {
+          this.parent2children[parentid] = [];
+        }
+        this.parent2children[parentid].push(this.data[i]);
+      }
+    },
+    initDepth2Node() {
+      this.depth2node = {};
+      let depth = 1;
+      let parentList = [null];
+      for (;depth < 5; depth += 1) {
+        this.depth2node[depth] = [];
+        for (let i = 0; i < this.data.length; i += 1) {
+          if (parentList.includes(this.data[i].parent)) {
+            this.depth2node[depth].push(this.data[i]);
           }
-          this.depth2node[depth].push(this.data[i]);
         }
+        parentList = this.depth2node[depth].map((node) => node.id);
       }
-      parentList = this.depth2node[depth].map((node) => node.id);
-    }
-
-    this.calculateLayout();
-    this.locateAndDraw();
-  },
-  methods: {
+    },
     calculateLayout() {
+      // clear the layout
+      this.layout = {};
       const MARGIN_TOP = 40;
       const MARGIN_LEFT = 100;
       const COL_WIDTH = (WIDTH - MARGIN_LEFT) / 4;
@@ -102,13 +122,10 @@ export default {
         const { x, y } = this.layout[id];
         this.$refs[`treenode-${id}`].setTreeNodeOffset(x, y);
       }
-      // 在canvas上绘制节点间连线
-      const ctx = document.getElementById('line-drawer').getContext('2d');
-      ctx.clearRect(0, 0, WIDTH, HEIGHT);
-
-      ctx.lineWidth = 1;
-      ctx.strokeStyle = '#bbbbbb';
+      // clear the canvas
+      this.ctx.clearRect(0, 0, WIDTH, HEIGHT);
 
+      // 在canvas上绘制节点间连线
       const kvs = Object.entries(this.parent2children);
       // eslint-disable-next-line no-restricted-syntax
       for (const [parent, children] of kvs) {
@@ -132,12 +149,25 @@ export default {
             x: endX,
             y: endY + endH / 2,
           };
-          ctx.moveTo(start.x, start.y);
-          ctx.lineTo(end.x, end.y);
-          ctx.stroke();
+          this.ctx.beginPath();
+          this.ctx.moveTo(start.x, start.y);
+          this.ctx.lineTo(end.x, end.y);
+          this.ctx.stroke();
         }
       }
     },
+    addChild(id) {
+      this.$emit('change', {
+        type: 'addChild',
+        id,
+      });
+    },
+    deleteNode(id) {
+      this.$emit('change', {
+        type: 'deleteNode',
+        id,
+      });
+    },
   },
 };
 </script>

+ 34 - 4
src/components/TreeNode.vue

@@ -5,7 +5,8 @@
       <div class="node-title">{{title}}</div>
     </div>
     <div class="node-postfix">
-      +
+      <div class="icon" target="delete" @click="deleteNode(id)"></div>
+      <div class="icon" target="add" @click="addChild(id)"></div>
     </div>
   </div>
 </template>
@@ -35,6 +36,12 @@ export default {
         width: this.$refs.node.offsetWidth,
       };
     },
+    addChild(id) {
+      this.$emit('add-child', id);
+    },
+    deleteNode(id) {
+      this.$emit('delete-node', id);
+    },
     openDrawer(id) {
       // TODO: 打开详细信息抽屉
     },
@@ -51,8 +58,9 @@ export default {
   user-select: none;
   & .node-postfix {
     position: absolute;
-    left: 160px;
-    height: 120px;
+    top: 120px;
+    width: 100%;
+    height: 20px;
     display: none;
   }
 
@@ -67,5 +75,27 @@ export default {
   height: 120px;
   width: 100%;
 }
-
+.icon {
+  width: 14px;
+  height: 14px;
+  float: right;
+  background-position: center;
+  background-repeat: no-repeat;
+  background-size: 100% 100%;
+  margin-top: 4px;
+  margin-right: 10px;
+  transition: all .2s linear;
+}
+.icon:hover{
+  cursor: pointer;
+  transition-duration: 0.2s;
+  transform: scale(1.1);
+  transition-timing-function:ease-in-out;
+}
+.icon[target=add]{
+  background-image: url("../assets/icon/add.svg");
+}
+.icon[target=delete]{
+  background-image: url("../assets/icon/delete.svg");
+}
 </style>

+ 9 - 4
src/views/Project/Schedule.vue

@@ -1,11 +1,12 @@
 <template>
-  <Tree :data="requirements" @change="handleTreeChange"></Tree>
-  <button @click="submit">提交</button>
+  <Tree :data="requirements" @change="handleTreeChange" ref="tree"></Tree>
 </template>
 
 <script>
 import Tree from '@/components/Tree.vue';
 
+import RequirementAPI from '@/api';
+
 export default {
   name: 'Schedule',
   components: { Tree },
@@ -52,8 +53,12 @@ export default {
     };
   },
   methods: {
-    submit() {
-      // TODO: 提交
+    handleTreeChange(msg) {
+      // TODO: 补充API定义
+      const { type, id } = msg;
+      await RequirementAPI[type](id);
+      // this.requirements = await RequirementAPI.getTreeNode(projectId);
+      this.$refs.tree.regenerateTree();
     },
   },
 };