|
@@ -0,0 +1,231 @@
|
|
|
|
|
+<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}`"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <canvas id="line-drawer" :height="HEIGHT" :width="WIDTH"></canvas>
|
|
|
|
|
+</div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+import TreeNode from './TreeNode.vue';
|
|
|
|
|
+
|
|
|
|
|
+// 容器宽高
|
|
|
|
|
+const HEIGHT = 600;
|
|
|
|
|
+const WIDTH = 1100;
|
|
|
|
|
+
|
|
|
|
|
+export default {
|
|
|
|
|
+ name: 'Tree',
|
|
|
|
|
+ components: { TreeNode },
|
|
|
|
|
+ props: {
|
|
|
|
|
+ data: Array,
|
|
|
|
|
+ },
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ HEIGHT,
|
|
|
|
|
+ WIDTH,
|
|
|
|
|
+ parent2children: {},
|
|
|
|
|
+ depth2node: {},
|
|
|
|
|
+ layout: {},
|
|
|
|
|
+ };
|
|
|
|
|
+ },
|
|
|
|
|
+ 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) {
|
|
|
|
|
+ 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] = [];
|
|
|
|
|
+ }
|
|
|
|
|
+ this.depth2node[depth].push(this.data[i]);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ parentList = this.depth2node[depth].map((node) => node.id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.calculateLayout();
|
|
|
|
|
+ this.locateAndDraw();
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ calculateLayout() {
|
|
|
|
|
+ const MARGIN_TOP = 40;
|
|
|
|
|
+ const MARGIN_LEFT = 100;
|
|
|
|
|
+ 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,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ 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);
|
|
|
|
|
+ }
|
|
|
|
|
+ // 在canvas上绘制节点间连线
|
|
|
|
|
+ const ctx = document.getElementById('line-drawer').getContext('2d');
|
|
|
|
|
+ ctx.clearRect(0, 0, WIDTH, HEIGHT);
|
|
|
|
|
+
|
|
|
|
|
+ ctx.lineWidth = 1;
|
|
|
|
|
+ ctx.strokeStyle = '#bbbbbb';
|
|
|
|
|
+
|
|
|
|
|
+ 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,
|
|
|
|
|
+ };
|
|
|
|
|
+ ctx.moveTo(start.x, start.y);
|
|
|
|
|
+ ctx.lineTo(end.x, end.y);
|
|
|
|
|
+ ctx.stroke();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+};
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss">
|
|
|
|
|
+#tree-container {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content:center;
|
|
|
|
|
+}
|
|
|
|
|
+#node-container {
|
|
|
|
|
+ position:absolute;
|
|
|
|
|
+ z-index: 101;
|
|
|
|
|
+}
|
|
|
|
|
+#line-drawer {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+}
|
|
|
|
|
+.jsmind-inner {
|
|
|
|
|
+ position:relative;
|
|
|
|
|
+ overflow:auto;
|
|
|
|
|
+ width:100%;
|
|
|
|
|
+ height:100%;
|
|
|
|
|
+ user-select:none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+canvas {
|
|
|
|
|
+ position:absolute;
|
|
|
|
|
+ z-index:1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+jmnodes {
|
|
|
|
|
+ position:absolute;
|
|
|
|
|
+ z-index:2;
|
|
|
|
|
+ background-color:rgba(0,0,0,0);
|
|
|
|
|
+}
|
|
|
|
|
+jmnode {
|
|
|
|
|
+ position:absolute;
|
|
|
|
|
+ cursor:default;
|
|
|
|
|
+ max-width:400px;
|
|
|
|
|
+ white-space:nowrap;
|
|
|
|
|
+ overflow:hidden;
|
|
|
|
|
+ text-overflow:ellipsis;
|
|
|
|
|
+
|
|
|
|
|
+ padding:10px;
|
|
|
|
|
+ background-color:#fff;
|
|
|
|
|
+ color:#333;
|
|
|
|
|
+ border-radius:1px;
|
|
|
|
|
+ border: 1px solid #333;
|
|
|
|
|
+ font:16px/1.125 Verdana,Arial,Helvetica,sans-serif;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ box-shadow:2px 2px 4px #333;
|
|
|
|
|
+ background-color:#ebebeb;
|
|
|
|
|
+ color:#333;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.selected {
|
|
|
|
|
+ background-color:#11f;
|
|
|
|
|
+ color:#fff;
|
|
|
|
|
+ box-shadow:2px 2px 8px #000;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.root {
|
|
|
|
|
+ font-size:24px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+jmexpander {
|
|
|
|
|
+ position:absolute;
|
|
|
|
|
+ width:11px;
|
|
|
|
|
+ height:11px;
|
|
|
|
|
+ display:block;
|
|
|
|
|
+ overflow:hidden;
|
|
|
|
|
+ line-height:12px;
|
|
|
|
|
+ font-size:12px;
|
|
|
|
|
+ text-align:center;
|
|
|
|
|
+ border-radius:6px;
|
|
|
|
|
+ border-width:1px;
|
|
|
|
|
+ border-style:solid;
|
|
|
|
|
+ cursor:pointer;
|
|
|
|
|
+
|
|
|
|
|
+ border-color:gray;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ border-color:#000;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@media screen and (max-device-width: 1024px) {
|
|
|
|
|
+ jmnode{padding:5px;border-radius:3px;font-size:14px;}
|
|
|
|
|
+ jmnode.root{font-size:21px;}
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|