| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288 |
- <template>
- <div id="tree-container" :style="`height:${HEIGHT}px`">
- <div id="node-container" :style="`height:${HEIGHT}px; width:${WIDTH}px`">
- <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>
- </template>
- <script>
- import TreeNode from './TreeNode.vue';
- // 容器宽高
- const HEIGHT = 700;
- const WIDTH = 1000;
- const TREE_MAX_HEIGHT = 4;
- const NODE_STYLE = {
- width: 250,
- height: 100,
- padding: 25,
- };
- export default {
- name: 'Tree',
- components: { TreeNode },
- 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([]);
- }
- if (this.data.length === 0) {
- return colList;
- }
- this.data.forEach((item) => {
- if (item.deep > 0 && item.deep <= TREE_MAX_HEIGHT) {
- colList[item.deep - 1].push(item);
- } else {
- console.warn('需求树数据有误,树高度错误');
- }
- if (!item.parentId) rootNode = item;
- // 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.parentId).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);
- }
- });
- }
- // 计算所需容器高度
- let maxPositionY = 0;
- this.data.forEach((item) => {
- if (item.positionY && item.positionY > maxPositionY) maxPositionY = item.positionY;
- });
- this.drawLine((maxPositionY * 130 + 100).toFixed(0));
- // 返回col
- return colList;
- },
- },
- data() {
- return {
- HEIGHT,
- WIDTH,
- TREE_MAX_HEIGHT,
- NODE_STYLE,
- ctx: null,
- };
- },
- mounted() {
- this.initCanvasCtx();
- // this.regenerateTree();
- },
- methods: {
- initCanvasCtx() {
- this.ctx = document.getElementById('line-drawer').getContext('2d');
- },
- drawLine(height) {
- setTimeout(() => {
- this.ctx.clearRect(0, 0, WIDTH, height > HEIGHT ? height : 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();
- const start = {
- x: parentCard.left + parentCard.width,
- y: parentCard.top + parentCard.height / 2,
- };
- 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', {
- type: 'addChildNode',
- id,
- });
- },
- deleteNode(id) {
- this.$confirm('是否确认删除该节点?', '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning',
- }).then(() => {
- this.$emit('change', {
- type: 'deleteTreeNode',
- id,
- });
- }).catch(() => {
- this.$message({
- type: 'info',
- message: '已取消删除',
- });
- });
- },
- nodeClick(id) {
- this.$emit('node-click', id);
- },
- },
- };
- </script>
- <style lang="scss">
- #tree-container {
- border-radius: 4px;
- box-shadow: 0 0 3px 0px #BDC3C7;
- position: relative;
- display: flex;
- overflow-y: auto;
- justify-content:center;
- background: url("https://devcloud.cn-north-4.huaweicloud.com/projectman/plan_back.05189e7a664e6edc2f8e.png") repeat;
- }
- #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>
|