Tree.vue 6.2 KB

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