AntvG6 树图增加节点tooltip 和事件监听

1,644 阅读4分钟

AntvG6 树图需要, 悬浮节点node增加弹窗提示节点信息,需要点击节点node请求下游节点。原本打算用jsmind实现,但是jsmind不知道怎么加事件监听。

踩的坑:

  1. 节点id一定要为字符串string类型,我用接口返回的id为数字number类型显示不出来
  2. fitView() 方法,会适应视图,因为我只有几个节点,导致节点渲染特别大,所以注释了,然后就自己定义节点的坐标

渲染出来的效果 image.png

知识点:

  1. 根据接口返回树图
  2. 自定义节点样式坐标位置 (圆角)
  3. 组件监听鼠标事件 进入和点击 (mouseenter和click) 增加tooltip
  4. 组件销毁提升性能
使用antvG6 渲染树图

不想看这块代码的 可以直接看官方demo 怎么渲染树图

1.1 加载antvG6 树图一些配置公共样式 node节点、文本, 没有用fitView()方法 改了x,y坐标
// treeG6Config.js
import G6 from '@antv/g6'

const CustomNode = {
  draw: function drawShape(cfg, group) {
    const r = 12
    const color = '#5B8FF9'
    const w = cfg.size[0]
    const h = cfg.size[1]
    const shape = group.addShape('rect', {
      attrs: {
        x: w / 2 + 310,
        y: h / 2,
        width: w,
        height: h,
        stroke: color,
        radius: r,
        fill: '#fff',
      },
      name: 'main-box',
      draggable: true,
    })

    group.addShape('text', {
      attrs: {
        textBaseline: 'middle',
        x: w / 2 + 320,
        y: h + 10,
        lineHeight: 20,
        cursor: 'pointer',
        // 标签显示表名
        text: cfg.tableName,
        fontSize: 14,
        fill: '#333',
      },
      name: 'title',
    })

    if (cfg.children) {
      group.addShape('marker', {
        attrs: {
          x: w + w / 2 + 310,
          y: h,
          r: 6,
          cursor: 'pointer',
          symbol: cfg.collapsed ? G6.Marker.expand : G6.Marker.collapse,
          stroke: '#666',
          lineWidth: 1,
          fill: '#fff',
        },
        name: 'collapse-icon',
      })
    }

    return shape
  },

  setState(name, value, item) {
    if (name === 'collapsed') {
      const marker = item
        .get('group')
        .find(ele => ele.get('name') === 'collapse-icon')
      const icon = value ? G6.Marker.expand : G6.Marker.collapse
      marker.attr('symbol', icon)
    }
  },
}

// 标签文本过长换行
export function addNewlines(str, charsPerLine) {
  let result = ''
  for (let i = 0; i < str.length; i += charsPerLine) {
    result += str.substr(i, charsPerLine) + '\n'
  }
  return result
}

//  递归转换后台返回的数据 把tableId转为id字段
export const convertData = data => {
  if (data?.length) {
    data.forEach(d => {
      d.id = String(d.tableId)
      d.tableName = addNewlines(d.tableName, 12)
      if (d.children && d.children.length) {
        convertData(d.children)
      }
    })
  }
}
export default CustomNode

1.2 onMounted时候渲染dom
import {
  onMounted,
  reactive,
  watch,
  ref,
  watchEffect,
  nextTick,
  onBeforeUnmount,
} from 'vue'
import G6 from '@antv/g6'
import treeG6Config, { addNewlines, convertData } from '@/utils/treeG6Config'

// 加载公共配置 设置节点 文本
G6.registerNode('card-node', treeG6Config)

const chartDataObj = ref<any>(null) // 是一个对象啊

// 渲染dom
const initialRender = () => {
  const container = document.getElementById('container')
  const width = container.scrollWidth || 1024
  const height = container.scrollHeight || 300

  graphInstance = new G6.TreeGraph({
    container: 'container',
    width,
    height,
    plugins: [tooltip],
    modes: {
      default: ['drag-canvas'],
    },
    defaultNode: {
      type: 'card-node',
      size: [100, 40],
    },
    defaultEdge: {
      type: 'cubic-horizontal',
      style: {
        endArrow: true,
      },
    },
    layout: {
      type: 'indented',
      direction: 'LR',
      dropCap: false,
      indent: 200,
      getHeight: () => {
        return 60
      },
    },
  })

  if (typeof window !== 'undefined')
    window.onresize = () => {
      if (!graphInstance || graphInstance.get('destroyed')) return
      if (!container || !container.scrollWidth || !container.scrollHeight)
        return
      graphInstance.changeSize(container.scrollWidth, container.scrollHeight)
    }
}

onMounted(() => {
  // 只是渲染dom 没有数据
  initialRender()
})
1.3请求接口 渲染树图
const props = defineProps({
  row: {
    type: Object,
    default: () => {},
  },
})

// 渲染树图
const renderTree = () => {
  if (chartDataObj.value) {
    graphInstance.data(chartDataObj.value)
    graphInstance.render()
    // graphInstance.fitView();
  }
}

watchEffect(() => {
  if (props.row?.id) {
    // 请求接口
    getBuildBlood({
      tableId: props.row.id,
      type: branchDirectionType.value,
    }).then(res => {
      // 树图需要id字段 必须是string 类型
      rowCopy.id = String(rowCopy.id)
      rowCopy.dataBaseName = rowCopy.databases
      let data = {
        ...rowCopy,
        type: 0,
        children: [],
        tableName: addNewlines(rowCopy.tableName, 8),
      }
      // 因为接口返回的数据有[null]有这种情况
      if (res.data?.length && res.data[0] !== null) {
        convertData(res.data)
        data.children = res.data
      } else {
        data.children = []
      }
      chartDataObj.value = data
      nextTick(() => {
        renderTree()
      })
    })
  }
})
增加节点tooltip

Tooltip 插件主要用于在节点和边上展示一些辅助信息.

2.1 定义tooltip 插件
const tooltip = new G6.Tooltip({
  offsetX: 10,
  offsetY: 20,
  trigger: 'mouseenter',
  getContent(e) {
    const outDiv = document.createElement('div')
    outDiv.style.width = '270px'
    outDiv.innerHTML = `
      <ul>
        <li>
          <span style="width: 120px; display: inline-block;">表名:</span> ${
            e.item.getModel().tableName
          }</li>
        <li><span style="width: 120px; display: inline-block;">数据库名:</span>  ${
          e.item.getModel().dataBaseName || '-'
        }</li>
        <li><span style="width: 120px; display: inline-block;">创建人:</span>  ${
          e.item.getModel().creator || '-'
        }</li>
        <li><span style="width: 120px; display: inline-block;">创建时间信息:</span>  ${
          e.item.getModel().createTime || '-'
        }</li>
      </ul>`
    return outDiv
  },
  itemTypes: ['node'],
})
2.2 加载tooltip插件
  graphInstance = new G6.TreeGraph({
    container: 'container',
    // 加载插件
    plugins: [tooltip],
    ...
})

插件配置看官网

image.png

增加事件监听

通过graphInstance.on('node:click', e => {})增加事件监听,e.item._cfg可以获取节点的数据信息

  graphInstance.on('node:click', e => {
    if (e.target.get('name') === 'collapse-icon') {
      e.item.getModel().collapsed = !e.item.getModel().collapsed
      graphInstance.setItemState(
        e.item,
        'collapsed',
        e.item.getModel().collapsed
      )
      graphInstance.layout()
    }
    const item = e.item // 被操作的节点 item
    const target = e.target // 被操作的具体图形
    // 点击节点 看有没有下级节点 有就新增 没有就提示
    if (!item?._cfg?.model.children || !item?._cfg?.model.children?.length) {
      console.log('yellow----', item)
      let tableId = Number(item?._cfg?.id)
      getBuildBlood({
        tableId: tableId,
        type: branchDirectionType.value,
      }).then(res => {
        // let chartDataCopy = JSON.parse(JSON.stringify(chartDataObj.value))
        if (res.data?.length && res.data[0] !== null) {
          convertData(res.data)
          // todo 后台没有返回有下游表的 可能有问题
          setChildren(chartDataObj.value.children, item, res.data)
          // 还是不要这样改 不然就会指着上级做下游
          // if (branchDirectionType.value === 2) {
          //   setChildren(chartDataObj.value.children, item, res.data)
          // } else {
          //   chartDataObj.value = {
          //     ...res.data,
          //     children: chartDataCopy,
          //   }
          // }
        } else {
          proxy.$message({
            message: `表名${item?._cfg?.model.tableName}没有${
              branchDirectionType.value === 1 ? '上游' : '下游'
            }表信息`,
            duration: 1000,
            type: 'warning',
          })
          return
        }
        nextTick(() => {
          renderTree()
        })
      })
    }
  })

image.png

完整代码

<!--
 * @Date: 2024-04-15 14:26:26
 * @LastEditors: mingongze (andersonmingz@gmail.com)
 * @LastEditTime: 2024-04-16 17:05:34
 * @FilePath: /datagov_web/src/components/views/metadata/build/buildBlood.vue
-->
<script setup lang="ts">
import {
  onMounted,
  reactive,
  watch,
  ref,
  watchEffect,
  nextTick,
  onBeforeUnmount,
} from 'vue'
import { getBuildBlood } from '@/api/metaData'
import G6 from '@antv/g6'
import treeG6Config, { addNewlines, convertData } from '@/utils/treeG6Config'
const props = defineProps({
  row: {
    type: Object,
    default: () => {},
  },
})

const branchDirectionType = ref(2)
let rowCopy = JSON.parse(JSON.stringify(props.row))
const { proxy } = getCurrentInstance()

// 查询下游表名 找到了就给它 当作children
const setChildren = (data, item, res) => {
  data.forEach(d => {
    if (d.tableId === item?._cfg?.model.tableId) {
      d.children = res
    } else {
      if (d.children?.length) {
        setChildren(d.children, item, res)
      }
    }
  })
}
G6.registerNode('card-node', treeG6Config)

const chartDataObj = ref<any>(null) // 是一个对象啊

// TODO 这个不知道定义什么类型
let graphInstance: any = null

const renderTree = () => {
  if (chartDataObj.value) {
    graphInstance.data(chartDataObj.value)
    graphInstance.render()
    // graphInstance.fitView();
  }
}

const tooltip = new G6.Tooltip({
  offsetX: 10,
  offsetY: 20,
  trigger: 'mouseenter',
  getContent(e) {
    const outDiv = document.createElement('div')
    outDiv.style.width = '270px'
    outDiv.innerHTML = `
      <ul>
        <li>
          <span style="width: 120px; display: inline-block;">表名:</span> ${
            e.item.getModel().tableName
          }</li>
        <li><span style="width: 120px; display: inline-block;">数据库名:</span>  ${
          e.item.getModel().dataBaseName || '-'
        }</li>
        <li><span style="width: 120px; display: inline-block;">创建人:</span>  ${
          e.item.getModel().creator || '-'
        }</li>
        <li><span style="width: 120px; display: inline-block;">创建时间信息:</span>  ${
          e.item.getModel().createTime || '-'
        }</li>
      </ul>`
    return outDiv
  },
  itemTypes: ['node'],
})

const initialRender = () => {
  const container = document.getElementById('container')
  const width = container.scrollWidth || 1024
  const height = container.scrollHeight || 300

  graphInstance = new G6.TreeGraph({
    container: 'container',
    width,
    height,
    plugins: [tooltip],
    modes: {
      default: ['drag-canvas'],
    },
    defaultNode: {
      type: 'card-node',
      size: [100, 40],
    },
    defaultEdge: {
      type: 'cubic-horizontal',
      style: {
        endArrow: true,
      },
    },
    layout: {
      type: 'indented',
      direction: 'LR',
      dropCap: false,
      indent: 200,
      getHeight: () => {
        return 60
      },
    },
  })

  graphInstance.on('node:click', e => {
    if (e.target.get('name') === 'collapse-icon') {
      e.item.getModel().collapsed = !e.item.getModel().collapsed
      graphInstance.setItemState(
        e.item,
        'collapsed',
        e.item.getModel().collapsed
      )
      graphInstance.layout()
    }
    
    const item = e.item // 被操作的节点 item
    const target = e.target // 被操作的具体图形
    // 点击节点 看有没有下级节点 有就新增 没有就提示
    if (!item?._cfg?.model.children || !item?._cfg?.model.children?.length) {
      console.log('yellow----', item)
      let tableId = Number(item?._cfg?.id)
      getBuildBlood({
        tableId: tableId,
        type: branchDirectionType.value,
      }).then(res => {
        // let chartDataCopy = JSON.parse(JSON.stringify(chartDataObj.value))
        if (res.data?.length && res.data[0] !== null) {
          convertData(res.data)
          // todo 后台没有返回有下游表的 可能有问题
          setChildren(chartDataObj.value.children, item, res.data)
          // 还是不要这样改 不然就会指着上级做下游
          // if (branchDirectionType.value === 2) {
          //   setChildren(chartDataObj.value.children, item, res.data)
          // } else {
          //   chartDataObj.value = {
          //     ...res.data,
          //     children: chartDataCopy,
          //   }
          // }
        } else {
          proxy.$message({
            message: `表名${item?._cfg?.model.tableName}没有${
              branchDirectionType.value === 1 ? '上游' : '下游'
            }表信息`,
            duration: 1000,
            type: 'warning',
          })
          return
        }
        nextTick(() => {
          renderTree()
        })
      })
    }
  })

  if (typeof window !== 'undefined')
    window.onresize = () => {
      if (!graphInstance || graphInstance.get('destroyed')) return
      if (!container || !container.scrollWidth || !container.scrollHeight)
        return
      graphInstance.changeSize(container.scrollWidth, container.scrollHeight)
    }
}

onMounted(() => {
  // 只是渲染dom 没有数据
  initialRender()
})

watchEffect(() => {
  if (props.row?.id) {
    getBuildBlood({
      tableId: props.row.id,
      type: branchDirectionType.value,
    }).then(res => {
      rowCopy.id = String(rowCopy.id)
      rowCopy.dataBaseName = rowCopy.databases
      let data = {
        ...rowCopy,
        type: 0,
        children: [],
        tableName: addNewlines(rowCopy.tableName, 8),
      }
      if (res.data?.length && res.data[0] !== null) {
        convertData(res.data)
        data.children = res.data
      } else {
        data.children = []
      }
      chartDataObj.value = data
      console.log('japna', data)
      nextTick(() => {
        renderTree()
      })
    })
  }
})

onBeforeUnmount(() => {
  if (!graphInstance || graphInstance.get('destroyed')) return
  graphInstance.destroy()
  // console.log("ss", graphInstance);
})
</script>
<template>
  <div>
    <el-select
      v-model="branchDirectionType"
      clearable
      placeholder="请选择"
      class="!w-[300px] mb-3"
    >
      <el-option label="下游表信息" :value="2"></el-option>
      <el-option label="上游表信息" :value="1"></el-option>
    </el-select>
    <div class="jsmind pt-[190px]">
      <div
        id="container"
        ref="mindRef"
        class="overflow-y-auto overflow-x-hidden"
      ></div>
    </div>
  </div>
</template>
<style lang="less" scoped>
.jsmind {
  width: 1024px;
  height: 400px;
  border: solid 1px #ccc;
}
</style>

退出页面销毁组件

image.png 参考链接

  1. g6.antv.vision/api/Plugins…
  2. g6.antv.vision/zh/examples…
  3. g6-v3-2.antv.vision/zh/examples…