Vue3使用 DAG 图(AntV X6)

参考文档

可自定义设置以下属性

  • 容器宽度(width),类型:number | string,默认 '100%'
  • 容器高度(height),类型:number | string,默认 '100%'

效果如下图:

安装插件

bash 复制代码
pnpm add @antv/x6

创建DAG组件DAGChart.vue

ts 复制代码
<script setup lang="ts">
import type { Edge, Graph as X6Graph, Node } from '@antv/x6'
import { Graph } from '@antv/x6'
import { useResizeObserver, debounce } from 'vue-amazing-ui'

interface Props {
  width?: string | number // 容器宽度
  height?: string | number // 容器高度
}
const props = withDefaults(defineProps<Props>(), {
  width: '100%',
  height: '100%'
})
const chartWidth = computed(() => {
  if (typeof props.width === 'number') {
    return `${props.width}px`
  }
  return props.width
})
const chartHeight = computed(() => {
  if (typeof props.height === 'number') {
    return `${props.height}px`
  }
  return props.height
})

const chartRef = useTemplateRef('chartRef')
let graph: X6Graph | null = null

type DagNodeData = {
  label: string
  type: 'source' | 'transform' | 'sink' | 'hello' | string
}

function getDefaultNodePorts() {
  return {
    groups: {
      in: {
        position: 'left',
        zIndex: 1,
        attrs: {
          circle: { r: 4, magnet: false, stroke: '#5F95FF', strokeWidth: 1, fill: '#fff' }
        }
      },
      out: {
        position: 'right',
        zIndex: 1,
        attrs: {
          circle: { r: 4, magnet: false, stroke: '#52c41a', strokeWidth: 1, fill: '#fff' }
        }
      }
    },
    items: [
      { id: 'in-1', group: 'in' },
      { id: 'out-1', group: 'out' }
    ]
  }
}

function getGraphData() {
  const nodes: Node.Metadata[] = [
    {
      id: 'source-1',
      shape: 'rect', // 节点图形 'rect' | 'circle' | 'ellipse' | 'polygon' | 'polyline' | 'path' | 'image' | 'html' (HTML 节点,使用 foreignObject 渲染 HTML 片段)
      x: 80, // 节点位置 x 坐标,单位为 px
      y: 140, // 节点位置 y 坐标,单位为 px
      width: 140, // 节点宽度,单位为 px
      height: 42, // 节点高度,单位为 px
      angle: 0, // 节点旋转角度,单位为度
      data: { label: 'Source: Kafka', type: 'source' } as DagNodeData,
      attrs: {
        body: { fill: '#e6f4ff', stroke: '#91caff', rx: 6, ry: 6 },
        label: { text: 'Source: Kafka', fill: '#1f1f1f' }
      },
      ports: getDefaultNodePorts()
    },
    {
      id: 'transform-1',
      shape: 'rect',
      x: 340,
      y: 120,
      width: 160,
      height: 42,
      data: { label: 'Transform: Clean', type: 'transform' } as DagNodeData,
      attrs: {
        body: { fill: '#f6ffed', stroke: '#b7eb8f', rx: 6, ry: 6 },
        label: { text: 'Transform: Clean', fill: '#1f1f1f' }
      },
      ports: getDefaultNodePorts()
    },
    {
      id: 'transform-2',
      shape: 'rect',
      x: 340,
      y: 200,
      width: 160,
      height: 42,
      data: { label: 'Transform: Enrich', type: 'transform' } as DagNodeData,
      attrs: {
        body: { fill: '#f6ffed', stroke: '#b7eb8f', rx: 6, ry: 6 },
        label: { text: 'Transform: Enrich', fill: '#1f1f1f' }
      },
      ports: getDefaultNodePorts()
    },
    {
      id: 'sink-1',
      shape: 'rect',
      x: 620,
      y: 160,
      width: 140,
      height: 42,
      data: { label: 'Sink: ClickHouse', type: 'sink' } as DagNodeData,
      attrs: {
        body: { fill: '#fff7e6', stroke: '#ffadd2', rx: 6, ry: 6 },
        label: { text: 'Sink: ClickHouse', fill: '#1f1f1f' }
      },
      ports: getDefaultNodePorts()
    },
    {
      id: 'hello-1',
      shape: 'rect',
      x: 880,
      y: 60,
      width: 140,
      height: 42,
      data: { label: 'Hello: World', type: 'hello' } as DagNodeData,
      attrs: {
        body: { fill: '#fff0f6', stroke: '#ffadd2', rx: 6, ry: 6 },
        label: { text: 'Hello: World', fill: '#1f1f1f' }
      },
      ports: getDefaultNodePorts()
    },
    {
      id: 'hello-2',
      shape: 'rect',
      x: 880,
      y: 160,
      width: 140,
      height: 42,
      data: { label: 'Hello: Forest', type: 'hello' } as DagNodeData,
      attrs: {
        body: { fill: '#fff0f6', stroke: '#ffadd2', rx: 6, ry: 6 },
        label: { text: 'Hello: Forest', fill: '#1f1f1f' }
      },
      ports: getDefaultNodePorts()
    },
    {
      id: 'hello-3',
      shape: 'rect',
      x: 880,
      y: 260,
      width: 140,
      height: 42,
      data: { label: 'Hello: Sea', type: 'hello' } as DagNodeData,
      attrs: {
        body: { fill: '#fff0f6', stroke: '#ffadd2', rx: 6, ry: 6 },
        label: { text: 'Hello: Sea', fill: '#1f1f1f' }
      },
      ports: getDefaultNodePorts()
    }
  ]

  const edges: Edge.Metadata[] = [
    {
      id: 'edge-1',
      source: { cell: 'source-1', port: 'out-1' },
      target: { cell: 'transform-1', port: 'in-1' },
      connector: { name: 'smooth' }
    },
    {
      id: 'edge-2',
      source: { cell: 'source-1', port: 'out-1' },
      target: { cell: 'transform-2', port: 'in-1' },
      connector: { name: 'smooth' }
    },
    {
      id: 'edge-3',
      source: { cell: 'transform-1', port: 'out-1' },
      target: { cell: 'sink-1', port: 'in-1' },
      connector: { name: 'smooth' }
    },
    {
      id: 'edge-4',
      source: { cell: 'transform-2', port: 'out-1' },
      target: { cell: 'sink-1', port: 'in-1' },
      connector: { name: 'smooth' }
    },
    {
      id: 'edge-5',
      source: { cell: 'sink-1', port: 'out-1' },
      target: { cell: 'hello-1', port: 'in-1' },
      connector: { name: 'smooth' }
    },
    {
      id: 'edge-6',
      source: { cell: 'sink-1', port: 'out-1' },
      target: { cell: 'hello-2', port: 'in-1' },
      connector: { name: 'smooth' }
    },
    {
      id: 'edge-7',
      source: { cell: 'sink-1', port: 'out-1' },
      target: { cell: 'hello-3', port: 'in-1' },
      connector: { name: 'smooth' }
    }
  ]

  return { nodes, edges }
}

function initGraph() {
  if (!chartRef.value) return
  graph = new Graph({
    container: chartRef.value,
    grid: {
      visible: true, // 绘制网格,默认绘制 dot 类型网格
      size: 10, // 网格大小,单位 px
      type: 'dot', // 网格类型,可选 'dot' | 'fixedDot' | 'mesh' | 'doubleMesh' | ''
      args: {
        color: '#ddd', // 网点颜色
        thickness: 1 // 网点大小
      }
    },
    background: { color: '#fafafa' }, // 背景
    panning: true, // 是否可以拖拽平移
    mousewheel: true, // 鼠标滚轮缩放
    interacting: { nodeMovable: true, edgeMovable: false, vertexMovable: false, edgeLabelMovable: false },
    autoResize: true // 监听容器大小改变,并自动更新画布大小
  })

  // 点击事件
  graph.on('node:click', ({ node }) => {
    const data = (node.getData ? node.getData() : node.getData) as DagNodeData | undefined
    console.log(`节点被点击:${data?.label}`)
  })
  const data = getGraphData()
  graph.fromJSON(data) // 渲染元素
  fitView()
  centerView()
  useResizeObserver(
    chartRef,
    debounce(() => {
      console.log('centerView')
      fitView()
      centerView()
    }, 100) as ResizeObserverCallback
  )
}
// 将画布缩放级别增加 0.1
function zoomIn() {
  graph?.zoom(0.1)
}
// 将画布缩放级别减少 0.1
function zoomOut() {
  graph?.zoom(-0.1)
}
// 将画布中元素缩小或者放大一定级别,让画布正好容纳所有元素,可以通过 maxScale 配置最大缩放级别
function fitView() {
  graph?.zoomToFit({ padding: 20, maxScale: 1 })
}
// 将画布中元素居中展示
function centerView() {
  graph?.centerContent()
}
onMounted(() => {
  initGraph()
})
onBeforeUnmount(() => {
  graph?.dispose()
  graph = null
})
</script>
<template>
  <div class="dag-container">
    <div
      ref="chartRef"
      class="graph-container"
      :style="`--chart-width: ${chartWidth}; --chart-height: ${chartHeight};`"
    ></div>
  </div>
</template>
<style lang="less" scoped>
.dag-container {
  .graph-container {
    width: var(--chart-width) !important;
    height: var(--chart-height) !important;
    padding: 16px 24px;
    border-radius: 4px;
  }
}
</style>

在要使用的页面引入

ts 复制代码
<script setup lang="ts">
import DAGChart from './DAGChart.vue'
</script>
<template>
  <div>
    <h1>DAGChart 参考文档</h1>
    <ul class="m-list">
      <li>
        <a class="u-file" href="https://x6.antv.antgroup.com/tutorial/about" target="_blank">AntV X6 文档</a>
      </li>
    </ul>
    <DAGChart :height="500" />
  </div>
</template>