前言
因公司业务要求,需要实现一个类似于拓扑图的功能,能够实现拖拽,组件自定义,弹窗提示,自动连线区分状态,数据保存重新渲染,状态更新等功能,查阅文档发现@antv/x6的灵活性比较高,能够满足这些需求
文章借鉴 先知demons 的 vue项目中使用antvX6新手教程,附demo案例讲解,功能大都也都有实现,所以也抽空整理了一下文档
功能预览
antv x6
功能实现
页面布局什么的就不赘述了直接怼用到的功能做描述了
1.下载引入插件
直接 npm install *** 相关依赖就行了
import { Graph } from '@antv/x6'
import insertCss from 'insert-css' // 连接线的动画效果
import { register } from '@antv/x6-vue-shape' // 自定义组件 vue的版本
连接线的其他效果可以参考 边-样式
2.界面
2.1初始化拓扑图
// 初始化拓扑图
initGraph () {
const container = document.getElementById('container')
if (this.graph !== null) this.graph.dispose()
this.graph = new Graph({
autoResize: true,
container: container, // 画布容器
width: container.offsetWidth, // 画布宽
height: container.offsetHeight, // 画布高
background: false, // 背景(透明)
snapline: true, // 对齐线
// 配置连线规则
connecting: {
snap: true, // 自动吸附
allowBlank: false, // 是否允许连接到画布空白位置的点
allowMulti: true, // 是否允许在相同的起始节点和终止之间创建多条边
allowLoop: false, // 是否允许创建循环连线,即边的起始节点和终止节点为同一节点
highlight: true, // 拖动边时,是否高亮显示所有可用的节点
allowNode: false, // 是否允许边连接到节点(非节点上的连接桩),默认为 true。
allowEdge: false, // 是否允许边连接到另一个边,默认为 true。
allowPort: false, // 是否允许边连接到连接桩,默认为 true。
highlighting: {
magnetAdsorbed: {
name: 'stroke',
args: {
attrs: {
fill: '#5F95FF',
stroke: '#5F95FF'
}
}
}
},
router: {
// 对路径添加额外的点
name: 'orth'
},
connector: {
// 边渲染到画布后的样式
name: 'rounded',
args: {
radius: 10
}
}
},
panning: {
enabled: false
},
mousewheel: {
enabled: true, // 支持滚动放大缩小
zoomAtMousePosition: true,
modifiers: 'ctrl',
minScale: 0.5, // 数值越小 看到的内容越多
maxScale: 1
},
grid: {
type: 'dot',
size: 20, // 网格大小 10px
visible: true, // 渲染网格背景
args: {
color: '#a0a0a0', // 网格线/点颜色
thickness: 2 // 网格线宽度/网格点大小
}
},
// 这一步非常重要 设置到能不能拖动节点
interacting: function (cellView) {
if (
cellView.cell.getData() !== undefined &&
!cellView.cell.getData().disableMove
) {
return { nodeMovable: false }
}
return true
}
})
// 自定义组件 一定要提前注册 不然刷新页面后重新渲染会报错
register({
shape: 'custom-vue-node',
width: 100,
height: 100,
component: ItemStyle
})
this.obstacles = []
this.edge = []
const update = () => {
const edgeView = this.graph.findViewByCell(this.edge)
edgeView.update()
}
this.obstacles.forEach((obstacle) =>
obstacle.on('change:position', update)
)
this.nodeAddEvent() // 添加事件监听
this.addSourceNode() // 添加初始图标 就是左侧那个大黑盒
},
2.2添加默认节点
// 添加默认DTU
addSourceNode () {
// 我这里是回显数据 可以忽略
if (this.uData.topologyData) {
const _data = JSON.parse(this.uData.topologyData)
this.fromJSONData = _data
this.refresh()
return
}
// 这里是处理大黑盒右边的连接点样式 根据自己的需求处理
const _protType = this.protocolTypeArray.map((item) => {
return {
id: item.dictLabel,// 有id的时候不会重复添加
group: 'bottom',
attrs: {
text: {
// stroke: item.cssClass,
// fill: item.cssClass,
text: item.dictLabel // 连接点文案
},
circle: {
r: 7,
magnet: true,
stroke: '#ffffff',
strokeWidth: 0,
fill: item.cssClass //节点样式
}
},
tip: item.dictLabel
}
})
this.source = this.graph.addNode({
id: 'source_DTU',
shape: 'image',
x: 40,
y: 180,
width: 192,
height: 240,
imageUrl: this.sourcePng,// 这里是图片
attrs: {
body: {
fill: '#f5f5f5',
stroke: '#d9d9d9'
},
},
ports: {
groups: {
bottom: {
position: 'right',
// args: { x: -50 },
label: {
position: {
name: 'right' // 连接点文案所在方向
}
// },
// attrs: {
// circle: {
// r: 6,
// magnet: true,
// stroke: '#ffffff',
// strokeWidth: 2,
// fill: '#5F95FF'
// }
}
}
},
items: _protType // 这里是你的所有连接点
},
data: {
disableMove: false // true为可拖拽,false不可拖拽 和前面的 interacting 设置有关
}
})
},
2.3 处理事件
// 节点操作
nodeAddEvent () {
// const { graph } = this
// const container = document.getElementById('container')
// const changePortsVisible = (visible) => {
// const ports = container.querySelectorAll('.x6-port-body')
// for (let i = 0, len = ports.length; i < len; i = i + 1) {
// ports[i].style.visibility = visible ? 'visible' : 'hidden'
// }
// }
// this.graph.on('node:mouseenter', () => {
// console.log('鼠标进入!!!')
// changePortsVisible(true)
// })
// this.graph.on('node:mouseleave', () => {
// console.log('鼠标离开!!!')
// changePortsVisible(false)
// })
// 删除
this.graph.on('cell:removed', ({ cell, index, options }) => {
// console.log('删除!!!', cell, index, options)
// 处理气泡显示状态 可以忽略
// 某个节点被删除时 激活在列表中的状态
[this.moduleList, this.moduleData].forEach((module) => {
module.forEach((item) => {
if (cell.id === item.id) {
item.uid = null
}
})
})
})
this.graph.on('scale', ({ sx, sy, ox, oy }) => {
// console.log('缩放!!!', sx, sy, ox, oy)
// this.mapSize = 1
})
this.graph.on('node:mousedown', ({ e, x, y, node, view }) => {
// console.log('鼠标按下!!!', node, node.id)
// 处理气泡显示状态 可以忽略
// 有气泡显示时禁止拖动
if (node.id === 'source_DTU' || this.topPopupShow) {
node.data.disableMove = false
}
})
this.graph.on('node:mouseup', ({ e, x, y, node, view }) => {
// console.log('鼠标抬起!!!', node, node.id)
// 处理气泡显示状态 可以忽略
// 鼠标抬起 气泡显示
if (node.id === 'source_DTU') return
node.data.disableMove = true
})
this.graph.on('node:move', ({ e, x, y, node, view }) => {
// console.log('移动!!!', node)
})
this.graph.on('node:embed', ({ e, x, y, node, view }) => {
// console.log('嵌入!!!', node)
})
// 节点绑定点击事件 单击 双击都可以 看业务需要
this.graph.on('node:dblclick', ({ e, x, y, node, view }) => {
// console.log('双击!!!', node)
if (node.id === 'source_DTU') return
// 判断是否有选中过节点
if (this.curSelectNode) {
// 移除选中状态
this.curSelectNode.removeTools()
// 判断两次选中节点是否相同
if (this.curSelectNode !== node) {
node.addTools([
{
name: 'boundary',
args: {
attrs: {
fill: '#16B8AA',
stroke: '#2F80EB',
strokeWidth: 1,
fillOpacity: 0.1
}
}
},
{
name: 'button-remove',
args: {
x: '100%',
y: 0,
offset: {
x: 0,
y: 0
}
}
}
])
this.curSelectNode = node
} else {
this.curSelectNode = null
}
} else {
this.curSelectNode = node
node.addTools([
{
name: 'boundary',
args: {
attrs: {
fill: '#16B8AA',
stroke: '#2F80EB',
strokeWidth: 1,
fillOpacity: 0.1
}
}
},
{
name: 'button-remove',
args: {
x: '100%',
y: 0,
offset: {
x: 0,
y: 0
}
}
}
])
}
})
},
2.4 添加节点
<div
v-for="item in moduleData"
:key="item.id"
:draggable=" true"
@dragend="handleDragEnd($event, item)"
class="menu-item"
>
右侧可拖动的列表相关样式
</div>
这里是连接线上的离线图标 根据自己的需求判断是否需要
// 离线图标样式
const offlinesStyle = {
markup: [
{
tagName: 'image',
attrs: {
'xlink:href': offlinePng, // 图片地址
width: 20,
height: 20,
y: -10,
x: -10
}
}
],
position: {
distance: 0.8, // 图标位于这条线的位置
args: {
keepGradient: true,
ensureLegibility: true
}
}
}
// 拖动后松开鼠标触发事件
handleDragEnd (e, item) {
// console.log(e, item, this.boxStyle.top, this.boxStyle.left) // 可以获取到最后拖动后松开鼠标时的坐标和拖动的节点相关信息
if (item.uid) return // 如果有绑定 忽略
// 这里处理的是 拖入后的位置 根据你的需求调整 我这里是有动态调整 所以对位置有处理
this.boxStyle = document
.getElementsByClassName('antvBox')[0]
.getBoundingClientRect()
// 向左移动一定的距离才判定是移入
if (e.offsetX < -100) {
item.uid = new Date().getTime()
let min = this.boxStyle.left + this.boxStyle.width - 60
let X = e.pageX - this.boxStyle.left - 30
let Y = e.pageY - this.boxStyle.top - 30
this.addHandleNode(Math.min(X, min), Y, item)
}
},
// 添加节点到画布
addHandleNode (x, y, item) {
// 添加自定义节点到图表
const target = this.graph.addNode({
shape: 'custom-vue-node', // 这里是你的自定义组件名称 和前面 register 定义的保持一致
id: item.id, // 有ID 不会重复添加 id不可为0
x,
y,
width: 260,
height: 80,
data: {
disableMove: true, // true为可拖拽,false不可拖拽
...item
}
})
this.graph.addEdge({
source: {
cell: this.source,
port: item.dictLabel // 连接桩 从哪个桩开始 和前面添加默认点的 id 对应
},
target,
router: { name: 'manhattan' },
connector: { name: 'rounded' },
attrs: {
line: {
stroke: item.borderColor,
targetMarker: 'classic',
strokeDasharray: 10,
style: {
// 这里绑定的事你的 连接线 的动画
animation: item.connectionStatus
? 'ant-line 60s infinite linear'
: ''
}
}
},
labels: item.connectionStatus ? [] : [offlinesStyle]
})
},
2.5 节点页面
在你的父页面 注册好页面
import ItemStyle from './ItemStyle.vue'
components: { ItemStyle },
数据在这一步会携带过去
initGraph () {
......
// 自定义组件 一定要提前注册 不然刷新页面后重新渲染会报错
register({
shape: 'custom-vue-node',
width: 100,
height: 100,
component: ItemStyle
})
......
},
// 添加节点到画布
addHandleNode (x, y, item) {
// 添加自定义节点到图表
const target = this.graph.addNode({
......
data: {
disableMove: true, // true为可拖拽,false不可拖拽
...item // 你的数据
}
})
......
},
子页面接收数据
created () {
const node = this.getNode()
this.dataInfo = node.data
// console.log('node===', node, this.dataInfo)
},
3.保存 复现
这里可以直接看文档 数据处理 很详细
// 导出
save () {
// 内容很明了 点开基本就知道什么是什么了
const _data = this.graph.toJSON()
// 处理你的业务需求
......
},
// 导入
refresh () {
// 如果有需要 可以直接更新 _data 数据
this.graph.fromJSON(_data) Ï
}