mGraph.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. <template>
  2. <div
  3. style="height:100%; position: relative; font-size: 16px;"
  4. v-loading="loading"
  5. class="d-flex align-center justify-center"
  6. >
  7. <div class="legend d-flex pa-3 justify-space-between">
  8. <div class="select pointerEvents">
  9. <v-select
  10. v-model="type"
  11. :items="items"
  12. dense
  13. outlined
  14. hide-details
  15. style="background-color: #fff; width: 150px;"
  16. @change="changeType"
  17. ></v-select>
  18. </div>
  19. <!-- 图例 -->
  20. <div class="d-flex align-center">
  21. <div
  22. v-for="item in legend"
  23. :key="item.title"
  24. class="d-flex ml-5"
  25. >
  26. <div class="pa-3 mr-3 rounded-circle" :style="`background-color: ${item.color};`"></div>
  27. {{ item.title }}
  28. </div>
  29. <v-switch
  30. class="pointerEvents mt-0 ml-3"
  31. hide-details
  32. v-model="showMeta"
  33. :false-value="false"
  34. :true-value="true"
  35. label="显示元数据"
  36. @change="handleChangeMeta"
  37. ></v-switch>
  38. </div>
  39. </div>
  40. <MEmpty v-if="empty"></MEmpty>
  41. <relation-graph
  42. v-show="!empty"
  43. ref="graphRef"
  44. :options="graphOptions"
  45. :on-node-click="onNodeClick"
  46. :on-line-click="onLineClick"
  47. >
  48. <template #node="{node}">
  49. <div @contextmenu.prevent="handleContextmenu($event, node)">
  50. <div
  51. :style="{ width: node.width + 'px', height: node.height + 'px' }"
  52. :class="{ 'node-active': menu.activeId === +node.id }"
  53. class="rounded-circle"
  54. >
  55. </div>
  56. <div
  57. :style="{ 'background-color': node.color + '44', width: 20 * ((node.text || '').length) + 'px' }"
  58. class="node-text"
  59. :class="{ 'node-active': menu.activeId === +node.id }"
  60. >
  61. {{ node.text || '' }}
  62. </div>
  63. </div>
  64. </template>
  65. <template #graph-plug>
  66. <v-menu v-model="menu.show" attach :position-x="menu.x" :position-y="menu.y" absolute offset-y min-width="200">
  67. <v-list dense>
  68. <v-list-item v-for="(k, i) in menu.items" :key="i" @click="k.handle">
  69. <v-list-item-title>{{ k.title }}</v-list-item-title>
  70. </v-list-item>
  71. </v-list>
  72. </v-menu>
  73. </template>
  74. </relation-graph>
  75. </div>
  76. </template>
  77. <script>
  78. import RelationGraph from 'relation-graph'
  79. import MEmpty from '@/components/Common/empty'
  80. import { api } from '@/api/dataGovernance'
  81. const defaultNodeColor = 'rgba(238, 178, 94, 1)'
  82. const NODES_SIZE = {
  83. width: 30,
  84. height: 30
  85. }
  86. // const LINE_COLOR_MAP = {
  87. // 包含: '#F44336', // red
  88. // 影响: '#E91E63', // pink
  89. // 依赖: '#3F51B5', // indigo
  90. // 来源: '#4CAF50', // green
  91. // 引用: '#9C27B0', // purple
  92. // 继承: '#673AB7', // deep-purple
  93. // 标记: '#2196F3', // blue
  94. // 使用: '#03A9F4', // light-blue
  95. // 关联: '#00BCD4', // cyan
  96. // 拥有: '#009688', // teal
  97. // 下级: '#8BC34A', // light-green
  98. // 上级: '#CDDC39', // lime
  99. // 联动: '#FF9800' // orange
  100. // }
  101. export default {
  102. name: 'details-graph',
  103. components: { RelationGraph, MEmpty },
  104. props: {
  105. toApi: {
  106. type: Function,
  107. default: api.getResourceGraph
  108. },
  109. meta: {
  110. type: Boolean,
  111. default: false
  112. },
  113. query: {
  114. type: Object,
  115. default: () => ({})
  116. }
  117. },
  118. data () {
  119. return {
  120. menu: {
  121. x: 0,
  122. y: 0,
  123. show: false,
  124. items: [
  125. { title: '查看', handle: this.handleView }
  126. ],
  127. activeId: +this.$route.params.id,
  128. item: {}
  129. },
  130. empty: true,
  131. items: [
  132. { text: '全链关系', value: 'all' },
  133. { text: '血缘关系', value: 'kinship' },
  134. { text: '影响关系', value: 'impact' }
  135. ],
  136. type: 'all',
  137. loading: false,
  138. graphOptions: {
  139. // defaultJunctionPoint: 'lr',
  140. // 这里可以参考"Graph 图谱"中的参数进行设置 https://www.relation-graph.com/#/docs/graph
  141. debug: false, // 是否开始调试模式,调试模式下会在控制台打印额外的日志信息
  142. showDebugPanel: false, // 是否显示调试按钮,通过此按钮可以打印配置、数据等
  143. backgroundImage: '', // 图谱水印url,如:https://ssl.relation-graph.com/images/relatioon-graph-canvas-bg.png
  144. downloadImageFileName: '', // 下载图片时,图片的名称
  145. disableZoom: false, // 是否禁用图谱的缩放功能
  146. disableDragNode: false, // 是否禁用图谱中节点的拖动
  147. moveToCenterWhenRefresh: true, // 当图谱刷新后(调用setJsonData或refresh方法都会触发),让图谱根据节点居中(图片会默认将根节点作为中心展示,此选项会根据节点分布寻找中心)
  148. zoomToFitWhenRefresh: true, // 当图谱刷新后(调用setJsonData或refresh方法都会触发),是否让图谱缩放到适合可见区域大小,此选项不适用于fixed和force布局
  149. useAnimationWhenRefresh: true, // 当图谱刷新后(调用setJsonData或refresh方法都会触发),使用动画让图居中、缩放
  150. useAnimationWhenExpanded: true,
  151. defaultFocusRootNode: true, // 默认为根节点添加一个被选中的样式
  152. disableNodeClickEffect: false, // 是否禁用节点默认的点击效果(选中、闪烁)
  153. disableLineClickEffect: false, // 是否禁用线条默认的点击效果(选中、闪烁)
  154. allowShowZoomMenu: true, // 是否在右侧菜单栏显示放大缩小的按钮,此设置和disableZoom不冲突
  155. allowAutoLayoutIfSupport: true, // 是否在工具栏中显示【自动布局】按钮(只有在布局支持且此选项为true时才会显示的按钮)
  156. allowShowRefreshButton: true, // 是否在工具栏中显示【刷新】按钮
  157. allowShowDownloadButton: true, // 是否在工具栏中显示【下载图片】按钮
  158. backgroundImageNoRepeat: false, // 只在右下角显示水印,不重复显示水印
  159. allowSwitchLineShape: true, // 是否在工具栏中显示切换线条形状的按钮
  160. allowSwitchJunctionPoint: true, // 是否在工具栏中显示切换连接点位置的按钮
  161. isMoveByParentNode: false, // 是否在拖动节点后让子节点跟随
  162. defaultExpandHolderPosition: 'hide', // 默认的节点展开/关闭按钮位置(left/top/right/bottom/hide)
  163. defaultNodeColor, // 默认的节点背景颜色
  164. checkedLineColor: '#FD8B37', // 当线条被选中时的颜色
  165. defaultNodeFontColor: '#ffffff', // 默认的节点文字颜色
  166. defaultNodeBorderColor: '#90EE90', // 默认的节点边框颜色
  167. defaultNodeBorderWidth: 0, // 默认的节点边框粗细(像素)
  168. defaultLineColor: '#cccccc', // 默认的线条颜色
  169. defaultLineWidth: 2, // 默认的线条粗细(像素)
  170. defaultLineShape: 2, // 默认的线条样式(1:直线/2:样式2/3:样式3/4:折线/5:样式5/6:样式6)使用示例
  171. defaultNodeShape: 0, // 默认的节点形状,0:圆形;1:矩形
  172. defaultShowLineLabel: true, // 默认是否显示连线文字,v2版本此选项已无效,主要是这个选项没什么用
  173. hideNodeContentByZoom: true, // 是否根据缩放比例隐藏节点内容
  174. // disableDragCanvas: false,
  175. // lineUseTextPath: false,
  176. defaultLineMarker: { // 默认的线条箭头样式,示例参考:配置工具中的选项:连线箭头样式
  177. markerWidth: 24,
  178. markerHeight: 24,
  179. refX: 6,
  180. refY: 6,
  181. data: 'M2,2 L10,6 L2,10 L6,6 L2,2'
  182. },
  183. layouts: [
  184. {
  185. label: '自动布局',
  186. layoutName: 'center', // 布局方式(tree树状布局/center中心布局/force自动布局)
  187. from: 'left',
  188. maxLayoutTimes: 20,
  189. layoutClassName: 'seeks-layout-force',
  190. useLayoutStyleOptions: false,
  191. defaultNodeColor: '#FFC5A6',
  192. defaultNodeFontColor: '#000000',
  193. defaultNodeBorderColor: '#efefef',
  194. defaultNodeBorderWidth: 1,
  195. defaultLineColor: '#FD8B37',
  196. defaultLineWidth: 1,
  197. defaultShowLineLabel: true,
  198. defaultLineMarker: {
  199. markerWidth: 12,
  200. markerHeight: 12,
  201. refX: 6,
  202. refY: 6,
  203. data: 'M2,2 L10,6 L2,10 L6,6 L2,2'
  204. }
  205. }
  206. ]
  207. },
  208. config: {
  209. BusinessDomain: {
  210. color: '#9FA8DA',
  211. title: '业务域',
  212. className: 'sourceNode',
  213. ...NODES_SIZE
  214. },
  215. DataSource: {
  216. color: '#4CAF50',
  217. title: '数据源',
  218. className: 'modelNode',
  219. ...NODES_SIZE
  220. },
  221. // DataResource: { // 资源
  222. // color: '#9FA8DA',
  223. // title: '数据资源',
  224. // className: 'sourceNode',
  225. // ...NODES_SIZE
  226. // },
  227. // DataModel: { // 模型
  228. // color: '#EF9A9A',
  229. // title: '数据模型',
  230. // className: 'modelNode',
  231. // ...NODES_SIZE
  232. // },
  233. // DataMetric: { // 指标
  234. // color: '#00BCD4',
  235. // title: '数据指标',
  236. // className: 'metricNode',
  237. // ...NODES_SIZE
  238. // },
  239. // standard: { // 标准
  240. // color: '#009688',
  241. // title: '数据标准',
  242. // className: 'standardNode',
  243. // ...NODES_SIZE
  244. // },
  245. DataLabel: { // 标签
  246. color: '#9C27B0',
  247. title: '数据标签',
  248. className: 'labelNode',
  249. ...NODES_SIZE
  250. },
  251. DataMeta: { // 元数据
  252. color: defaultNodeColor,
  253. title: '元数据',
  254. className: '',
  255. ...NODES_SIZE
  256. }
  257. },
  258. showMeta: this.meta
  259. }
  260. },
  261. computed: {
  262. legend () {
  263. return Object.values(this.config)
  264. }
  265. },
  266. mounted () {
  267. this.init()
  268. },
  269. methods: {
  270. handleContextmenu (v, node) {
  271. const { left, top } = this.$refs.graphRef.$el.getBoundingClientRect()
  272. this.menu.x = v.clientX - left
  273. this.menu.y = v.clientY - top
  274. this.menu.item = node
  275. this.menu.show = true
  276. },
  277. handleView () {
  278. this.draw({ id: +this.menu.item.id }, () => {
  279. this.menu.activeId = +this.menu.item.id
  280. })
  281. },
  282. handleChangeMeta (val) {
  283. this.showMeta = val
  284. this.init()
  285. },
  286. changeType () {
  287. this.init()
  288. },
  289. handleClick (node) {
  290. console.log(node)
  291. },
  292. async init () {
  293. const query = {
  294. ...this.query
  295. }
  296. if (this.$route.params.id) {
  297. Object.assign(query, {
  298. id: +this.$route.params.id
  299. })
  300. }
  301. this.draw(query)
  302. },
  303. async draw (query, successCallback = () => {}) {
  304. // 清空再渲染
  305. this.loading = true
  306. this.empty = false
  307. try {
  308. const { data } = await this.toApi({
  309. ...query,
  310. type: this.type,
  311. meta: this.showMeta
  312. })
  313. if (!data.nodes || !data.nodes.length) {
  314. this.empty = true
  315. this.loading = false
  316. return
  317. }
  318. this.graphOptions.downloadImageFileName = data.rootId ?? ''
  319. data.nodes.forEach(ele => {
  320. if (!this.config[ele.node_type]) {
  321. return
  322. }
  323. ele.text = ele.name_zh || ele.name_en
  324. Object.assign(ele, this.config[ele.node_type])
  325. })
  326. // data.lines.forEach(ele => {
  327. // ele.color = LINE_COLOR_MAP[ele.text]
  328. // })
  329. this.$nextTick(() => {
  330. this.$refs.graphRef.setOptions(this.graphOptions, async (graphInstance) => {
  331. if (!this.$refs.graphRef || !this.$refs.graphRef.setJsonData) {
  332. return
  333. }
  334. this.$refs.graphRef.setJsonData(data, async (_graphInstance) => {
  335. await _graphInstance.setZoom(75)
  336. successCallback()
  337. this.loading = false
  338. })
  339. })
  340. })
  341. } catch (error) {
  342. this.empty = true
  343. this.$snackbar.error(error)
  344. }
  345. },
  346. onNodeClick (nodeObject, $event) {
  347. console.log('onNodeClick:', nodeObject)
  348. },
  349. onLineClick (lineObject, $event) {
  350. console.log('onLineClick:', lineObject)
  351. }
  352. }
  353. }
  354. </script>
  355. <style lang="scss" scoped>
  356. .legend {
  357. pointer-events: none;
  358. width: 100%;
  359. user-select: none;
  360. -moz-user-select: none;
  361. -webkit-user-select: none;
  362. -ms-user-select: none;
  363. position: absolute;
  364. top: 0;
  365. z-index: 10;
  366. color: #666;
  367. .rounded-circle {
  368. width: 24px;
  369. height: 24px;
  370. }
  371. }
  372. .pointerEvents {
  373. pointer-events: auto;
  374. }
  375. // ::v-deep .sourceNode .rel-node-checked {
  376. // box-shadow: 0 0 0 8px #C5CAE9 !important;
  377. // }
  378. // ::v-deep .modelNode .rel-node-checked {
  379. // box-shadow: 0 0 0 8px #ffd9d9 !important;
  380. // }
  381. // ::v-deep .metricNode .rel-node-checked {
  382. // box-shadow: 0 0 0 8px #a7f3fd !important;
  383. // }
  384. // ::v-deep .standardNode .rel-node-checked {
  385. // box-shadow: 0 0 0 8px #58c1b7 !important;
  386. // }
  387. // ::v-deep .labelNode .rel-node-checked {
  388. // box-shadow: 0 0 0 8px #f4bdff !important;
  389. // }
  390. ::v-deep .rel-node-checked {
  391. box-shadow: unset !important;
  392. }
  393. .node-active {
  394. box-shadow: 0 0 0px 6px #f3f900 !important;
  395. // background: #000 !important;
  396. // border: 2px solid #000;
  397. }
  398. .node-text {
  399. color: #000;
  400. font-size: 16px;
  401. position: absolute;
  402. height:25px;
  403. transform: translate(-50%, 0);
  404. line-height: 25px;
  405. left: 50%;
  406. margin-top:5px;
  407. text-align: center;
  408. }
  409. ::v-deep .rel-toolbar {
  410. background-color: #f39930;
  411. color: #ffffff;
  412. .c-current-zoom {
  413. color: #ffffff;
  414. }
  415. }
  416. </style>