AntvG6使用实践

1,016 阅读3分钟

项目介绍:

Vue3 + ts

antv/g6官网地址:g6.antv.antgroup.com/examples

实现效果:

image.png 首先:

pnpm install --save @antv/g6
//样式我这里额外使用了 inset-css,大家看情况参考
pnpm install --save @insert-css

直接上代码:

vue组件
<script setup lang="ts">
import G6, { TreeGraph } from '@antv/g6'
import insertCss from 'insert-css'import useRegister from './useRegister'
import { transTree, iconMap } from './data'
import Drawer from './Drawer.vue'const props = defineProps({
  node: {
    type: Object,
    default: () => ({})
  },
  name: {
    type: String,
    default: ''
  },
  envId: {
    type: Number,
    default: 0
  }
})
​
// 工具栏样式
insertCss(`
.g6-toolbar-ul {
    width: 42px;
    height: 96px;
    position: absolute;
    bottom: 20px;
    left: 20px;
    background: #ffffff;
    padding-top: 15px;
    padding-bottom: 15px;
    box-shadow: 0px 1px 3px -2px rgba(7, 88, 202, 0.14),
      0px 3px 8px 0px rgba(7, 88, 202, 0.1),
      0px 5px 16px 4px rgba(7, 88, 202, 0.06);
    border-radius: 21px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: space-between;
    cursor: pointer;
  }
  .g6-toolbar-ul .first::after {
    content: '';
    display: block;
    width: 24px;
    height: 1px;
    background: #e8e8e8;
    padding-left: 2px;
  }
  .g6-toolbar-ul .second {
    margin-top: 10px;
  }
`)
​
// tooltip 样式
insertCss(`
  .g6-component-tooltip {
    background-color: rgba(0,0,0, 0.65);
    padding: 10px;
    box-shadow: rgb(174, 174, 174) 0px 0px 10px;
    width: fit-content;
    color: #fff;
    border-radius = 4px;
  }
`)
​
// 画布实例和挂载容器
let graph: TreeGraph
let container: HTMLElement | null
const first = ref<boolean>(true)
​
// 基础配置
const visible = ref(false)
const currentNode = ref<{ [key: string]: any }>({
  type: '',
  label: '',
  tooltip: ''
})
​
const url =
  'ws://' +
  import.meta.env.VITE_MANAGE_API.replace('http://', '') +
  'service/ws/topology'onMounted(() => {
  container = document.getElementById('container')
  getData()
})
​
// 获取数据
const getData = () => {
  let nodeData = []
  const { status, data, send, open, close } = useWebSocket(url, {
    autoReconnect: {
      retries: 3,
      delay: 1000
      // onFailed() {
      //   alert('Failed to connect topology after 3 retries')
      // }
    },
    onMessage: (ws, e) => {
      nodeData = transTree(JSON.parse(e.data))
      G6.Util.traverseTree(nodeData[0], subtree => {
        if (subtree.level === undefined) subtree.level = 0
        subtree.children?.forEach(child => (child.level = subtree.level + 1))
        switch (subtree.level) {
          case 0:
            subtree.type = 'root'
            break
          case 1:
            subtree.type = 'treeNode'
            break
          default:
            subtree.type = 'treeNode'
        }
      })
      if (first.value) {
        // 生成画布
        createGraph(nodeData[0])
        first.value = false
      } else {
        // 更新画布
        graph.changeData(nodeData[0])
      }
    }
  })
  send(JSON.stringify({ envId: Number(props.envId), serviceName: props.name }))
}
​
const toolbar = new G6.ToolBar({
  className: 'g6-toolbar-ul',
  getContent: () => {
    return `
      <ul>
        <li class="li-style first"  code='zoomOut'>
          <svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
            <path d="M658.432 428.736a33.216 33.216 0 0 1-33.152 33.152H525.824v99.456a33.216 33.216 0 0 1-66.304 0V461.888H360.064a33.152 33.152 0 0 1 0-66.304H459.52V296.128a33.152 33.152 0 0 1 66.304 0V395.52H625.28c18.24 0 33.152 14.848 33.152 33.152z m299.776 521.792a43.328 43.328 0 0 1-60.864-6.912l-189.248-220.992a362.368 362.368 0 0 1-215.36 70.848 364.8 364.8 0 1 1 364.8-364.736 363.072 363.072 0 0 1-86.912 235.968l192.384 224.64a43.392 43.392 0 0 1-4.8 61.184z m-465.536-223.36a298.816 298.816 0 0 0 298.432-298.432 298.816 298.816 0 0 0-298.432-298.432A298.816 298.816 0 0 0 194.24 428.8a298.816 298.816 0 0 0 298.432 298.432z"></path>
          </svg>
        </li>
        <li  class="li-style second"  code='zoomIn'>
          <svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
            <path d="M639.936 416a32 32 0 0 1-32 32h-256a32 32 0 0 1 0-64h256a32 32 0 0 1 32 32z m289.28 503.552a41.792 41.792 0 0 1-58.752-6.656l-182.656-213.248A349.76 349.76 0 0 1 480 768 352 352 0 1 1 832 416a350.4 350.4 0 0 1-83.84 227.712l185.664 216.768a41.856 41.856 0 0 1-4.608 59.072zM479.936 704c158.784 0 288-129.216 288-288S638.72 128 479.936 128a288.32 288.32 0 0 0-288 288c0 158.784 129.216 288 288 288z" p-id="3853"></path>
          </svg>
        </li>
      </ul>
    `
  },
  handleClick: (code, graph) => {
    if (code === 'zoomOut') {
      toolbar.zoomOut()
    } else if (code === 'zoomIn') {
      toolbar.zoomIn()
    }
  }
})
​
const tooltip = new G6.Tooltip({
  offsetX: 20,
  offsetY: 30,
  // 允许出现 tooltip 的 item 类型
  itemTypes: ['node'],
  // 自定义 tooltip 内容
  getContent: e => {
    const outDiv = document.createElement('div')
    const nodeName = e?.item?.getModel().name
    const tag = e?.item?.getModel().namespace
    outDiv.innerHTML = `${nodeName}<br/>${tag || ''} `
    return outDiv
  },
  shouldBegin: e => {
    if (e?.target.get('name') === 'label-shape') return true
    return false
  }
})
​
const createGraph = data => {
  useRegister(G6)
​
  const width = container?.scrollWidth || 1000
  const height = container?.scrollHeight || 500
  graph = new G6.TreeGraph({
    container: 'container',
    width,
    height,
    fitView: true,
    enabledStack: true,
    plugins: [toolbar, tooltip],
    modes: {
      // default: ['zoom-canvas', 'drag-canvas']
      default: ['drag-canvas']
    },
    defaultNode: {
      // type: 'treeNode',
      anchorPoints: [
        [0, 0.5],
        [1, 0.5]
      ]
    },
    defaultEdge: {
      type: 'cubic-horizontal',
      style: {
        radius: 10,
        offset: 30
      }
    },
    layout: {
      type: 'compactBox',
      direction: 'LR',
      // 节点ID
      getId: function getId(d: { uid: any }) {
        return d.uid
      },
      // 节点高度
      getHeight: function getHeight() {
        return 60
      },
      // 节点宽度
      getWidth: function getWidth() {
        return 200
      },
      // // 节点垂直间隙
      getVGap: function getVGap() {
        return 15
      },
      // 节点水平间隙
      getHGap: function getHGap() {
        return 30
      }
    }
  })
  //先清除图表,再重新渲染
  graph.clear()
  graph.data(data)
  graph.render()
  graph.fitView()
​
  //监听collapse点击
  graph.on('collapse-text:click', e => {
    handleCollapse(e)
  })
  graph.on('collapse-back:click', e => {
    handleCollapse(e)
  })
  //点击展开弹窗
  graph.on('node:click', e => {
    const item = e.item // 被操作的节点 item
    const target = e.target // 被操作的具体图形
    if (
      target.get('name') === 'collapse-back' ||
      target.get('name') === 'collapse-text'
    )
      return
    const model = item?.getModel()
    //@ts-ignore
    currentNode.value = model || null
    if (currentNode.value.type !== 'root') visible.value = true
  })
}
​
const handleCollapse = e => {
  const target = e.target
  const uid = target.get('modelId')
  const item = graph.findById(uid)
  const nodeModel = item.getModel()
  nodeModel.collapsed = !nodeModel.collapsed
  graph.layout()
  //@ts-ignore
  graph.setItemState(item, 'collapse', nodeModel.collapsed)
}
​
// 适配屏幕宽度
if (typeof window !== 'undefined') {
  window.onresize = () => {
    if (!graph || graph.get('destroyed')) return
    if (!container || !container.scrollWidth || !container.scrollHeight) return
    graph.changeSize(container.scrollWidth, container.scrollHeight)
  }
}
​
provide('currentNode', currentNode)
provide('serviceName', props.name)
provide('envId', props.envId)
</script>
<template>
  <div class="diagram">
    <div id="container" ref="container"></div>
  </div>
  <el-drawer
    v-model="visible"
    direction="rtl"
    size="60%"
    destroy-on-close
    append-to-body
  >
    //弹窗组件
    <Drawer />
  </el-drawer>
</template>
<style lang="scss" scoped>
.diagram {
  position: relative;
  width: 100%;
  height: 550px;
  margin-top: 10px;
  background-color: #f7faff;
  border-radius: 8px;
}
​
.headerImg {
  display: inline-block;
  vertical-align: bottom;
  margin-left: -8px;
  margin-right: 8px;
}
.headerText {
  font-size: 18px;
  font-weight: 600;
  color: #323640;
}
.tag {
  display: inline-block;
  padding: 4px;
  margin-right: 4px;
  background: #f7faff;
  border-radius: 2px;
  border: 1px solid #b3d8ff;
  color: #4290ff;
}
</style>
自定义钩子

主要用来定义节点样式:

import root from '@/assets/tuopu/root.png'
import heartG from '@/assets/tuopu/heart.png'
import heartR from '@/assets/tuopu/heartR.png'
import process from '@/assets/tuopu/process.png'
import leftArrow from '../img/leftArrow.png'
import rightArrow from '../img/rightArrow.png'
//一些图片的导入 可忽略
import { iconMap, kindMap } from './data'
​
export default G6 => {
  //treeNode类型节点
  G6.registerNode('treeNode', {
    // 绘制节点,包含文本
    /*
     * @param  {Object} cfg 节点的配置项  节点或边的配置项
     * @param  {G.Group} group 图形分组,节点中图形对象的容器
     * @return {G.Shape} 返回一个绘制的图形作为 keyShape,通过 node.get('keyShape') 可以获取。
     */
    draw: (cfg, group) => {
      const size = [200, 56]
      const keyShape = group.addShape('rect', {
        attrs: {
          width: size[0],
          height: size[1],
          stroke: '#D7DAE2',
          lineWidth: 0.5,
          x: -size[0] / 2,
          y: -size[1] / 2,
          fill: '#fff',
          radius: 4
        },
        draggable: true,
        name: 'root-keyshape'
      })
      group.addShape('rect', {
        attrs: {
          width: 35,
          height: size[1],
          x: -size[0] / 2,
          y: -size[1] / 2,
          fill: '#EFF5FE',
          radius: 4
        },
        draggable: true,
        name: 'root-keyshape'
      })
      group.addShape('rect', {
        attrs: {
          width: 10,
          height: size[1],
          x: -size[0] / 2 + 25,
          y: -size[1] / 2,
          fill: '#EFF5FE'
        },
        draggable: true,
        name: 'root-keyshape'
      })
      group.addShape('image', {
        attrs: {
          x: -size[0] / 2 + 8,
          y: -10,
          width: 20,
          height: 20,
          img: iconMap.get(cfg.kind)
        },
        name: 'image-shape'
      })
      group.addShape('text', {
        attrs: {
          text:
            cfg.name.length > 10 ? cfg.name.substr(0, 10) + '...' : cfg.name,
          fill: '#000',
          fontSize: 10,
          x: -size[0] / 2 + 45,
          y: -5
        },
        cursor: 'pointer',
        name: 'label-shape'
      })
      group.addShape('image', {
        attrs: {
          x: -size[0] / 2 + 45,
          y: 0,
          width: 18,
          height: 18,
          img:
            cfg.health === 'Processing'
              ? process
              : cfg.health === 'Healthy'
              ? heartG
              : cfg.health === 'Error'
              ? heartR
              : ''
        },
        name: 'image1-shape'
      })
      group.addShape('text', {
        attrs: {
          text: kindMap.get(cfg.kind),
          fill: '#4290FF',
          fontSize: 10,
          x: cfg.health ? -35 : -55,
          y: 16
        },
        cursor: 'pointer',
        name: 'text-shape'
      })
      // day tag
      group.addShape('rect', {
        attrs: {
          x:
            cfg.ago.length < 8
              ? size[0] / 2 - 50
              : cfg.ago.length < 13
              ? size[0] / 2 - 75
              : size[0] / 2 - 85,
          y: -size[1] / 2 - 10,
          width: cfg.ago.length < 9 ? 50 : cfg.ago.length < 13 ? 68 : 80,
          height: 16,
          stroke: '#D7DAE2',
          lineWidth: 0.5,
          fill: '#F7FAFF',
          radius: 2
        },
        name: 'rect-tag'
      })
      group.addShape('text', {
        attrs: {
          text: cfg.ago,
          fill: '#9098A9',
          fontSize: 10,
          x:
            cfg.ago.length < 8
              ? size[0] / 2 - 45
              : cfg.ago.length < 13
              ? size[0] / 2 - 68
              : size[0] / 2 - 82,
          y: -size[1] / 2 + 4
        },
        cursor: 'pointer',
        name: 'text-tag'
      })
      // collapse circle
      if (cfg.children && cfg.children.length) {
        group.addShape('circle', {
          attrs: {
            x: size[0] / 2,
            y: 0,
            stroke: '#D7DAE2',
            lineWidth: 0.5,
            r: 8,
            cursor: 'pointer',
            fill: '#fff'
          },
          name: 'collapse-back',
          modelId: cfg.uid
        })
​
        // collpase text
        group.addShape('image', {
          attrs: {
            x: size[0] / 2 - 6,
            y: -6,
            width: 12,
            height: 12,
            img: cfg.collapsed ? rightArrow : leftArrow,
            cursor: 'pointer'
          },
          name: 'collapse-text',
          modelId: cfg.uid
        })
      }
      return keyShape
    },
    setState(name, value, item) {
      const size = [200, 56]
      if (name === 'collapse') {
        const group = item.getContainer()
        const collapseText = group.find(e => e.get('name') === 'collapse-text')
        if (collapseText) {
          if (!value) {
            collapseText.attr({
              img: leftArrow,
              x: size[0] / 2 - 6
            })
          } else {
            collapseText.attr({
              img: rightArrow,
              x: size[0] / 2 - 5
            })
          }
        }
      }
    }
  })
​
  // root node 类型节点
  G6.registerNode('root', {
    draw: (cfg, group) => {
      const size = [180, 56]
      const keyShape = group.addShape('rect', {
        attrs: {
          width: size[0],
          height: size[1],
          stroke: '#D7DAE2',
          lineWidth: 0.5,
          x: -size[0] / 2,
          y: -size[1] / 2,
          fill: '#fff',
          radius: 4
        },
        draggable: true,
        name: 'root-keyshape'
      })
      group.addShape('circle', {
        attrs: {
          x: -size[0] / 2,
          y: 0,
          stroke: '#D7DAE2',
          lineWidth: 0.5,
          r: 30,
          fill: '#fff'
        },
        name: 'circle-bg'
      })
      group.addShape('circle', {
        attrs: {
          x: -size[0] / 2 + 7,
          y: 0,
          r: 28,
          fill: '#fff'
        },
        name: 'circle-bg2'
      })
      group.addShape('circle', {
        attrs: {
          x: -size[0] / 2,
          y: 0,
          r: 26,
          fill: '#EFF5FE'
        },
        name: 'circle-bg2'
      })
      group.addShape('image', {
        attrs: {
          x: -size[0] / 2 - 12,
          y: -size[1] / 2 + 16,
          width: 24,
          height: 24,
          img: root
        },
        name: 'image-shape'
      })
      group.addShape('text', {
        attrs: {
          text:
            cfg.name.length > 10 ? cfg.name.substr(0, 10) + '...' : cfg.name,
          fill: '#000',
          fontSize: 10,
          x: -size[0] / 2 + 35,
          y: -5
        },
        cursor: 'pointer',
        name: 'label-shape'
      })
      // collapse circle
      if (cfg.children && cfg.children.length) {
        group.addShape('circle', {
          attrs: {
            x: size[0] / 2,
            y: 0,
            stroke: '#D7DAE2',
            lineWidth: 0.5,
            r: 8,
            cursor: 'pointer',
            fill: '#fff'
          },
          name: 'collapse-back',
          modelId: cfg.uid
        })
​
        // collpase text
        group.addShape('image', {
          attrs: {
            x: size[0] / 2 - 5,
            y: -6,
            width: 12,
            height: 12,
            img: cfg.collapsed ? rightArrow : leftArrow,
            cursor: 'pointer'
          },
          name: 'collapse-text',
          modelId: cfg.uid
        })
      }
      return keyShape
    },
    //设置收缩节点变化
    setState(name, value, item) {
      const size = [180, 56]
      if (name === 'collapse') {
        const group = item.getContainer()
        const collapseText = group.find(e => e.get('name') === 'collapse-text')
        if (collapseText) {
          if (!value) {
            collapseText.attr({
              img: leftArrow,
              x: size[0] / 2 - 6
            })
          } else {
            collapseText.attr({
              img: rightArrow,
              x: size[0] / 2 - 5
            })
          }
        }
      }
    }
  })
}
数据格式
const defaultData = {
  uid: '001',
  id: '001',
  version: 'v1',
  group: 'apps',
  images: ['nginx:1.7.9'],
  name: 'xxxxx',
  health: 'Healthy',
  state: 'Running',
  ago: '1 day',
  children: [
    {
      uid: '002',
      id: '002',
      kind: 'Deployment',
      version: 'v1',
      group: 'apps',
      namespace: 'default',
      images: ['nginx:1.7.9'],
      name: 'xxxxx',
      health: 'Healthy',
      state: 'Running',
      ago: '1 day',
      children: [
        {
          uid: '003',
          id: '003',
          kind: 'Pod',
          version: 'v1',
          group: 'apps',
          namespace: 'default',
          images: ['nginx:1.7.9'],
          name: 'xxxxx',
          health: 'Healthy',
          state: 'Running',
          ago: '2 days',
          children: [
            {
              uid: '004',
              id: '004',
              kind: 'Pod',
              version: 'v1',
              group: 'apps',
              namespace: 'default',
              images: ['nginx:1.7.9'],
              name: 'xxxxx',
              health: 'Healthy',
              state: 'Running',
              ago: '45 minutes'
            },
            {
              uid: '005',
              id: '005',
              kind: 'Pod',
              version: 'v1',
              group: 'apps',
              namespace: 'default',
              images: ['nginx:1.7.9'],
              name: 'xxxxx',
              health: 'Healthy',
              state: 'Running',
              ago: '59 seconds'
            }
          ]
        },
        {
          uid: '006',
          id: '006',
          kind: 'Pod',
          version: 'v1',
          group: 'apps',
          namespace: 'default',
          images: ['nginx:1.7.9'],
          name: 'xxxxx',
          health: 'Healthy',
          state: 'Running',
          ago: '10 months'
        }
      ]
    },
    {
      uid: '007',
      id: '007',
      kind: 'Deployment',
      version: 'v1',
      group: 'apps',
      namespace: 'default',
      images: ['nginx:1.7.9'],
      name: 'xxxxx',
      health: 'Healthy',
      state: 'Running',
      ago: '1 week'
    }
  ]
}