Просмотр исходного кода

feat: 动态计算并改变树容器的高度,防止超出画布

Jinyao 5 лет назад
Родитель
Сommit
954b9a6af7
6 измененных файлов с 359 добавлено и 57 удалено
  1. 1 0
      mock/index.js
  2. 167 0
      mock/routers/tree.js
  3. 16 5
      src/components/Tree.vue
  4. 25 9
      src/components/TreeNode.vue
  5. 2 0
      src/element-plus.ts
  6. 148 43
      src/views/Project/Schedule.vue

+ 1 - 0
mock/index.js

@@ -24,6 +24,7 @@ app.use(`${prefix}/users`, require('./routers/user'));
 app.use(`${prefix}/project`, require('./routers/project'));
 app.use(`${prefix}/pipelines`, require('./routers/pipeline'));
 app.use(`${prefix}/deployments`, require('./routers/deployment'));
+app.use(`${prefix}/tree`, require('./routers/tree'));
 
 app.listen(4000);
 

+ 167 - 0
mock/routers/tree.js

@@ -0,0 +1,167 @@
+/* eslint-disable import/no-extraneous-dependencies */
+/* eslint-disable @typescript-eslint/no-var-requires */
+const router = require('express').Router();
+const Mock = require('mockjs');
+const wrapper = require('../wrapper');
+
+const { Random } = Mock;
+
+router.post('/node', (req, res) => {
+  res.json(wrapper('success'));
+});
+
+router.put('/node', (req, res) => {
+  res.json(wrapper('success'));
+});
+
+router.delete('/node/:nodeId', (req, res) => {
+  res.json(wrapper('success'));
+});
+
+router.post('/subNode/:parentId', (req, res) => {
+  res.json(wrapper('success'));
+});
+
+router.get('/:projectId', (req, res) => {
+  res.json(wrapper(
+    Mock.mock([{
+      deep: 1,
+      description: Random.csentence(10, 40),
+      endTime: Random.datetime(),
+      groupId: 101,
+      id: 1,
+      leaf: false,
+      parent: null,
+      priority: 0,
+      projectId: 101,
+      startTime: Random.datetime(),
+      'state|1': ['已创建', '进行中', '已完成'],
+      timeToTake: 10,
+      title: Random.cname(),
+      'type|1': ['需求', '任务'],
+      userId: 101,
+    },
+    {
+      deep: 2,
+      description: Random.csentence(10, 40),
+      endTime: Random.datetime(),
+      groupId: 101,
+      id: 2,
+      leaf: false,
+      parent: 1,
+      priority: 0,
+      projectId: 101,
+      startTime: Random.datetime(),
+      'state|1': ['已创建', '进行中', '已完成'],
+      timeToTake: 10,
+      title: Random.cname(),
+      'type|1': ['需求', '任务'],
+      userId: 101,
+    },
+    {
+      deep: 2,
+      description: Random.csentence(10, 40),
+      endTime: Random.datetime(),
+      groupId: 101,
+      id: 3,
+      leaf: false,
+      parent: 1,
+      priority: 0,
+      projectId: 101,
+      startTime: Random.datetime(),
+      'state|1': ['已创建', '进行中', '已完成'],
+      timeToTake: 10,
+      title: Random.cname(),
+      'type|1': ['需求', '任务'],
+      userId: 101,
+    },
+    {
+      deep: 3,
+      description: Random.csentence(10, 40),
+      endTime: Random.datetime(),
+      groupId: 101,
+      id: 4,
+      leaf: false,
+      parent: 3,
+      priority: 1,
+      projectId: 101,
+      startTime: Random.datetime(),
+      'state|1': ['已创建', '进行中', '已完成'],
+      timeToTake: 10,
+      title: Random.cname(),
+      'type|1': ['需求', '任务'],
+      userId: 101,
+    },
+    {
+      deep: 3,
+      description: Random.csentence(10, 40),
+      endTime: Random.datetime(),
+      groupId: 101,
+      id: 5,
+      leaf: false,
+      parent: 3,
+      priority: 2,
+      projectId: 101,
+      startTime: Random.datetime(),
+      'state|1': ['已创建', '进行中', '已完成'],
+      timeToTake: 10,
+      title: Random.cname(),
+      'type|1': ['需求', '任务'],
+      userId: 101,
+    },
+    {
+      deep: 4,
+      description: Random.csentence(10, 40),
+      endTime: Random.datetime(),
+      groupId: 101,
+      id: 6,
+      leaf: false,
+      parent: 5,
+      priority: 0,
+      projectId: 101,
+      startTime: Random.datetime(),
+      'state|1': ['已创建', '进行中', '已完成'],
+      timeToTake: 10,
+      title: Random.cname(),
+      'type|1': ['需求', '任务'],
+      userId: 101,
+    },
+    {
+      deep: 4,
+      description: Random.csentence(10, 40),
+      endTime: Random.datetime(),
+      groupId: 101,
+      id: 7,
+      leaf: false,
+      parent: 5,
+      priority: 0,
+      projectId: 101,
+      startTime: Random.datetime(),
+      'state|1': ['已创建', '进行中', '已完成'],
+      timeToTake: 10,
+      title: Random.cname(),
+      'type|1': ['需求', '任务'],
+      userId: 101,
+    },
+    {
+      deep: 4,
+      description: Random.csentence(10, 40),
+      endTime: Random.datetime(),
+      groupId: 101,
+      id: 8,
+      leaf: false,
+      parent: 5,
+      priority: 0,
+      projectId: 101,
+      startTime: Random.datetime(),
+      'state|1': ['已创建', '进行中', '已完成'],
+      timeToTake: 10,
+      title: Random.cname(),
+      'type|1': ['需求', '任务'],
+      userId: 101,
+    },
+    ]),
+  ));
+});
+
+module.exports = router;

+ 16 - 5
src/components/Tree.vue

@@ -21,6 +21,8 @@ import TreeNode from './TreeNode.vue';
 // 容器宽高
 const HEIGHT = 600;
 const WIDTH = 1100;
+const MARGIN_TOP = 80;
+const MARGIN_LEFT = 100;
 
 export default {
   name: 'Tree',
@@ -45,14 +47,12 @@ export default {
   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.calculateHeight();
       this.locateAndDraw();
     },
     initParent2Children() {
@@ -79,11 +79,10 @@ export default {
         parentList = this.depth2node[depth].map((node) => node.id);
       }
     },
+    // FIXME: 节点在部分情况下会出现重叠,待修复
     calculateLayout() {
       // clear the layout
       this.layout = {};
-      const MARGIN_TOP = 40;
-      const MARGIN_LEFT = 100;
       const COL_WIDTH = (WIDTH - MARGIN_LEFT) / 4;
 
       for (let depth = 4; depth > 0; depth -= 1) {
@@ -116,6 +115,16 @@ export default {
         }
       }
     },
+    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
@@ -125,6 +134,8 @@ export default {
       }
       // 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);

+ 25 - 9
src/components/TreeNode.vue

@@ -1,8 +1,14 @@
 <template>
   <div class="tree-node-container" ref="node">
     <div class="tree-node" @click="nodeClick(id)">
-      <div class="node-state">{{state}}</div>
-      <div class="node-title">{{title}}</div>
+      <ElCard class="tree-node-card" shadow="hover">
+        <template #header>
+          <div class="card-header">
+            <span class="node-title">{{title}}</span>
+            <ElTag class="node-state" :type="getTagTypeFromState(state)">{{state}}</ElTag>
+          </div>
+        </template>
+      </ElCard>
     </div>
     <div class="node-postfix">
       <div class="icon" target="delete" @click="deleteNode(id)"></div>
@@ -12,6 +18,17 @@
 </template>
 
 <script>
+const getTagTypeFromState = (state) => {
+  if (state === '已创建') {
+    return '';
+  } if (state === '进行中') {
+    return 'success';
+  } if (state === '已完成') {
+    return 'info';
+  }
+  return 'danger';
+};
+
 export default {
   name: 'TreeNode',
   props: {
@@ -20,6 +37,7 @@ export default {
     title: String,
   },
   methods: {
+    getTagTypeFromState,
     setTreeNodeOffsetX(x) {
       this.$refs.node.style.left = `${x}px`;
     },
@@ -52,15 +70,12 @@ export default {
 <style lang="scss" scoped>
 .tree-node-container {
   position: absolute;
-  height: 140px;
   width: 20%;
 
   user-select: none;
   & .node-postfix {
     position: absolute;
-    top: 120px;
     width: 100%;
-    height: 20px;
     display: none;
   }
 
@@ -70,11 +85,12 @@ export default {
 }
 .tree-node {
   box-sizing: border-box;
-  border-radius: 2px;
-  border: 1px solid #3f3f3f;
-  height: 120px;
   width: 100%;
 }
+.card-header {
+  display: flex;
+  justify-content: space-between;
+}
 .icon {
   width: 14px;
   height: 14px;
@@ -82,7 +98,7 @@ export default {
   background-position: center;
   background-repeat: no-repeat;
   background-size: 100% 100%;
-  margin-top: 4px;
+  margin-top: 10px;
   margin-right: 10px;
   transition: all .2s linear;
 }

+ 2 - 0
src/element-plus.ts

@@ -5,6 +5,7 @@ import {
   ElCarouselItem,
   ElContainer,
   ElDivider,
+  ElDrawer,
   ElHeader,
   ElLink,
   ElMain,
@@ -36,6 +37,7 @@ export const Components = [
   ElMain,
   ElMenu,
   ElDialog,
+  ElDrawer,
   ElLink,
   ElMenuItem,
   ElCarousel,

+ 148 - 43
src/views/Project/Schedule.vue

@@ -1,69 +1,174 @@
 <template>
   <Tree :data="requirements" @change="handleTreeChange" @nodeClick="handleNodeClick" ref="tree"></Tree>
-  <RightForm ref="rightForm"/>
+  <ElDrawer v-model="showRequirementDrawer">
+    <!-- TODO: 供展示和编辑节点详情,可在taskList中复用  -->
+  </ElDrawer>
 </template>
 
 <script>
 import Tree from '@/components/Tree.vue';
-import RightForm from '@/components/RightForm.vue';
 
 import { RequirementAPI } from '@/api';
 
 export default {
   name: 'Schedule',
-  components: { Tree, RightForm },
+  components: { Tree },
   data() {
     return {
-      requirements: [
-        {
-          id: 1,
-          title: 't1',
-          parent: null,
-          state: '未开始',
-        },
-        {
-          id: 2,
-          title: 't2',
-          parent: 1,
-          state: '未开始',
-        },
-        {
-          id: 3,
-          title: 't3',
-          parent: 1,
-          state: '未开始',
-        },
-        {
-          id: 4,
-          title: 't4',
-          parent: 2,
-          state: '未开始',
-        },
-        {
-          id: 5,
-          title: 't5',
-          parent: 2,
-          state: '进行中',
-        },
-        {
-          id: 6,
-          title: 't6',
-          parent: 4,
-          state: '已完成',
-        },
+      requirements: [{
+        deep: 1,
+        description: '这里是节点描述啊啊啊。',
+        endTime: '2021/3/17 12:00:01',
+        groupId: 101,
+        id: 1,
+        leaf: false,
+        parent: null,
+        priority: 0,
+        projectId: 101,
+        startTime: '2021/3/17 12:00:01',
+        state: '已创建',
+        timeToTake: 10,
+        title: '这是标题',
+        type: '需求',
+        userId: 101,
+      },
+      {
+        deep: 2,
+        description: '这里是节点描述啊啊啊。',
+        endTime: '2021/3/17 12:00:01',
+        groupId: 101,
+        id: 2,
+        leaf: false,
+        parent: 1,
+        priority: 0,
+        projectId: 101,
+        startTime: '2021/3/17 12:00:01',
+        state: '已创建',
+        timeToTake: 10,
+        title: '这是标题',
+        type: '需求',
+        userId: 101,
+      },
+      {
+        deep: 2,
+        description: '这里是节点描述啊啊啊。',
+        endTime: '2021/3/17 12:00:01',
+        groupId: 101,
+        id: 3,
+        leaf: false,
+        parent: 1,
+        priority: 0,
+        projectId: 101,
+        startTime: '2021/3/17 12:00:01',
+        state: '进行中',
+        timeToTake: 10,
+        title: '这是标题',
+        type: '需求',
+        userId: 101,
+      },
+      {
+        deep: 3,
+        description: '这里是节点描述啊啊啊。',
+        endTime: '2021/3/17 12:00:01',
+        groupId: 101,
+        id: 4,
+        leaf: false,
+        parent: 3,
+        priority: 1,
+        projectId: 101,
+        startTime: '2021/3/17 12:00:01',
+        state: '已完成',
+        timeToTake: 10,
+        title: '这是标题',
+        type: '需求',
+        userId: 101,
+      },
+      {
+        deep: 3,
+        description: '这里是节点描述啊啊啊。',
+        endTime: '2021/3/17 12:00:01',
+        groupId: 101,
+        id: 5,
+        leaf: false,
+        parent: 3,
+        priority: 2,
+        projectId: 101,
+        startTime: '2021/3/17 12:00:01',
+        state: '已完成',
+        timeToTake: 10,
+        title: '这是标题',
+        type: '需求',
+        userId: 101,
+      },
+      {
+        deep: 4,
+        description: '这里是节点描述啊啊啊。',
+        endTime: '2021/3/17 12:00:01',
+        groupId: 101,
+        id: 6,
+        leaf: false,
+        parent: 5,
+        priority: 0,
+        projectId: 101,
+        startTime: '2021/3/17 12:00:01',
+        state: '进行中',
+        timeToTake: 10,
+        title: '这是标题',
+        type: '需求',
+        userId: 101,
+      },
+      {
+        deep: 4,
+        description: '这里是节点描述啊啊啊。',
+        endTime: '2021/3/17 12:00:01',
+        groupId: 101,
+        id: 7,
+        leaf: false,
+        parent: 5,
+        priority: 0,
+        projectId: 101,
+        startTime: '2021/3/17 12:00:01',
+        state: '已完成',
+        timeToTake: 10,
+        title: '这是标题',
+        type: '需求',
+        userId: 101,
+      },
+      {
+        deep: 4,
+        description: '这里是节点描述啊啊啊。',
+        endTime: '2021/3/17 12:00:01',
+        groupId: 101,
+        id: 8,
+        leaf: false,
+        parent: 5,
+        priority: 0,
+        projectId: 101,
+        startTime: '2021/3/17 12:00:01',
+        state: '已完成',
+        timeToTake: 10,
+        title: '这是标题',
+        type: '需求',
+        userId: 101,
+      },
       ],
+      showRequirementDrawer: false,
     };
   },
+  async mounted() {
+    // this.requirements = await RequirementAPI.getTreeNodesByProjectId(this.$store.state.workspace.currentProjectId);
+    // this.$refs.tree.regenerateTree();
+  },
   methods: {
     async handleTreeChange(msg) {
-      // TODO: 补充API定义
       const { type, id } = msg;
       await RequirementAPI[type](id);
-      // this.requirements = await RequirementAPI.getTreeNode(projectId);
+      // this.requirements = await RequirementAPI.getTreeNodesByProjectId(this.$store.state.workspace.currentProjectId);
       this.$refs.tree.regenerateTree();
     },
     handleNodeClick(id) {
-      this.$refs.rightForm.showForm();
+      this.showRequirementDrawer = true;
+      this.requirementInDrawer = Array.prototype.find.call(this.requirements, (requirement) => requirement.id === id);
     },
   },
 };