|
|
@@ -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;
|
|
|
}
|