这是我实际开发项目中,利用 X6 开发的一个关系图。具备连线功能。这里我尽可能全的记录整个开发思路和部分编码,如果你也用了 X6 希望对你有帮助。

补充下我当时使用的版本(现在官网已经到 2.x 了,建议用新版,实现思路都是一样的):

"@antv/x6": "^1.30.0",
"@antv/layout": "^0.1.31"

创建画布

代码有删减,以下展示的代码全都有删减
index.vue

<template>
  <div id="dag-view"></div>
</template>

<script>
import { Graph } from '@antv/x6'
import { GraphCells } from './GraphCells'
export default {
  name: 'index',
  data () {
    return {
      graph: null,
      graphCells: new GraphCells()
    }
  },
  methods: {
    init () {
      this.graphCells.clear()
      this.graph = new Graph(this.graphCells.graphOptions({}))
      this.graphCells.graph = this.graph
    }
  },
  mounted () {
    this.init()
  }
}
</script>

GraphCells.js

class GraphCells {
  constructor () {
    this.graph = null
    this.cells = new Map()
    this.edges = []
  }

  clear () {
    this.graph = null
    this.cells.clear()
    this.edges = []
  }

  graphOptions (options) {
    return {
      container: document.getElementById('dag-view'),
      autoResize: true,
      // 是否可以拖动
      panning: false,
      grid: {
        size: 10,
        type: 'dot', // 'dot' | 'fixedDot' | 'mesh'
        visible: true,
        args: {
          color: '#a0a0a0', // 网格线/点颜色
          thickness: 1 // 网格线宽度/网格点大小
        }
      },
      highlighting: {
        magnetAvailable: {
          name: 'stroke',
          args: {
            attrs: {
              stroke: '#47C769'
            }
          }
        },
        magnetAdsorbed: {
          name: 'stroke',
          args: {
            attrs: {
              fill: '#fff',
              stroke: '#31d0c6'
            }
          }
        }
      },
      mousewheel: {
        enabled: true,
        modifiers: 'ctrl',
        factor: 1.1,
        maxScale: 1.5,
        minScale: 0.5
      },
      scroller: {
        enabled: true,
        pageVisible: false,
        pageBreak: false,
        pannable: true
      },
      connecting: {
        // 当 snap 设置为 true 时连线的过程中距离节点或者连接桩 50px 时会触发自动吸附
        snap: true,
        // 是否允许连接到画布空白位置的点
        allowBlank: false,
        // 是否允许创建循环连线
        allowLoop: false,
        // 拖动边时,是否高亮显示所有可用的连接桩或节点
        highlight: true
      },
      ...options
    }
  }
  
  ...
}

export {
  GraphCells
}

创建节点

DagNode.js

import { Node, ObjectExt, Dom } from '@antv/x6'

class DagNode extends Node {
  constructor (options) {
    super(options)
    this.options = options
  }
}

DagNode.config({
  zIndex: 2,
  width: 100,
  height: 28,
  markup: [
    {
      tagName: 'rect',
      selector: 'body'
    },
    {
      // 使用 foreignObject 渲染 HTML 片段
      tagName: 'foreignObject',
      attrs: {
      },
      children: [
        {
          // 当 tagName 指定的标签是 HTML 元素时,需要使用 HTML 元素的命名空间
          ns: Dom.ns.xhtml,
          tagName: 'body',
          attrs: {
            xmlns: Dom.ns.xhtml
          },
          style: {
            display: 'table-cell',
            // 设置上左和下左边框radius。在svg元素中很难做到一侧radius,所以这里选择html元素
            borderTopLeftRadius: '4px',
            borderBottomLeftRadius: '4px',
            // 背景颜色(下面介绍怎么动态设置)
            backgroundColor: '',
            textAlign: 'center',
            verticalAlign: 'middle',
            height: 28,
            padding: '0 5px'
          },
          children: [
            {
              tagName: 'i',
              attrs: {
                // 设置图标字体class
                class: 'iconfont iconzirenwu'
              },
              style: {
                fontSize: '16px',
                // 图标颜色(下面介绍怎么动态设置)
                color: ''
              }
            }
          ]
        }
      ]
    },
    {
      tagName: 'text',
      selector: 'label'
    }
  ],
  attrs: {
    body: {
      refWidth: '100%',
      refHeight: '100%',
      strokeWidth: 0,
      fill: '#ffffff',
      // stroke: '#5F95FF',
      rx: 4,
      ry: 4,
      filter: {
        name: 'highlight',
        args: {
          color: '#BFBFBF',
          width: 2,
          blur: 2,
          opacity: 0.5
        }
      }
    },
    label: {
      textWrap: {
        // text: 'lorem ipsum dolor lorem ipsum dolor lorem ipsum dolor',
        ellipsis: true,
        breakWord: true,
        width: -35
      },
      textAnchor: 'middle',
      textVerticalAnchor: 'middle',
      refX: 60,
      refY: '50%',
      refWidth: '80%',
      fontSize: 12,
      fill: '#333'
    }
  },
  // 定义连接桩
  ports: {
    groups: {
      right: {
        position: { name: 'right' },
        zIndex: 2,
        attrs: {
          portBody: {
            magnet: true,
            r: 3
          }
        }
      },
      in: {
        position: { name: 'left' },
        zIndex: 2,
        attrs: {
          portBody: {
            magnet: false,
            r: 0
          }
        }
      },
      out: {
        position: { name: 'right' },
        zIndex: 2,
        attrs: {
          portBody: {
            magnet: false,
            r: 0
          }
        }
      }
    }
  },
  portMarkup: {
    tagName: 'circle',
    selector: 'portBody',
    attrs: {
      fill: '#fff',
      stroke: '#F08BB4',
      strokeWidth: 1
    }
  },
  propHooks (metadata) {
    const { label, ...others } = metadata
    if (label) {
      ObjectExt.setByPath(others, 'attrs/label/textWrap/text', label)
    }
    return others
  }
})

Node.registry.register('dag-node', DagNode, true)

export default DagNode

在 index.vue 中引,并添加两个节点到画布中

<template>
  <div id="dag-view"></div>
</template>

<script>
import { Graph } from '@antv/x6'
import { GraphCells } from './GraphCells'
// 注册自定义节点
import './DagNode'
export default {
  name: 'index',
  data () {
    return {
      graph: null,
      graphCells: new GraphCells()
    }
  },
  methods: {
    init () {
      this.graphCells.clear()
      this.graph = new Graph(this.graphCells.graphOptions({}))
      this.graphCells.graph = this.graph
      // 添加节点
      this.graph.addNode({
        shape: 'dag-node',
        x: 100,
        y: 100,
        label: 'foofoo' // 显示文本。这样写归功于自定义节点中的propHooks
      })

      this.graph.addNode({
        shape: 'dag-node',
        x: 300,
        y: 100,
        label: 'barbar'
      })
    }
  },
  mounted () {
    this.init()
  }
}
</script>

显示效果:
在这里插入图片描述

创建连线

DagEdge.js

import { Shape, Edge } from '@antv/x6'

/* 连线1 */
class CommonEdge extends Shape.Edge {
  // ...
}
CommonEdge.config({
  zIndex: 1,
  router: {
    name: 'er',
    args: {
      offset: 24,
      direction: 'H'
    }
  },
  connector: 'rounded',
  connectionPoint: 'boundary',
  attrs: {
    line: {
      stroke: '#BFBFBF',
      strokeWidth: 1,
      targetMarker: null
    }
  }
})
Edge.registry.register('common-edge', CommonEdge, true)

export {
  CommonEdge
}

在 index.vue 中引入 DagEdge,并添加连线

<template>
  <div id="dag-view"></div>
</template>

<script>
import { Graph } from '@antv/x6'
import { GraphCells } from './GraphCells'
// 注册自定义节点
import './DagNode'
// 引入自定义连线  
import './DagEdge'  
export default {
  name: 'index',
  data () {
    return {
      graph: null,
      graphCells: new GraphCells()
    }
  },
  methods: {
    init () {
      this.graphCells.clear()
      this.graph = new Graph(this.graphCells.graphOptions({}))
      this.graphCells.graph = this.graph
      // 添加节点
      this.graph.addNode({
        id: 'node1',
        shape: 'dag-node',
        x: 100,
        y: 100,
        label: 'foofoo',
        // 创建连接桩
        ports: {
          items: [
            {
              id: 'node1_out',
              group: 'out'
            }
          ]
        }
      })

      this.graph.addNode({
        id: 'node2',
        shape: 'dag-node',
        x: 300,
        y: 100,
        label: 'barbar',
        ports: {
          items: [
            {
              id: 'node2_in',
              group: 'in'
            }
          ]
        }
      })

      this.graph.addEdge({
        shape: 'common-edge',
        source: { cell: 'node1', port: 'node1_out' },
        target: { cell: 'node2', port: 'node2_in' }
      })
    }
  },
  mounted () {
    this.init()
  }
}
</script>

此时显示效果:
在这里插入图片描述

自定义连接

如果再添加一个节点 bazbaz,此时我想手动在 barbar 和 bazbaz 之间连接一条线,这条线和已有的线还不一样,该怎么做呢?
在 DagEdge 中添加另一条自定义连线:

import { Shape, Edge, Graph } from '@antv/x6'

/* 连线1 */
class CommonEdge extends Shape.Edge {
  // ...
}
CommonEdge.config({
  zIndex: 1,
  router: {
    name: 'er',
    args: {
      offset: 24,
      direction: 'H'
    }
  },
  connector: 'rounded',
  connectionPoint: 'boundary',
  attrs: {
    line: {
      stroke: '#BFBFBF',
      strokeWidth: 1,
      targetMarker: null
    }
  }
})
Edge.registry.register('common-edge', CommonEdge, true)

/* 连线2 */
class RelationEdge extends Shape.Edge {
  // hover加粗线
  hoverLine () {
    this.attr('line', {
      strokeWidth: 8
    })
  }

  // 清除hover
  clearHover () {
    this.attr('line', {
      strokeWidth: 1
    })
  }

  // 清除箭头和虚线
  clearMarker () {
    this.attr('line', {
      strokeDasharray: '',
      targetMarker: ''
    })
  }
}
RelationEdge.config({
  zIndex: 1,
  connector: 'rounded',
  connectionPoint: 'boundary',
  router: {
    name: 'oneSide',
    args: { side: 'right' }
  },
  attrs: {
    line: {
      stroke: '#F08BB4',
      strokeWidth: 1,
      strokeDasharray: 5, // 控制虚线间隔
      targetMarker: {
        name: 'classic',
        size: 5
      }
    }
  }
})
Edge.registry.register('relation-edge', RelationEdge, true)

export {
  CommonEdge,
  RelationEdge
}

在 GraphCells.js 中添加

import { RelationEdge } from './DagEdge'
graphOptions (options) {
    return {
      ...
      connecting: {
        ...
        // 连接的过程中创建新的边
        createEdge () {
          return new RelationEdge()
        },
        // 在移动边的时候判断连接是否有效,如果返回 false,当鼠标放开的时候,不会连接到当前元素,否则会连接到当前元素
        validateConnection ({ sourceView, targetView, targetMagnet }) {
          if (!targetMagnet) {
            return false
          }
          if (targetMagnet.getAttribute('port-group') !== 'right') {
            return false
          }
          return true
        }
      }
    }
  }

在 index.vue 中添加 bazbaz 节点

<template>
  <div id="dag-view"></div>
</template>

<script>
import { Graph } from '@antv/x6'
import { GraphCells } from './GraphCells'
// 注册自定义节点
import './DagNode'
// 引入自定义连线  
import './DagEdge'  
export default {
  name: 'index',
  data () {
    return {
      graph: null,
      graphCells: new GraphCells()
    }
  },
  methods: {
    init () {
      this.graphCells.clear()
      this.graph = new Graph(this.graphCells.graphOptions({}))
      this.graphCells.graph = this.graph
      // 添加节点
      this.graph.addNode({
        id: 'node1',
        shape: 'dag-node',
        x: 100,
        y: 100,
        label: 'foofoo',
        ports: {
          items: [
            {
              id: 'node1_out',
              group: 'out'
            }
          ]
        }
      })

      this.graph.addNode({
        id: 'node2',
        shape: 'dag-node',
        x: 300,
        y: 100,
        label: 'barbar',
        ports: {
          items: [
            {
              id: 'node2_in',
              group: 'in'
            },
            {
              id: 'node3_right',
              group: 'right'
            }
          ]
        }
      })

      this.graph.addNode({
        id: 'node3',
        shape: 'dag-node',
        x: 300,
        y: 200,
        label: 'bazbaz',
        ports: {
          items: [
            {
              id: 'node3_right',
              group: 'right'
            }
          ]
        }
      })

      this.graph.addEdge({
        shape: 'common-edge',
        source: { cell: 'node1', port: 'node1_out' },
        target: { cell: 'node2', port: 'node2_in' }
      })
    }
  },
  mounted () {
    this.init()
  }
}
</script>

显示效果:
在这里插入图片描述

添加事件

当鼠标移到连线上时,线加粗,并且显示删除按钮。
index.vue

<template>
  <div id="dag-view"></div>
</template>

<script>
import { Graph } from '@antv/x6'
import { GraphCells } from './GraphCells'
// 注册自定义节点
import './DagNode'
// 引入自定义连线  
import {RelationEdge} from './DagEdge'  
export default {
  name: 'index',
  data () {
    return {
      graph: null,
      graphCells: new GraphCells()
    }
  },
  methods: {
    init () {
      this.graphCells.clear()
      this.graph = new Graph(this.graphCells.graphOptions({}))
      this.graphCells.graph = this.graph
      // 添加节点
      this.graph.addNode({
        id: 'node1',
        shape: 'dag-node',
        x: 100,
        y: 100,
        label: 'foofoo',
        ports: {
          items: [
            {
              id: 'node1_out',
              group: 'out'
            }
          ]
        }
      })

      this.graph.addNode({
        id: 'node2',
        shape: 'dag-node',
        x: 300,
        y: 100,
        label: 'barbar',
        ports: {
          items: [
            {
              id: 'node2_in',
              group: 'in'
            },
            {
              id: 'node3_right',
              group: 'right'
            }
          ]
        }
      })

      this.graph.addNode({
        id: 'node3',
        shape: 'dag-node',
        x: 300,
        y: 200,
        label: 'bazbaz',
        ports: {
          items: [
            {
              id: 'node3_right',
              group: 'right'
            }
          ]
        }
      })

      this.graph.addEdge({
        shape: 'common-edge',
        source: { cell: 'node1', port: 'node1_out' },
        target: { cell: 'node2', port: 'node2_in' }
      })
     
      /* 添加事件 */
      this.graph.on('edge:mouseenter', ({ edge }) => {
        if (edge instanceof RelationEdge) {
          // 在 RelationEdge 中已定义
          edge.hoverLine()
          // X6 提供的小工具 https://x6.antv.vision/zh/docs/api/registry/edge-tool
          edge.addTools([
            {
              name: 'button-remove',
              args: {
                distance: '50%',
                offset: 0,
                // 删除回调
                // onClick ({ cell }) {
                // }
              }
            }
          ])
        }
      })
      
      this.graph.on('edge:mouseleave', ({ edge }) => {
        if (edge instanceof RelationEdge) {
          edge.clearHover()
          edge.removeTools()
        }
      })
      
      this.graph.on('edge:connected', ({ isNew, edge }) => {
        if (isNew) {
          edge.clearMarker()
        }
      })
    }
  },
  mounted () {
    this.init()
  }
}
</script>

此时效果:
在这里插入图片描述

使用布局

使用布局使节点按照一定形式排列
index.vue

<template>
  <div id="dag-view"></div>
</template>

<script>
import { Graph } from '@antv/x6'
import { GraphCells } from './GraphCells'
import { DagreLayout } from '@antv/layout'  
// 注册自定义节点
import './DagNode'
// 引入自定义连线  
import {RelationEdge} from './DagEdge'  
export default {
  name: 'index',
  data () {
    return {
      graph: null,
      dagreLayout: null,
      graphCells: new GraphCells()
    }
  },
  methods: {
    init () {
      this.graphCells.clear()
      this.graph = new Graph(this.graphCells.graphOptions({}))
      this.graphCells.graph = this.graph
     
      /* 添加事件 */
      this.graph.on('edge:mouseenter', ({ edge }) => {
        if (edge instanceof RelationEdge) {
          // 在 RelationEdge 中已定义
          edge.hoverLine()
          // X6 提供的小工具 https://x6.antv.vision/zh/docs/api/registry/edge-tool
          edge.addTools([
            {
              name: 'button-remove',
              args: {
                distance: '50%',
                offset: 0,
                // 删除回调
                // onClick ({ cell }) {
                // }
              }
            }
          ])
        }
      })
      
      this.graph.on('edge:mouseleave', ({ edge }) => {
        if (edge instanceof RelationEdge) {
          edge.clearHover()
          edge.removeTools()
        }
      })
      
      this.graph.on('edge:connected', ({ isNew, edge }) => {
        if (isNew) {
          edge.clearMarker()
        }
      })
      
      this.dagreLayout = new DagreLayout({
        type: 'dagre',
        rankdir: 'LR',
        align: undefined,
        ranksep: 50,
        nodesep: 5,
        controlPoints: true,
        begin: [50, 80]
      })
    },
      
    // 布局渲染
    graphLayout () {
      const newModel = this.dagreLayout.layout(this.graphCells.getModel())
      this.graph.fromJSON(newModel)
      this.graph.centerContent()
    },
      
    getData () {
      // 接口获取节点和连线信息。添加到 graphCells 中
      // this.graphCells.setCell(cell)
      //  this.graphCells.setEdge(edge)
      
      this.$nextTick(() => {
        this.graph && this.graphLayout()
      })
    }
  },
  created () {
    this.getData()
  },  
  mounted () {
    this.init()
  }
}
</script>

GraphCell.js

import { Dom, Shape } from '@antv/x6'
import { RelationEdge } from './DagEdge'

class GraphCells {
  constructor () {
    this.graph = null
    this.cells = new Map()
    this.edges = []
  }

  clear () {
    this.graph = null
    this.cells.clear()
    this.edges = []
  }

  graphOptions (options) {
    return {
      container: document.getElementById('dag-view'),
      autoResize: true,
      // 是否可以拖动
      panning: false,
      grid: {
        size: 10,
        type: 'dot', // 'dot' | 'fixedDot' | 'mesh'
        visible: true,
        args: {
          color: '#a0a0a0', // 网格线/点颜色
          thickness: 1 // 网格线宽度/网格点大小
        }
      },
      highlighting: {
        magnetAvailable: {
          name: 'stroke',
          args: {
            attrs: {
              stroke: '#47C769'
            }
          }
        },
        magnetAdsorbed: {
          name: 'stroke',
          args: {
            attrs: {
              fill: '#fff',
              stroke: '#31d0c6'
            }
          }
        }
      },
      mousewheel: {
        enabled: true,
        modifiers: 'ctrl',
        factor: 1.1,
        maxScale: 1.5,
        minScale: 0.5
      },
      scroller: {
        enabled: true,
        pageVisible: false,
        pageBreak: false,
        pannable: true
      },
      connecting: {
        // 当 snap 设置为 true 时连线的过程中距离节点或者连接桩 50px 时会触发自动吸附
        snap: true,
        // 是否允许连接到画布空白位置的点
        allowBlank: false,
        // 是否允许创建循环连线
        allowLoop: false,
        // 拖动边时,是否高亮显示所有可用的连接桩或节点
        highlight: true,
        createEdge () {
          return new RelationEdge()
        },
        validateConnection ({ sourceView, targetView, targetMagnet }) {
          if (!targetMagnet) {
            return false
          }
          if (targetMagnet.getAttribute('port-group') !== 'right') {
            return false
          }
          return true
        }
      },
      ...options
    }
  }

  // 根据节点类型生成连接桩
  getPortItems (id) {
    const items = []
    const inPort = {
      id: `port_${id}_in`,
      group: 'in'
    }
    const outPort = {
      id: `port_${id}_out`,
      group: 'out'
    }
    const rightPort = {
      id: `port_${id}_right`,
      group: 'right'
    }
    return items
  }

  /**
   * @function 添加节点
   * */
  setCell (options) {
    const id = options.id
    const cell = {
      shape: 'dag-node',
      // 注意添加节点数据时传入的makup会覆盖DagNode.config中的markup。
      // 所以DagNode中的markup可以删除,在这动态定义,就可以根据节点类型不同改变色值和图标
      markup: [
        {
          tagName: 'rect',
          selector: 'body'
        },
        {
          tagName: 'foreignObject',
          attrs: {
          },
          children: [
            {
              ns: Dom.ns.xhtml,
              tagName: 'body',
              attrs: {
                xmlns: Dom.ns.xhtml
              },
              style: {
                display: 'table-cell',
                borderTopLeftRadius: '4px',
                borderBottomLeftRadius: '4px',
                // 动态传入颜色
                backgroundColor: '',
                textAlign: 'center',
                verticalAlign: 'middle',
                height: 28,
                padding: '0 5px'
              },
              children: [
                {
                  tagName: 'i',
                  attrs: {
                    // 动态传入图标class
                    class: ''
                  },
                  style: {
                    fontSize: '16px',
                    // 动态传入颜色
                    color: ''
                  }
                }
              ]
            }
          ]
        },
        {
          tagName: 'text',
          selector: 'label'
        }
      ],
      ports: {
        items: this.getPortItems(id)
      },
      ...options
    }
    this.cells.set(id, cell)
  }

  /**
   * @function 添加连线
   * @param edge {object} 连线配置
   * */
  setEdge (edge) {
    const { source, target } = edge
    if (this.cells.has(source.cell) && this.cells.has(target.cell)) {
      this.edges.push({
        // 默认普通连线
        shape: 'common-edge',
        ...edge
      })
    }
  }

  /* graph model */
  getModel () {
    return {
      nodes: [...this.cells.values()],
      edges: this.edges
    }
  }
}

export {
  GraphCells
}

Tooltip

当节点 label 文本过长会显示 … ,鼠标移入显示 tooltip 显示全名。在 X6 中没有找到合适的小工具,我自己写了一个 Tooltip 组件。这里只是配合我的代码结构使用,并非封装完美的 Tooltip 组件。仅供大家参考。
Tooltip.vue

<template>
  <div id="lz_tooltip_container" class="lz_tooltip_container">
    <div class="tooltip">
      <span class="arrow"></span>
      <div class="cont"></div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Tooltip'
}
</script>

<style lang="scss">
  .lz_tooltip_container {
    position: fixed;
    left: -1000px;
    top: -1000px;
    z-index: 999;
  }

  .lz_tooltip_container > .tooltip {
    position: absolute;
    background: black;
    color: white;
    top: -40px;
    font-size: 14px;
    padding: 8px 16px;
    border-radius: 5px;
  }

  .lz_tooltip_container > .tooltip > .cont {
    white-space: nowrap
  }

  .lz_tooltip_container > .tooltip > .arrow {
    position: absolute;
    width: 0;
    height: 0;
    bottom: -6px;
    left: 24px;
    border-left: 6px solid transparent;
    border-right: 6px solid transparent;
    border-top: 6px solid #000000;
    filter: drop-shadow(0 2px 12px rgba(0,0,0, .03));
  }

  .lz_tooltip_container > .tooltip_hidden {
    display: none;
  }
</style>

我使用的是相对于屏幕的固定定位 fixed。因为在画布会上下滚动,左右平移,所以要根据节点相对画布的位置,转换成相对屏幕的位置然后动态设置 Tooltip 的 left top 属性。
在 index.vue 中 添加事件

<template>
  <div id="dag-view"></div>
</template>

<script>
import Tooltip from './Tooltip'  
export default {
  name: 'index',
   components: {
    Tooltip
  },
  methods: {
    init () {
      ...
      this.graph.on('node:mouseenter', ({ e, node, view }) => {
        // 节点相对画布位置
        const pos = node.position({ relative: true })
        // 转为相对屏幕位置
        const { x, y } = this.graph.localToClient(pos.x, pos.y)
        const tooltip = document.getElementById('lz_tooltip_container')
        if (!tooltip) return
        const cont = tooltip.querySelector('.cont')
        if (!cont) return
        cont.innerText = node.options.label
        tooltip.style.left = `${x + 20}px`
        tooltip.style.top = `${y - 5}px`
      })

      this.graph.on('node:mouseleave', ({ node }) => {
        const tooltip = document.getElementById('lz_tooltip_container')
        if (!tooltip) return
        tooltip.style.left = '-1000px'
        tooltip.style.top = '-1000px'
      })
    }
  }
}
</script>

效果展示:
在这里插入图片描述

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐