LibraryTree.vue 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. <template>
  2. <b-overlay :show="nodeTree.nodes.length === 0" class="h-100" z-index="0">
  3. <map-zoom
  4. id="library-tree"
  5. :min-zoom="0.3" :max-zoom="1" :center="center"
  6. :key="nodeDepartId"
  7. >
  8. <path
  9. v-for="link in nodeTree.links"
  10. :key="`${link.source.data.id}_${link.target.data.id}`"
  11. :d="lineGenerator([link.source, link.target])"
  12. :class="['svg-link', (link.linkType || link.target.data.linkType)]"
  13. />
  14. <circle
  15. v-for="node in nodeTree.nodes"
  16. :key="'circle' + node.data.id"
  17. :cx="node.x"
  18. :cy="node.y"
  19. class="svg-dot"
  20. :id="'preview-node-' + node.data.id"
  21. :class="['svg-dot-' + node.data.variant, { 'origin': node.parent === null }]"
  22. tabindex="0"
  23. @focus="activeNode = node"
  24. @mouseenter="activeNode = node"
  25. @mouseleave="activeNode = null"
  26. @blur="activeNode = null"
  27. @click.stop="onNodeClick(node.data)"
  28. @keyup.enter="onNodeClick(node.data)"
  29. />
  30. <g>
  31. <rect class="svg-overlay" />
  32. <foreignObject
  33. v-if="activeNode"
  34. :x="activeNode.x"
  35. :y="activeNode.y"
  36. >
  37. <dot-button
  38. :variant="activeNode.data.variant"
  39. active hovered
  40. @click="onNodeClick(activeNode.data)"
  41. >
  42. <template v-if="activeNode.data.type === 'prod'">
  43. {{ $t('variants.' + activeNode.data.variant) }}<br>
  44. </template>
  45. <node-view-title v-else :node="activeNode.data" block />
  46. </dot-button>
  47. </foreignObject>
  48. </g>
  49. </map-zoom>
  50. <node-preview-zone v-model="previewNode" :nodes="dataNodes" @open-node="onPreviewNodeClick" />
  51. </b-overlay>
  52. </template>
  53. <script>
  54. import { mapGetters } from 'vuex'
  55. import { line } from 'd3-shape'
  56. import { forceSimulation, forceLink, forceManyBody, forceX, forceY } from 'd3-force'
  57. import { MapZoom, NodePreviewZone } from '@/components/layouts'
  58. import { NodeViewTitle } from '@/components/nodes'
  59. export default {
  60. name: 'LibraryTree',
  61. components: {
  62. MapZoom,
  63. NodePreviewZone,
  64. NodeViewTitle
  65. },
  66. data () {
  67. return {
  68. activeNode: null,
  69. previewNode: null
  70. }
  71. },
  72. computed: {
  73. ...mapGetters(['nodeDepartId', 'nodeTree']),
  74. dataNodes () {
  75. return this.nodeTree.nodes.map(node => node.data)
  76. },
  77. // ONE TIME GETTER
  78. simulation () {
  79. return forceSimulation()
  80. .force('charge', forceManyBody().strength((d) => {
  81. if (d.data.linkType === 'parents') return -2000
  82. if (d.data.linkType === 'siblings') return -500
  83. return -1000
  84. }))
  85. .force('link', forceLink().id(d => d.id).distance((d) => {
  86. if (d.linkType === 'parents') return 200
  87. if (d.linkType === 'siblings') return 100
  88. return 0
  89. }).strength(0.5))
  90. .force('x', forceX())
  91. .force('y', forceY())
  92. },
  93. center () {
  94. const { x, y } = this.nodeTree.nodes.length ? this.nodeTree.nodes[0] : { x: 0, y: 0 }
  95. return { x, y }
  96. },
  97. // ONE TIME GETTER
  98. lineGenerator () {
  99. return line().x(node => node.x).y(node => node.y)
  100. }
  101. },
  102. watch: {
  103. nodeTree (tree) {
  104. this.activeNode = null
  105. this.previewNode = null
  106. this.simulation.nodes(tree.nodes)
  107. this.simulation.force('link').links(tree.links)
  108. this.simulation.alpha(0.5).restart()
  109. }
  110. },
  111. methods: {
  112. onNodeClick (node) {
  113. this.$store.dispatch('GET_NODE', { id: node.id, dataLevel: 'partial' })
  114. this.previewNode = node
  115. this.$root.$emit('bv::show::popover', 'preview-node-' + node.id)
  116. },
  117. onPreviewNodeClick (ids) {
  118. this.$root.$emit('bv::hide::popover', 'preview-node-' + this.previewNode.id)
  119. this.$emit('open-node', ids)
  120. this.previewNode = null
  121. }
  122. },
  123. created () {
  124. this.$store.dispatch('INIT_LIBRARY_TREE')
  125. }
  126. }
  127. </script>
  128. <style lang="scss" scoped>
  129. .svg {
  130. &-link {
  131. stroke: grey;
  132. vector-effect: non-scaling-stroke;
  133. &.siblings {
  134. stroke-dasharray: 4;
  135. }
  136. &.parents {
  137. stroke: red;
  138. opacity: .3;
  139. }
  140. }
  141. &-dot {
  142. cursor: pointer;
  143. r: 9.5px;
  144. @each $color, $value in $theme-colors {
  145. &-#{$color} {
  146. fill: $value;
  147. @if $color == 'depart' {
  148. stroke: $black;
  149. stroke-width: 3px;
  150. vector-effect: non-scaling-stroke;
  151. }
  152. }
  153. }
  154. &.origin {
  155. r: 15px;
  156. }
  157. }
  158. &-overlay {
  159. width: 100%;
  160. height: 100%;
  161. x: -50%;
  162. y: -50%;
  163. fill: transparent;
  164. pointer-events: none;
  165. }
  166. }
  167. foreignObject {
  168. height: 1px;
  169. width: 1px;
  170. overflow: visible;
  171. }
  172. .dot-btn {
  173. transform: translate(-50%, -50%);
  174. pointer-events: none;
  175. &-depart {
  176. box-shadow: none;
  177. }
  178. h6 {
  179. margin: 0;
  180. }
  181. }
  182. </style>