| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- <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"
- />
- </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: {},
- ctx: null,
- };
- },
- mounted() {
- 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) {
- 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);
- }
- },
- 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) {
- 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);
- }
- // 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) {
- 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();
- }
- }
- },
- addChild(id) {
- this.$emit('change', {
- type: 'addChild',
- id,
- });
- },
- deleteNode(id) {
- this.$emit('change', {
- type: 'deleteNode',
- id,
- });
- },
- nodeClick(id) {
- this.$emit('node-click', id);
- },
- },
- };
- </script>
- <style lang="scss">
- #tree-container {
- display: flex;
- justify-content:center;
- }
- #node-container {
- position:absolute;
- z-index: 11;
- }
- #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>
|