antv/X6 可视化连线流程图
用于实现流程图,可进行交互。本文记录的是
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
注册,否则后面做回显时,使用不了toJSON
和fromJSON
。回显时使用这两个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>• 统计对象</div> <div 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/,这是我博客地址,让我们一起在前端道路上持续发胖吧,
作者:GauharChan
链接:https://juejin.cn/post/7022833474667020302