Sfoglia il codice sorgente

修改树布局算法

chenyitao.0928 5 anni fa
parent
commit
cf6e22ea52
3 ha cambiato i file con 162 aggiunte e 153 eliminazioni
  1. 122 128
      src/components/Tree.vue
  2. 32 17
      src/components/TreeNode.vue
  3. 8 8
      src/views/Project/Schedule.vue

+ 122 - 128
src/components/Tree.vue

@@ -1,15 +1,28 @@
 <template>
 <div id="tree-container" :style="`height:${HEIGHT}px`">
   <div id="node-container" :style="`height:${HEIGHT}px; width:${WIDTH}px`">
-    <TreeNode
-      v-for="requirement in data"
-      :key="requirement.id"
-      v-bind="requirement"
-      :ref="`treenode-${requirement.id}`"
-      @addChild="addChild"
-      @deleteNode="deleteNode"
-      @nodeClick="nodeClick"
-    />
+    <template v-for="(col, index) in treeNodeColList" :key="index">
+      <TreeNode
+        v-for="item in col"
+        v-bind="item"
+        :key="item.id"
+        :position-x="index"
+        :node-style="NODE_STYLE"
+        @addChild="addChild"
+        @deleteNode="deleteNode"
+        @nodeClick="nodeClick"
+        :ref="`treenode-${item.id}`"
+      />
+    </template>
+<!--    <TreeNode-->
+<!--      v-for="requirement in data"-->
+<!--      :key="requirement.id"-->
+<!--      v-bind="requirement"-->
+<!--      :ref="`treenode-${requirement.id}`"-->
+<!--      @addChild="addChild"-->
+<!--      @deleteNode="deleteNode"-->
+<!--      @nodeClick="nodeClick"-->
+<!--    />-->
   </div>
   <canvas id="line-drawer" :height="HEIGHT" :width="WIDTH"></canvas>
 </div>
@@ -23,6 +36,12 @@ const HEIGHT = 600;
 const WIDTH = 1100;
 const MARGIN_TOP = 80;
 const MARGIN_LEFT = 100;
+const TREE_MAX_HEIGHT = 4;
+const NODE_STYLE = {
+  width: 250,
+  height: 100,
+  padding: 25,
+};
 
 export default {
   name: 'Tree',
@@ -30,10 +49,77 @@ export default {
   props: {
     data: Array,
   },
+  computed: {
+    /**
+     * @returns {*[][]}
+     */
+    treeNodeColList() {
+      // 将点存入map便于调用
+      const nodeMap = new Map();
+      this.data.forEach((item) => {
+        nodeMap.set(item.id, item);
+      });
+      // 将array转为树状结构;顺便拆成col
+      let rootNode;
+      const colList = [];
+      for (let i = 0; i < TREE_MAX_HEIGHT; i += 1) {
+        colList.push([]);
+      }
+      this.data.forEach((item) => {
+        if (item.deep > 0 && item.deep <= TREE_MAX_HEIGHT) {
+          colList[item.deep - 1].push(item);
+        } else {
+          console.warn('需求树数据有误,树高度错误');
+        }
+        if (item.parent) {
+          const tempParent = nodeMap.get(item.parent);
+          if (!tempParent.childs) {
+            tempParent.childs = [];
+          }
+          tempParent.childs.push(item);
+        } else {
+          rootNode = item;
+        }
+      });
+      // 计算一个节点包含他的后代所需要的空间
+      const calculateNodeContentHeight = (node) => {
+        if (!node.childs) {
+          node.contentHeight = 1;
+          return 1;
+        }
+        node.contentHeight = node.childs.reduce((total, current) => total + calculateNodeContentHeight(current), 0);
+        return node.contentHeight;
+      };
+      calculateNodeContentHeight(rootNode);
+      // 计算节点基准Y轴位置
+      rootNode.positionY = 0;
+      for (let i = 1; i < TREE_MAX_HEIGHT; i += 1) {
+        let sum = 0;
+        colList[i].forEach((item) => {
+          item.positionY = nodeMap.get(item.parent).positionY + sum;
+          sum += item.contentHeight;
+        });
+      }
+      // 计算节点Y轴偏移位置
+      for (let i = TREE_MAX_HEIGHT - 2; i >= 0; i -= 1) {
+        colList[i].forEach((item) => {
+          if (item.childs) {
+            const { length } = item.childs;
+            item.positionY = item.childs.reduce((total, current) => total + current.positionY / length, 0);
+          }
+        });
+      }
+      // 返回col
+      this.drawLine();
+      return colList;
+    },
+  },
   data() {
     return {
       HEIGHT,
       WIDTH,
+      TREE_MAX_HEIGHT,
+      NODE_STYLE,
       parent2children: {},
       depth2node: {},
       layout: {},
@@ -42,131 +128,38 @@ export default {
   },
   mounted() {
     this.initCanvasCtx();
-    this.regenerateTree();
+    // this.regenerateTree();
   },
   methods: {
     initCanvasCtx() {
       this.ctx = document.getElementById('line-drawer').getContext('2d');
     },
-    regenerateTree() {
-      this.initParent2Children();
-      this.initDepth2Node();
-      this.calculateLayout();
-      this.calculateHeight();
-      this.locateAndDraw();
-    },
-    initParent2Children() {
-      this.parent2children = {};
-      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]);
-      }
-    },
-    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]);
-          }
-        }
-        parentList = this.depth2node[depth].map((node) => node.id);
-      }
-    },
-    // FIXME: 节点在部分情况下会出现重叠,待修复
-    calculateLayout() {
-      // clear the layout
-      this.layout = {};
-      const COL_WIDTH = (WIDTH - MARGIN_LEFT) / 4;
-
-      for (let depth = 4; depth > 0; depth -= 1) {
-        const offsetLeft = MARGIN_LEFT + (COL_WIDTH * (depth * 2 - 1)) / 2;
-        let offsetTop = MARGIN_TOP;
-        for (let i = 0; i < this.depth2node[depth].length; i += 1) {
-          const treenode = this.$refs[`treenode-${this.depth2node[depth][i].id}`];
-          const { width, height } = treenode.getTreeNodeSize();
-          const offsetX = offsetLeft - width / 2;
-          let offsetY;
-          if (depth === 4) {
-            offsetY = offsetTop;
-            offsetTop += height + MARGIN_TOP;
-          } else if (Object.prototype.hasOwnProperty.call(this.parent2children, this.depth2node[depth][i].id)) {
-            const childrenList = this.parent2children[this.depth2node[depth][i].id].map((node) => node.id);
-            offsetY = childrenList.reduce(
-              (sum, curr) => sum + this.layout[curr].y + this.layout[curr].height / 2,
-              0,
-            ) / childrenList.length - height / 2;
-          } else {
-            offsetY = offsetTop;
-          }
-          offsetTop = offsetY + height + MARGIN_TOP;
-          this.layout[this.depth2node[depth][i].id] = {
-            x: offsetX,
-            y: offsetY,
-            width,
-            height,
+    drawLine() {
+      setTimeout(() => {
+        this.ctx.clearRect(0, 0, WIDTH, HEIGHT);
+        this.ctx.lineWidth = 1;
+        this.ctx.strokeStyle = '#bbbbbb';
+        this.data.forEach((item) => {
+          if (!item.childs) return;
+          const parentCard = this.$refs[`treenode-${item.id}`].getTreeNodeSizeAndPosition();
+          console.warn(parentCard);
+          const start = {
+            x: parentCard.left + parentCard.width,
+            y: parentCard.top + parentCard.height / 2,
           };
-        }
-      }
-    },
-    calculateHeight() {
-      const height = Object.values(this.layout).reduce((max, curr) => {
-        const now = curr.y + curr.height + MARGIN_TOP;
-        return now > max ? now : max;
-      }, 0);
-
-      document.getElementById('tree-container').style.height = `${height}px`;
-      document.getElementById('node-container').style.height = `${height}px`;
-      document.getElementById('line-drawer').height = height;
-    },
-    locateAndDraw() {
-      // 定位TreeNodeContainer
-      // eslint-disable-next-line no-restricted-syntax
-      for (const id of Object.keys(this.layout)) {
-        const { x, y } = this.layout[id];
-        this.$refs[`treenode-${id}`].setTreeNodeOffset(x, y);
-      }
-      // clear the canvas
-      this.ctx.clearRect(0, 0, WIDTH, HEIGHT);
-      this.ctx.lineWidth = 1;
-      this.ctx.strokeStyle = '#bbbbbb';
-
-      // 在canvas上绘制节点间连线
-      const kvs = Object.entries(this.parent2children);
-      // eslint-disable-next-line no-restricted-syntax
-      for (const [parent, children] of kvs) {
-        if (parent === 'null') {
-          // eslint-disable-next-line no-continue
-          continue;
-        }
-        const {
-          x: startX, y: startY, width: startW, height: startH,
-        } = this.layout[parent];
-        const start = {
-          x: startX + startW,
-          y: startY + startH / 2,
-        };
-        // eslint-disable-next-line no-restricted-syntax
-        for (const child of children) {
-          const {
-            x: endX, y: endY, height: endH,
-          } = this.layout[child.id];
-          const end = {
-            x: endX,
-            y: endY + endH / 2,
-          };
-          this.ctx.beginPath();
-          this.ctx.moveTo(start.x, start.y);
-          this.ctx.lineTo(end.x, end.y);
-          this.ctx.stroke();
-        }
-      }
+          item.childs.forEach((child) => {
+            const childCard = this.$refs[`treenode-${child.id}`].getTreeNodeSizeAndPosition();
+            const end = {
+              x: childCard.left,
+              y: childCard.top + childCard.height / 2,
+            };
+            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', {
@@ -189,6 +182,7 @@ export default {
 
 <style lang="scss">
 #tree-container {
+  position: relative;
   display: flex;
   justify-content:center;
 }

+ 32 - 17
src/components/TreeNode.vue

@@ -1,6 +1,6 @@
 <template>
-  <div class="tree-node-container" ref="node">
-    <div class="tree-node" @click="nodeClick(id)">
+  <div class="tree-node-container" :style="containerStyle" ref="node">
+    <div class="tree-node" :style="cardStyle"  @click="nodeClick(id)">
       <ElCard class="tree-node-card" shadow="hover">
         <template #header>
           <div class="card-header">
@@ -35,23 +35,34 @@ export default {
     id: Number,
     state: String,
     title: String,
+    positionY: Number,
+    positionX: Number,
+    nodeStyle: Object,
   },
-  methods: {
-    getTagTypeFromState,
-    setTreeNodeOffsetX(x) {
-      this.$refs.node.style.left = `${x}px`;
-    },
-    setTreeNodeOffsetY(y) {
-      this.$refs.node.style.top = `${y}px`;
+  computed: {
+    containerStyle() {
+      return {
+        top: `${this.positionY * 130}px`,
+        left: `${280 * this.positionX}px`,
+        width: `${this.nodeStyle.width + this.nodeStyle.padding * 2}px`,
+        height: `${this.nodeStyle.height + this.nodeStyle.padding * 2}px`,
+      };
     },
-    setTreeNodeOffset(x, y) {
-      this.$refs.node.style.left = `${x}px`;
-      this.$refs.node.style.top = `${y}px`;
+    cardStyle() {
+      return {
+        width: `${this.nodeStyle.width}px`,
+        height: `${this.nodeStyle.height}px`,
+      };
     },
-    getTreeNodeSize() {
+  },
+  methods: {
+    getTagTypeFromState,
+    getTreeNodeSizeAndPosition() {
       return {
-        height: this.$refs.node.offsetHeight,
-        width: this.$refs.node.offsetWidth,
+        top: this.$refs.node.offsetTop + this.nodeStyle.padding,
+        left: this.$refs.node.offsetLeft + this.nodeStyle.padding,
+        height: this.nodeStyle.height,
+        width: this.nodeStyle.width,
       };
     },
     addChild(id) {
@@ -70,11 +81,16 @@ export default {
 <style lang="scss" scoped>
 .tree-node-container {
   position: absolute;
-  width: 20%;
+  box-sizing: border-box;
+  display: flex;
+  justify-content: center;
+  align-items: center;
 
   user-select: none;
   & .node-postfix {
     position: absolute;
+    bottom: 0px;
+    right: 25px;
     width: 100%;
     display: none;
   }
@@ -85,7 +101,6 @@ export default {
 }
 .tree-node {
   box-sizing: border-box;
-  width: 100%;
 }
 .card-header {
   display: flex;

+ 8 - 8
src/views/Project/Schedule.vue

@@ -28,7 +28,7 @@ export default {
         startTime: '2021/3/17 12:00:01',
         state: '已创建',
         timeToTake: 10,
-        title: '这是标题',
+        title: '这是标题1',
         type: '需求',
         userId: 101,
       },
@@ -45,7 +45,7 @@ export default {
         startTime: '2021/3/17 12:00:01',
         state: '已创建',
         timeToTake: 10,
-        title: '这是标题',
+        title: '这是标题2',
         type: '需求',
         userId: 101,
       },
@@ -62,7 +62,7 @@ export default {
         startTime: '2021/3/17 12:00:01',
         state: '进行中',
         timeToTake: 10,
-        title: '这是标题',
+        title: '这是标题3',
         type: '需求',
         userId: 101,
       },
@@ -79,7 +79,7 @@ export default {
         startTime: '2021/3/17 12:00:01',
         state: '已完成',
         timeToTake: 10,
-        title: '这是标题',
+        title: '这是标题4',
         type: '需求',
         userId: 101,
       },
@@ -96,7 +96,7 @@ export default {
         startTime: '2021/3/17 12:00:01',
         state: '已完成',
         timeToTake: 10,
-        title: '这是标题',
+        title: '这是标题5',
         type: '需求',
         userId: 101,
       },
@@ -113,7 +113,7 @@ export default {
         startTime: '2021/3/17 12:00:01',
         state: '进行中',
         timeToTake: 10,
-        title: '这是标题',
+        title: '这是标题6',
         type: '需求',
         userId: 101,
       },
@@ -130,7 +130,7 @@ export default {
         startTime: '2021/3/17 12:00:01',
         state: '已完成',
         timeToTake: 10,
-        title: '这是标题',
+        title: '这是标题7',
         type: '需求',
         userId: 101,
       },
@@ -147,7 +147,7 @@ export default {
         startTime: '2021/3/17 12:00:01',
         state: '已完成',
         timeToTake: 10,
-        title: '这是标题',
+        title: '这是标题8',
         type: '需求',
         userId: 101,
       },