ソースを参照

关系图谱:节点详情浮窗展示

Xiao_123 3 日 前
コミット
0fb6e5a7a4

+ 60 - 15
src/views/dataBook/components/mGraph.vue

@@ -47,21 +47,38 @@
       :on-line-click="onLineClick"
     >
       <template #node="{node}">
-        <div @contextmenu.prevent="handleContextmenu($event, node)">
-          <div
-            :style="{ width: node.width + 'px', height: node.height + 'px' }"
-            :class="{ 'node-active':  menu.activeId === +node.id }"
-            class="rounded-circle"
-          >
+        <v-tooltip left :color="node.color || 'primary'">
+          <template v-slot:activator="{ on, attrs }">
+            <div @contextmenu.prevent="handleContextmenu($event, node)" v-bind="attrs" v-on="on">
+              <div
+                :style="{ width: node.width + 'px', height: node.height + 'px' }"
+                :class="{ 'node-active':  menu.activeId === +node.id }"
+                class="rounded-circle"
+              >
+              </div>
+              <div
+                :style="{ 'background-color': node.color + '44', width: 20 * ((node.text || '').length) + 'px' }"
+                class="node-text"
+                :class="{ 'node-active':  menu.activeId === +node.id }"
+              >
+                {{ node.text || '' }}
+              </div>
+            </div>
+          </template>
+          <!-- 浮窗展示节点详情 -->
+          <div class="d-flex">
+            <ul style="color: #fff;text-align:right;">
+              <template v-for="key in Object.keys(detailKeys)">
+                <li v-if="node.data[key]" :key="key">{{ detailKeys[key] }} :</li>
+              </template>
+            </ul>
+            <ul>
+              <template v-for="key in Object.keys(detailKeys)">
+                <li v-if="node.data[key]" :key="key">{{ key === 'status' ? (node.data[key] ? '已启用' : '已禁用') : node.data[key] }}</li>
+              </template>
+            </ul>
           </div>
-          <div
-            :style="{ 'background-color': node.color + '44', width: 20 * ((node.text || '').length) + 'px' }"
-            class="node-text"
-            :class="{ 'node-active':  menu.activeId === +node.id }"
-          >
-            {{ node.text || '' }}
-          </div>
-        </div>
+        </v-tooltip>
       </template>
       <template #graph-plug>
         <v-menu v-model="menu.show" attach :position-x="menu.x" :position-y="menu.y" absolute offset-y min-width="200">
@@ -258,7 +275,31 @@ export default {
           ...NODES_SIZE
         }
       },
-      showMeta: this.meta
+      showMeta: this.meta,
+      // 节点详情
+      detailKeys: {
+        name_zh: '中文名称',
+        name_en: '英文名称',
+        category: '分类',
+        organization: '所属机构',
+        frequency: '更新频率',
+        data_sensitivity: '数据敏感度',
+        data_source: '使用数据源',
+        storage_location: '存储位置',
+        type: '数据库类型',
+        host: '数据库IP地址',
+        port: '数据库IP端口',
+        database: '数据库名称',
+        username: '用户名',
+        password: '密码',
+        param: '参数',
+        leader: '负责人',
+        status: '状态',
+        create_time: '创建时间',
+        update_time: '更新时间',
+        describe: '描述/备注',
+        desc: '描述/备注'
+      }
     }
   },
   computed: {
@@ -325,6 +366,7 @@ export default {
             return
           }
           ele.text = ele.name_zh || ele.name_en
+          ele.data = { ...ele }
           Object.assign(ele, this.config[ele.node_type])
         })
 
@@ -359,6 +401,9 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+ul{
+  list-style: none;
+}
 .legend {
   pointer-events: none;
   width: 100%;

+ 423 - 0
src/views/dataBook/components/mGraphCopy.vue

@@ -0,0 +1,423 @@
+<template>
+  <div
+    style="height:100%; position: relative; font-size: 16px;"
+    v-loading="loading"
+    class="d-flex align-center justify-center"
+  >
+    <div class="legend d-flex pa-3 justify-space-between">
+      <!-- <div class="select pointerEvents">
+        <v-select
+          v-model="type"
+          :items="items"
+          dense
+          outlined
+          hide-details
+          style="background-color: #fff; width: 150px;"
+          @change="changeType"
+        ></v-select>
+      </div> -->
+      <!-- 图例 -->
+      <div class="d-flex align-center">
+        <div
+          v-for="item in legend"
+          :key="item.title"
+          class="d-flex mr-5"
+        >
+          <div class="pa-3 mr-3 rounded-circle" :style="`background-color: ${item.color};`"></div>
+          {{ item.title }}
+        </div>
+        <v-switch
+          v-if="!meta"
+          class="pointerEvents mt-0 ml-3"
+          hide-details
+          v-model="showMeta"
+          :false-value="false"
+          :true-value="true"
+          label="显示元数据"
+          @change="handleChangeMeta"
+        ></v-switch>
+      </div>
+    </div>
+    <MEmpty v-if="empty"></MEmpty>
+    <relation-graph
+      v-show="!empty"
+      ref="graphRef"
+      :options="graphOptions"
+      :on-node-click="onNodeClick"
+      :on-line-click="onLineClick"
+    >
+      <template #node="{node}">
+        <div @contextmenu.prevent="handleContextmenu($event, node)">
+          <div
+            :style="{ width: node.width + 'px', height: node.height + 'px' }"
+            :class="{ 'node-active':  menu.activeId === +node.id }"
+            class="rounded-circle"
+          >
+          </div>
+          <div
+            :style="{ 'background-color': node.color + '44', width: 20 * ((node.text || '').length) + 'px' }"
+            class="node-text"
+            :class="{ 'node-active':  menu.activeId === +node.id }"
+          >
+            {{ node.text || '' }}
+          </div>
+        </div>
+      </template>
+      <template #graph-plug>
+        <v-menu v-model="menu.show" attach :position-x="menu.x" :position-y="menu.y" absolute offset-y min-width="200">
+          <v-list dense>
+            <v-list-item v-for="(k, i) in menu.items" :key="i" @click="k.handle">
+              <v-list-item-title>{{ k.title }}</v-list-item-title>
+            </v-list-item>
+          </v-list>
+        </v-menu>
+      </template>
+    </relation-graph>
+
+  </div>
+</template>
+
+<script>
+import RelationGraph from 'relation-graph'
+import MEmpty from '@/components/Common/empty'
+import { api } from '@/api/dataGovernance'
+const defaultNodeColor = 'rgba(238, 178, 94, 1)'
+const NODES_SIZE = {
+  width: 30,
+  height: 30
+}
+// const LINE_COLOR_MAP = {
+//   包含: '#F44336', // red
+//   影响: '#E91E63', // pink
+//   依赖: '#3F51B5', // indigo
+//   来源: '#4CAF50', // green
+//   引用: '#9C27B0', // purple
+//   继承: '#673AB7', // deep-purple
+//   标记: '#2196F3', // blue
+//   使用: '#03A9F4', // light-blue
+//   关联: '#00BCD4', // cyan
+//   拥有: '#009688', // teal
+//   下级: '#8BC34A', // light-green
+//   上级: '#CDDC39', // lime
+//   联动: '#FF9800' // orange
+// }
+export default {
+  name: 'details-graph',
+  components: { RelationGraph, MEmpty },
+  props: {
+    toApi: {
+      type: Function,
+      default: api.getResourceGraph
+    },
+    meta: {
+      type: Boolean,
+      default: false
+    },
+    query: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  data () {
+    return {
+      menu: {
+        x: 0,
+        y: 0,
+        show: false,
+        items: [
+          { title: '查看', handle: this.handleView }
+        ],
+        activeId: +this.$route.params.id,
+        item: {}
+      },
+      empty: true,
+      items: [
+        { text: '全链关系', value: 'all' },
+        { text: '血缘关系', value: 'kinship' },
+        { text: '影响关系', value: 'impact' }
+      ],
+      type: 'all',
+      loading: false,
+      graphOptions: {
+        // defaultJunctionPoint: 'lr',
+        // 这里可以参考"Graph 图谱"中的参数进行设置 https://www.relation-graph.com/#/docs/graph
+        debug: false, // 是否开始调试模式,调试模式下会在控制台打印额外的日志信息
+        showDebugPanel: false, // 是否显示调试按钮,通过此按钮可以打印配置、数据等
+        backgroundImage: '', // 图谱水印url,如:https://ssl.relation-graph.com/images/relatioon-graph-canvas-bg.png
+        downloadImageFileName: '', // 下载图片时,图片的名称
+        disableZoom: false, // 是否禁用图谱的缩放功能
+        disableDragNode: false, // 是否禁用图谱中节点的拖动
+        moveToCenterWhenRefresh: true, // 当图谱刷新后(调用setJsonData或refresh方法都会触发),让图谱根据节点居中(图片会默认将根节点作为中心展示,此选项会根据节点分布寻找中心)
+        zoomToFitWhenRefresh: true, // 当图谱刷新后(调用setJsonData或refresh方法都会触发),是否让图谱缩放到适合可见区域大小,此选项不适用于fixed和force布局
+        useAnimationWhenRefresh: true, // 当图谱刷新后(调用setJsonData或refresh方法都会触发),使用动画让图居中、缩放
+        useAnimationWhenExpanded: true,
+        defaultFocusRootNode: true, // 默认为根节点添加一个被选中的样式
+        disableNodeClickEffect: false, // 是否禁用节点默认的点击效果(选中、闪烁)
+        disableLineClickEffect: false, // 是否禁用线条默认的点击效果(选中、闪烁)
+        allowShowZoomMenu: true, // 是否在右侧菜单栏显示放大缩小的按钮,此设置和disableZoom不冲突
+        allowAutoLayoutIfSupport: true, // 是否在工具栏中显示【自动布局】按钮(只有在布局支持且此选项为true时才会显示的按钮)
+        allowShowRefreshButton: true, // 是否在工具栏中显示【刷新】按钮
+        allowShowDownloadButton: true, // 是否在工具栏中显示【下载图片】按钮
+        backgroundImageNoRepeat: false, // 只在右下角显示水印,不重复显示水印
+        allowSwitchLineShape: true, // 是否在工具栏中显示切换线条形状的按钮
+        allowSwitchJunctionPoint: true, // 是否在工具栏中显示切换连接点位置的按钮
+        isMoveByParentNode: false, // 是否在拖动节点后让子节点跟随
+        defaultExpandHolderPosition: 'hide', // 默认的节点展开/关闭按钮位置(left/top/right/bottom/hide)
+        defaultNodeColor, // 默认的节点背景颜色
+        checkedLineColor: '#FD8B37', // 当线条被选中时的颜色
+        defaultNodeFontColor: '#ffffff', // 默认的节点文字颜色
+        defaultNodeBorderColor: '#90EE90', // 默认的节点边框颜色
+        defaultNodeBorderWidth: 0, // 默认的节点边框粗细(像素)
+        defaultLineColor: '#cccccc', // 默认的线条颜色
+        defaultLineWidth: 2, // 默认的线条粗细(像素)
+        defaultLineShape: 2, // 默认的线条样式(1:直线/2:样式2/3:样式3/4:折线/5:样式5/6:样式6)使用示例
+        defaultNodeShape: 0, // 默认的节点形状,0:圆形;1:矩形
+        defaultShowLineLabel: true, // 默认是否显示连线文字,v2版本此选项已无效,主要是这个选项没什么用
+        hideNodeContentByZoom: true, // 是否根据缩放比例隐藏节点内容
+        // disableDragCanvas: false,
+        // lineUseTextPath: false,
+        defaultLineMarker: { // 默认的线条箭头样式,示例参考:配置工具中的选项:连线箭头样式
+          markerWidth: 24,
+          markerHeight: 24,
+          refX: 6,
+          refY: 6,
+          data: 'M2,2 L10,6 L2,10 L6,6 L2,2'
+        },
+        layouts: [
+          {
+            label: '自动布局',
+            layoutName: 'center', // 布局方式(tree树状布局/center中心布局/force自动布局)
+            from: 'left',
+            maxLayoutTimes: 20,
+            layoutClassName: 'seeks-layout-force',
+            useLayoutStyleOptions: false,
+            defaultNodeColor: '#FFC5A6',
+            defaultNodeFontColor: '#000000',
+            defaultNodeBorderColor: '#efefef',
+            defaultNodeBorderWidth: 1,
+            defaultLineColor: '#FD8B37',
+            defaultLineWidth: 1,
+            defaultShowLineLabel: true,
+            defaultLineMarker: {
+              markerWidth: 12,
+              markerHeight: 12,
+              refX: 6,
+              refY: 6,
+              data: 'M2,2 L10,6 L2,10 L6,6 L2,2'
+            }
+          }
+        ]
+      },
+      config: {
+        BusinessDomain: {
+          color: '#9FA8DA',
+          title: '业务域',
+          className: 'sourceNode',
+          ...NODES_SIZE
+        },
+        DataSource: {
+          color: '#4CAF50',
+          title: '数据源',
+          className: 'modelNode',
+          ...NODES_SIZE
+        },
+        // DataResource: { // 资源
+        //   color: '#9FA8DA',
+        //   title: '数据资源',
+        //   className: 'sourceNode',
+        //   ...NODES_SIZE
+        // },
+        // DataModel: { // 模型
+        //   color: '#EF9A9A',
+        //   title: '数据模型',
+        //   className: 'modelNode',
+        //   ...NODES_SIZE
+        // },
+        // DataMetric: { // 指标
+        //   color: '#00BCD4',
+        //   title: '数据指标',
+        //   className: 'metricNode',
+        //   ...NODES_SIZE
+        // },
+        data_standard: { // 标准
+          color: '#009688',
+          title: '数据标准',
+          className: 'standardNode',
+          ...NODES_SIZE
+        },
+        DataLabel: { // 标签
+          color: '#9C27B0',
+          title: '数据标签',
+          className: 'labelNode',
+          ...NODES_SIZE
+        },
+        DataMeta: { // 元数据
+          color: defaultNodeColor,
+          title: '元数据',
+          className: '',
+          ...NODES_SIZE
+        }
+      },
+      showMeta: this.meta
+    }
+  },
+  computed: {
+    legend () {
+      return Object.values(this.config)
+    }
+  },
+  mounted () {
+    this.init()
+  },
+  methods: {
+    handleContextmenu (v, node) {
+      const { left, top } = this.$refs.graphRef.$el.getBoundingClientRect()
+      this.menu.x = v.clientX - left
+      this.menu.y = v.clientY - top
+      this.menu.item = node
+      this.menu.show = true
+    },
+    handleView () {
+      this.draw({ id: +this.menu.item.id }, () => {
+        this.menu.activeId = +this.menu.item.id
+      })
+    },
+    handleChangeMeta (val) {
+      this.showMeta = val
+      this.init()
+    },
+    changeType () {
+      this.init()
+    },
+    handleClick (node) {
+      console.log(node)
+    },
+    async init () {
+      const query = {
+        ...this.query
+      }
+      if (this.$route.params.id) {
+        Object.assign(query, {
+          id: +this.$route.params.id
+        })
+      }
+      this.draw(query)
+    },
+    async draw (query, successCallback = () => {}) {
+      // 清空再渲染
+      this.loading = true
+      this.empty = false
+      try {
+        const { data } = await this.toApi({
+          ...query,
+          type: this.type,
+          meta: this.showMeta
+        })
+        if (!data.nodes || !data.nodes.length) {
+          this.empty = true
+          this.loading = false
+          return
+        }
+        this.graphOptions.downloadImageFileName = data.rootId ?? ''
+
+        data.nodes.forEach(ele => {
+          if (!this.config[ele.node_type]) {
+            return
+          }
+          ele.text = ele.name_zh || ele.name_en
+          Object.assign(ele, this.config[ele.node_type])
+        })
+
+        // data.lines.forEach(ele => {
+        //   ele.color = LINE_COLOR_MAP[ele.text]
+        // })
+        this.$nextTick(() => {
+          this.$refs.graphRef.setOptions(this.graphOptions, async (graphInstance) => {
+            if (!this.$refs.graphRef || !this.$refs.graphRef.setJsonData) {
+              return
+            }
+            this.$refs.graphRef.setJsonData(data, async (_graphInstance) => {
+              await _graphInstance.setZoom(75)
+              successCallback()
+              this.loading = false
+            })
+          })
+        })
+      } catch (error) {
+        this.empty = true
+        this.$snackbar.error(error)
+      }
+    },
+    onNodeClick (nodeObject, $event) {
+      console.log('onNodeClick:', nodeObject)
+    },
+    onLineClick (lineObject, $event) {
+      console.log('onLineClick:', lineObject)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.legend {
+  pointer-events: none;
+  width: 100%;
+  user-select: none;
+  -moz-user-select: none;
+  -webkit-user-select: none;
+  -ms-user-select: none;
+  position: absolute;
+  top: 0;
+  z-index: 10;
+  color: #666;
+  .rounded-circle {
+    width: 24px;
+    height: 24px;
+  }
+}
+.pointerEvents {
+  pointer-events: auto;
+}
+// ::v-deep .sourceNode .rel-node-checked {
+//   box-shadow: 0 0 0 8px #C5CAE9 !important;
+// }
+// ::v-deep .modelNode .rel-node-checked {
+//   box-shadow: 0 0 0 8px #ffd9d9 !important;
+// }
+// ::v-deep .metricNode .rel-node-checked {
+//   box-shadow: 0 0 0 8px #a7f3fd !important;
+// }
+// ::v-deep .standardNode .rel-node-checked {
+//   box-shadow: 0 0 0 8px #58c1b7 !important;
+// }
+// ::v-deep .labelNode .rel-node-checked {
+//   box-shadow: 0 0 0 8px #f4bdff !important;
+// }
+::v-deep .rel-node-checked {
+  box-shadow: unset !important;
+}
+.node-active {
+  box-shadow: 0 0 0px 6px #f3f900 !important;
+  // background: #000 !important;
+  // border: 2px solid #000;
+}
+.node-text {
+  color: #000;
+  font-size: 16px;
+  position: absolute;
+  height:25px;
+  transform: translate(-50%, 0);
+  line-height: 25px;
+  left: 50%;
+  margin-top:5px;
+  text-align: center;
+}
+
+::v-deep .rel-toolbar {
+  background-color: #f39930;
+  color: #ffffff;
+  .c-current-zoom {
+    color: #ffffff;
+  }
+}
+</style>