mGraph.vue 15 KB

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