antv/X6 可视化连线流程图

12,286 阅读1分钟

用于实现流程图,可进行交互。本文记录的是vue的用法。

完整的demo,点击看源码

安装

建议使用yarn安装哈,一开始我也是贪cnpm速度快一点,结果安装不了x6-vue-shape

# yarn
$ yarn add @antv/x6

# yarn
yarn add @antv/x6-vue-shape

# 在 vue2 下还需要安装 @vue/composition-api
yarn add @vue/composition-api --dev

1.创建画布

  • keyboard开启监听键盘
  • selecting设置选择节点,可通过filter去过滤。我只需要选择连接线(x6中叫边Edge)。graph.isEdge(node)判断传入的节点是否为边(Edge)
  • scroller使用scroller模式,超出container会出现滚动条可移动查看节点node,pannable是否启用画布平移能力
  • translating配置移动选项,我这里限制了子组件不可以拖动,限制在父组件里面,只能移动父组件
import '@antv/x6-vue-shape'
import { Graph, Line, Path, Curve, Addon } from '@antv/x6'

export default {
  data () {
    return {
      graph: null
    }
  },
}
// 创建画布
this.graph = new Graph({
  container: document.getElementById('drag-container'),
  keyboard: true,
  scroller: {
    enabled: true,
    pannable: true
  },
  selecting: {
    enabled: true,
    rubberband: true,
    filter (node) {
      // 只选连接线(边)
      return that.graph.isEdge(node)
    }
  },
  translating: {
    restrict (view) {
      const cell = view.cell
      if (cell.isNode()) {
        const parent = cell.getParent()
        if (parent) {
          // 限制子节点不能移动
          return {
            x: cell.getBBox().x,
            y: cell.getBBox().y,
            width: 0,
            height: 0
          }
        }
      }
      return null
    }
  }
})

2.注册节点

我的节点都是用vue组件写的,并不是用原生svg的一些图形(circle、rect),实际开发的需求肯定不是这么简单,vue组件必须用registerVueComponent注册,否则后面做回显时,使用不了toJSONfromJSON。回显时使用这两个api,不需要任何的操作,渲染即可

import Drag from '@/components/drag/drag.vue'
import Item from '@/components/drag/item.vue'

const that = this
// 注册父组件
Graph.registerVueComponent(
  'Drag',
  {
    template: '<drag @parentnoderemove="parentnoderemove"></drag>',
    methods: {
      parentnoderemove ({ id }) {
        // 删除对应的节点
        that.saveNodes.splice(that.saveNodes.findIndex(item => item.id === id), 1)
      }
    },
    components: {
      Drag
    }
  },
  true
)
// 注册子组件
Graph.registerVueComponent(
  'Item',
  {
    template: '<item @edge="edge"></item>',
    methods: {
      // 两个节点之间连线
      edge: that.edge
    },
    components: {
      Item
    }
  },
  true
)

3.生成节点

我这里用到了拖拽生成节点,对于拖拽,x6提供了Dnd。首先对Dnd进行创建配置

getDropNode配置拖拽结束,创建节点之前执行的函数,这个函数一定要return node节点

// 拖拽
this.dnd = new Addon.Dnd({
  target: this.graph, // 画布对象
  scaled: false,
  animation: true,
  getDropNode: that.handleEndDrag
})
  • 对开始拖拽的目标节点<div>添加mousedown事件去执行Dnd.start事件即可,start函数要传入mousedown事件的event对象
<template>
  <div>
    <div class="side-title">• 统计对象</div>
    <div
       class="side-item"
       v-for="(item, index) in leftSide"
       :key="index"
       @mousedown="handleDrag(item, $event)"
     >
      {{item.title}}
  	</div>
  </div>
</template>

methods

创建节点需要提交x、y坐标值,所以在handleEndDrag事件中我先让返回的父组件渲染完成后,获取父组件的x、y的值,再创建子节点。

<script>
  export default {
    methods: {
      // 开始拖拽
      handleDrag (item, e, weidu) {
        // 业务逻辑
        
        // item请求。。
        this.createChildren = [1, 2, 3] // item请求回来的数据
        this.handleCreateNode(item, e)
      },
      handleCreateNode (item, e) {
        const that = this
        // 创建父节点
        const parent = this.graph.createNode({
          shape: 'vue-shape',
          x: 100,
          y: 100,
          height: that.createChildren.length * 60 + 58, // 根据实际的UI样式来
          data: {
            item,
            height: that.createChildren.length * 60 + 58
          },
          component: 'Drag' // 这个名字对应registerVueComponent的名字
        })
        // 开始拖拽
        this.dnd.start(parent, e)
      },
      // 拖拽结束,渲染节点之前,必须返回克隆的节点
      handleEndDrag (node) {
        const cloneNode = node.clone({ deep: true })
        const that = this
        // 父节点渲染之后再执行,因为需要父节点的位置
        this.$nextTick(() => {
          const { x, y } = cloneNode.position()
          // 是否第一个节点
          const cellCount = that.graph.getCellCount()
          this.createChildren.forEach((item, index) => {
            const child = this.graph.addNode({
              shape: 'vue-shape',
              x: x + 20,
              y: index === 0 ? y + 58 : y + (index * 60 + 58),
              width: 240,
              height: 46,
              data: {
                item
              },
              component: 'Item'
            })
            cloneNode.addChild(child) // 添加到父节点
          })
          this.saveNodes.push(cloneNode)
          if (cellCount === 1) {
            this.firstNode = cloneNode
            that.$message.warning('第一个统计对象为主体')
          }
        })
        return cloneNode
      }
    }
  }
</script>

4.两个节点之间连线(创建边)

需求是点击两个node,就让它们连线

点击的事件我放在了item组件里,点击后触发父组件这边的edge方法

connector决定你的线是怎样的,我这边是圆弧

// 连线规则,圆弧
Graph.registerConnector( // Graph不是创建的画布实例(this.graph)!!!
  'smooth',
  (
    sourcePoint,
    targetPoint,
    routePoints,
    options
  ) => {
    const line = new Line(sourcePoint, targetPoint)
    const diff = 5
    const factor = 1
    const vertice = line
    .pointAtLength(line.length() / 2 + 12 * factor * Math.ceil(diff))
    .rotate(90, line.getCenter())

    const points = [sourcePoint, vertice, targetPoint]
    const curves = Curve.throughPoints(points)
    const path = new Path(curves)
    return options.raw ? path : path.serialize()
  },
  true
)
    
// 两个node之间连线
edge (node) {
  // console.log('node', node.id)
  // console.log('AllEdges', this.graph.getEdges(node))
  const allEdges = this.graph.getEdges(node)
  this.waitEdgeNodes.push(node)
  if (this.waitEdgeNodes.length === 2) {
    // 改变active状态
    this.$store.dispatch('callNodes', this.waitEdgeNodes.map(item => item.id))
    // 判断不是同一父级
    if (this.waitEdgeNodes[0]._parent.id !== this.waitEdgeNodes[1]._parent.id) {
      // 要连线的目标id和来源id
      const allTargetAndAllSource = allEdges.map((item) => [
        item.getTargetCellId(),
        item.getSourceCellId()
      ])
      const flag = allTargetAndAllSource.filter(item => 
        item.includes(this.waitEdgeNodes[0].id) && item.includes(this.waitEdgeNodes[1].id
      ))
      // 如果两个点已经连过线,
      if (flag.length) return (this.waitEdgeNodes.length = 0)
      // 这里通过坐标决定连线的点
      const sourceAnchor = this.waitEdgeNodes[0].getBBox().x < this.waitEdgeNodes[1].getBBox().x ? 'right' : 'left'
      const targetAnchor = this.waitEdgeNodes[0].getBBox().x < this.waitEdgeNodes[1].getBBox().x ? 'left' : 'right'
      // 设置箭头的大小
      const args = {
        size: 8
      }
      this.graph.addEdge({
        source: { cell: this.waitEdgeNodes[0], anchor: sourceAnchor, connectionPoint: 'anchor' },
        target: { cell: this.waitEdgeNodes[1], anchor: targetAnchor, connectionPoint: 'anchor' },
        connector: { name: 'smooth' },
        attrs: {
          line: {
            strokeDasharray: '5 5',
            stroke: '#666',
            strokeWidth: 1,
            sourceMarker: {
              args,
              name: 'block' // 实心箭头
            },
            targetMarker: {
              args,
              name: 'block'
            }
          }
        }
      })
    }
    // 无论如何都清空
    this.waitEdgeNodes.length = 0
  }
},

5.选择连接线,并监听键盘删除键进行删除

因为一开始的配置就限制了只能选择edge,所以这里不用判断其他的cell

这部分主要是更改样式

// 选择连接线(边)事件
this.graph.on('selection:changed', ({ added, removed }) => {
  this.selectLine = added
  added.forEach((cell) => {
    const args = { size: 15 }
    cell.setAttrs({
      line: {
        sourceMarker: {
          args,
          name: 'block'
        },
        targetMarker: {
          args,
          name: 'block'
        },
        stroke: '#2D8CF0',
        strokeWidth: 4
      }
    })
  })
  removed.forEach((cell) => {
    const args = { size: 8 }
    cell.setAttrs({
      line: {
        sourceMarker: {
          args,
          name: 'block'
        },
        targetMarker: {
          args,
          name: 'block'
        },
        stroke: '#666',
        strokeWidth: 1
      }
    })
  })
})

监听删除键删除

通过cell.remove方法删除

// 删除连接线(边)
this.graph.bindKey(['Backspace', 'Delete'], (e) => {
  if (this.selectLine.length) {
    this.$confirm('确认删除连线吗?', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    }).then(() => {
      this.selectLine[0].remove()
      this.$message({
        type: 'success',
        message: '删除成功!'
      })
    })
  }
})
    

点个赞支持一下吧🙏🏻

blog:gauharchan.github.io/,这是我博客地址,让我们一起在前端道路上持续发胖吧,😁