Tree.vue 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. <template>
  2. <div id="tree-container" :style="`height:${HEIGHT}px`">
  3. <div id="node-container" :style="`height:${HEIGHT}px; width:${WIDTH}px`">
  4. <template v-for="(col, index) in treeNodeColList" :key="index">
  5. <TreeNode
  6. v-for="item in col"
  7. v-bind="item"
  8. :key="item.id"
  9. :position-x="index"
  10. :node-style="NODE_STYLE"
  11. @addChild="addChild"
  12. @deleteNode="deleteNode"
  13. @nodeClick="nodeClick"
  14. :ref="`treenode-${item.id}`"
  15. />
  16. </template>
  17. <!-- <TreeNode-->
  18. <!-- v-for="requirement in data"-->
  19. <!-- :key="requirement.id"-->
  20. <!-- v-bind="requirement"-->
  21. <!-- :ref="`treenode-${requirement.id}`"-->
  22. <!-- @addChild="addChild"-->
  23. <!-- @deleteNode="deleteNode"-->
  24. <!-- @nodeClick="nodeClick"-->
  25. <!-- />-->
  26. </div>
  27. <canvas id="line-drawer" :height="HEIGHT" :width="WIDTH"></canvas>
  28. </div>
  29. </template>
  30. <script>
  31. import TreeNode from './TreeNode.vue';
  32. // 容器宽高
  33. const HEIGHT = 700;
  34. const WIDTH = 1000;
  35. const TREE_MAX_HEIGHT = 4;
  36. const NODE_STYLE = {
  37. width: 250,
  38. height: 100,
  39. padding: 25,
  40. };
  41. export default {
  42. name: 'Tree',
  43. components: { TreeNode },
  44. props: {
  45. data: Array,
  46. },
  47. computed: {
  48. /**
  49. * @returns {*[][]}
  50. */
  51. treeNodeColList() {
  52. // 将点存入map便于调用
  53. const nodeMap = new Map();
  54. this.data.forEach((item) => {
  55. nodeMap.set(item.id, item);
  56. });
  57. // 将array转为树状结构(父组件中已处理无需再处理);顺便拆成col
  58. let rootNode;
  59. const colList = [];
  60. for (let i = 0; i < TREE_MAX_HEIGHT; i += 1) {
  61. colList.push([]);
  62. }
  63. if (this.data.length === 0) {
  64. return colList;
  65. }
  66. this.data.forEach((item) => {
  67. if (item.deep > 0 && item.deep <= TREE_MAX_HEIGHT) {
  68. colList[item.deep - 1].push(item);
  69. } else {
  70. console.warn('需求树数据有误,树高度错误');
  71. }
  72. if (!item.parentId) rootNode = item;
  73. // if (item.parent) {
  74. // const tempParent = nodeMap.get(item.parent);
  75. // if (!tempParent.childs) {
  76. // tempParent.childs = [];
  77. // }
  78. // tempParent.childs.push(item);
  79. // } else {
  80. // rootNode = item;
  81. // }
  82. });
  83. // 计算一个节点包含他的后代所需要的空间
  84. const calculateNodeContentHeight = (node) => {
  85. if (!node.childs) {
  86. node.contentHeight = 1;
  87. return 1;
  88. }
  89. node.contentHeight = node.childs.reduce((total, current) => total + calculateNodeContentHeight(current), 0);
  90. return node.contentHeight;
  91. };
  92. calculateNodeContentHeight(rootNode);
  93. // 计算节点基准Y轴位置
  94. rootNode.positionY = 0;
  95. for (let i = 1; i < TREE_MAX_HEIGHT; i += 1) {
  96. let sum = 0;
  97. colList[i].forEach((item) => {
  98. item.positionY = nodeMap.get(item.parentId).positionY + sum;
  99. sum += item.contentHeight;
  100. });
  101. }
  102. // 计算节点Y轴偏移位置
  103. for (let i = TREE_MAX_HEIGHT - 2; i >= 0; i -= 1) {
  104. colList[i].forEach((item) => {
  105. if (item.childs) {
  106. const { length } = item.childs;
  107. item.positionY = item.childs.reduce((total, current) => total + current.positionY / length, 0);
  108. }
  109. });
  110. }
  111. // 计算所需容器高度
  112. let maxPositionY = 0;
  113. this.data.forEach((item) => {
  114. if (item.positionY && item.positionY > maxPositionY) maxPositionY = item.positionY;
  115. });
  116. this.drawLine((maxPositionY * 130 + 100).toFixed(0));
  117. // 返回col
  118. return colList;
  119. },
  120. },
  121. data() {
  122. return {
  123. HEIGHT,
  124. WIDTH,
  125. TREE_MAX_HEIGHT,
  126. NODE_STYLE,
  127. ctx: null,
  128. };
  129. },
  130. mounted() {
  131. this.initCanvasCtx();
  132. // this.regenerateTree();
  133. },
  134. methods: {
  135. initCanvasCtx() {
  136. this.ctx = document.getElementById('line-drawer').getContext('2d');
  137. },
  138. drawLine(height) {
  139. setTimeout(() => {
  140. this.ctx.clearRect(0, 0, WIDTH, height > HEIGHT ? height : HEIGHT);
  141. this.ctx.lineWidth = 1;
  142. this.ctx.strokeStyle = '#bbbbbb';
  143. this.data.forEach((item) => {
  144. if (!item.childs) return;
  145. const parentCard = this.$refs[`treenode-${item.id}`].getTreeNodeSizeAndPosition();
  146. const start = {
  147. x: parentCard.left + parentCard.width,
  148. y: parentCard.top + parentCard.height / 2,
  149. };
  150. item.childs.forEach((child) => {
  151. const childCard = this.$refs[`treenode-${child.id}`].getTreeNodeSizeAndPosition();
  152. const end = {
  153. x: childCard.left,
  154. y: childCard.top + childCard.height / 2,
  155. };
  156. this.ctx.beginPath();
  157. this.ctx.moveTo(start.x, start.y);
  158. this.ctx.lineTo(end.x, end.y);
  159. this.ctx.stroke();
  160. });
  161. });
  162. });
  163. },
  164. addChild(id) {
  165. this.$emit('change', {
  166. type: 'addChildNode',
  167. id,
  168. });
  169. },
  170. deleteNode(id) {
  171. this.$confirm('是否确认删除该节点?', '提示', {
  172. confirmButtonText: '确定',
  173. cancelButtonText: '取消',
  174. type: 'warning',
  175. }).then(() => {
  176. this.$emit('change', {
  177. type: 'deleteTreeNode',
  178. id,
  179. });
  180. }).catch(() => {
  181. this.$message({
  182. type: 'info',
  183. message: '已取消删除',
  184. });
  185. });
  186. },
  187. nodeClick(id) {
  188. this.$emit('node-click', id);
  189. },
  190. },
  191. };
  192. </script>
  193. <style lang="scss">
  194. #tree-container {
  195. border-radius: 4px;
  196. box-shadow: 0 0 3px 0px #BDC3C7;
  197. position: relative;
  198. display: flex;
  199. overflow-y: auto;
  200. justify-content:center;
  201. background: url("https://devcloud.cn-north-4.huaweicloud.com/projectman/plan_back.05189e7a664e6edc2f8e.png") repeat;
  202. }
  203. #node-container {
  204. position:absolute;
  205. z-index: 11;
  206. }
  207. #line-drawer {
  208. position: absolute;
  209. }
  210. .jsmind-inner {
  211. position:relative;
  212. overflow:auto;
  213. width:100%;
  214. height:100%;
  215. user-select:none;
  216. }
  217. canvas {
  218. position:absolute;
  219. z-index:1;
  220. }
  221. jmnodes {
  222. position:absolute;
  223. z-index:2;
  224. background-color:rgba(0,0,0,0);
  225. }
  226. jmnode {
  227. position:absolute;
  228. cursor:default;
  229. max-width:400px;
  230. white-space:nowrap;
  231. overflow:hidden;
  232. text-overflow:ellipsis;
  233. padding:10px;
  234. background-color:#fff;
  235. color:#333;
  236. border-radius:1px;
  237. border: 1px solid #333;
  238. font:16px/1.125 Verdana,Arial,Helvetica,sans-serif;
  239. &:hover {
  240. box-shadow:2px 2px 4px #333;
  241. background-color:#ebebeb;
  242. color:#333;
  243. }
  244. &.selected {
  245. background-color:#11f;
  246. color:#fff;
  247. box-shadow:2px 2px 8px #000;
  248. }
  249. &.root {
  250. font-size:24px;
  251. }
  252. }
  253. jmexpander {
  254. position:absolute;
  255. width:11px;
  256. height:11px;
  257. display:block;
  258. overflow:hidden;
  259. line-height:12px;
  260. font-size:12px;
  261. text-align:center;
  262. border-radius:6px;
  263. border-width:1px;
  264. border-style:solid;
  265. cursor:pointer;
  266. border-color:gray;
  267. &:hover {
  268. border-color:#000;
  269. }
  270. }
  271. @media screen and (max-device-width: 1024px) {
  272. jmnode{padding:5px;border-radius:3px;font-size:14px;}
  273. jmnode.root{font-size:21px;}
  274. }
  275. </style>