本文将通过代码实例详细讲解如何使用 Vue 3 和 AntV X6 构建一个可视化流程编辑器,重点介绍核心功能的实现方法。
效果图
点击查看源码
为什么选择 AntV X6?
AntV X6 是蚂蚁集团开源的图编辑引擎,专门为流程图、拓扑图等场景设计:
- ✅ 开箱即用:内置节点、连线、拖拽等功能
- ✅ Vue 友好:与 Vue 3 完美集成
- ✅ 文档完善:中文文档,示例丰富
- ✅ 功能强大:支持自定义节点、连线规则等
项目结构
我们的流程编辑器由 4 个核心组件组成:
bash
流程编辑器
├── ProcessToolbar.vue # 工具栏(保存、导出等)
├── NodePanel.vue # 左侧节点面板
├── ProcessCanvas.vue # 中间画布区域
└── NodeConfig.vue # 右侧属性配置
核心功能实现
1. 画布初始化
首先创建 X6 画布,这是整个流程编辑器的核心:
javascript
// ProcessCanvas.vue
import { Graph } from '@antv/x6'
export default {
setup() {
const container = ref(null)
const graph = ref(null)
const initGraph = () => {
graph.value = new Graph({
container: container.value,
width: '100%',
height: 600,
// 显示网格,方便对齐
grid: {
visible: true,
type: 'doubleMesh'
},
// 支持鼠标滚轮缩放
mousewheel: true,
// 支持画布拖拽
panning: {
enabled: true,
eventTypes: ['leftMouseDown']
},
// 连线配置
connecting: {
router: { name: 'manhattan' }, // 直角连线
connector: { name: 'rounded' }, // 圆角连线
allowBlank: false, // 不允许连接到空白处
allowLoop: false, // 不允许自连接
snap: true, // 自动吸附到连接桩
// 连接验证规则
validateConnection({ sourceView, targetView }) {
const sourceType = sourceView.cell.getData()?.type
const targetType = targetView.cell.getData()?.type
// 开始节点不能作为目标
if (targetType === 'start') return false
// 结束节点不能作为源
if (sourceType === 'end') return false
return sourceView !== targetView
}
}
})
// 监听节点点击事件
graph.value.on('node:click', ({ node }) => {
emit('nodeSelected', node)
})
}
onMounted(() => {
initGraph()
})
return {
container,
graph
}
}
}
关键配置说明:
grid
: 显示网格,帮助用户对齐节点mousewheel
: 支持滚轮缩放panning
: 支持拖拽画布connecting
: 连线规则配置validateConnection
: 自定义连接验证逻辑
2. 节点拖拽功能
从左侧面板拖拽节点到画布是核心功能,我们需要定义不同类型的节点:
javascript
// NodePanel.vue - 节点面板
export default {
setup() {
// 定义节点类型
const nodeList = [
{ name: '开始', type: 'start', iconClass: 'el-icon-video-play' },
{ name: '流程', type: 'process', iconClass: 'el-icon-s-operation' },
{ name: '判断', type: 'inspection', iconClass: 'el-icon-help' },
{ name: '结束', type: 'end', iconClass: 'el-icon-video-pause' }
]
// 开始拖拽
const startDrag = (nodeConfig, event) => {
emit('startDrag', nodeConfig, event)
}
return {
nodeList,
startDrag
}
}
}
javascript
// ProcessCanvas.vue - 处理拖拽
import { Dnd } from '@antv/x6-plugin-dnd'
export default {
setup() {
// 创建不同类型的节点
const createNode = (nodeConfig) => {
const baseConfig = {
width: 100,
height: 40,
label: nodeConfig.name,
data: { type: nodeConfig.type }
}
// 根据类型设置不同样式
switch (nodeConfig.type) {
case 'start':
return {
...baseConfig,
shape: 'circle',
width: 60,
height: 60,
attrs: {
body: {
fill: '#e8f7ee',
stroke: '#67c23a'
}
}
}
case 'end':
return {
...baseConfig,
shape: 'circle',
width: 60,
height: 60,
attrs: {
body: {
fill: '#fef0f0',
stroke: '#f56c6c'
}
}
}
case 'inspection':
return {
...baseConfig,
shape: 'polygon',
width: 80,
height: 80,
attrs: {
body: {
fill: '#fef9e7',
stroke: '#e6a23c',
refPoints: '0,10 10,0 20,10 10,20' // 菱形
}
}
}
default: // process 类型
return {
...baseConfig,
shape: 'rect',
attrs: {
body: {
fill: '#fff',
stroke: '#1890ff'
}
}
}
}
}
// 处理拖拽事件
const handleStartDrag = (nodeConfig, event) => {
const node = graph.value.createNode(createNode(nodeConfig))
const dnd = new Dnd({
target: graph.value
})
dnd.start(node, event)
}
return {
handleStartDrag
}
}
}
实现要点:
- 不同节点类型使用不同的形状(圆形、矩形、菱形)
- 通过
attrs.body
设置节点样式 - 使用 X6 的 Dnd 插件实现拖拽功能
3. 节点右键菜单
为节点添加右键菜单,提供删除等操作:
javascript
// ProcessCanvas.vue
export default {
setup() {
// 显示右键菜单
const showContextMenu = (node, event) => {
event.preventDefault()
// 创建菜单
const menu = document.createElement('div')
menu.innerHTML = `
<div class="context-menu">
<div class="menu-item" data-action="delete">
删除节点
</div>
</div>
`
// 设置菜单位置
menu.style.cssText = `
position: fixed;
left: ${event.clientX}px;
top: ${event.clientY}px;
z-index: 9999;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
`
document.body.appendChild(menu)
// 处理菜单点击
menu.addEventListener('click', (e) => {
const action = e.target.dataset.action
if (action === 'delete') {
graph.value.removeNode(node)
emit('nodeSelected', null) // 清空选中状态
}
document.body.removeChild(menu)
})
// 点击外部关闭菜单
const closeMenu = (e) => {
if (!menu.contains(e.target)) {
document.body.removeChild(menu)
document.removeEventListener('click', closeMenu)
}
}
setTimeout(() => {
document.addEventListener('click', closeMenu)
}, 100)
}
// 监听右键事件
graph.value.on('node:contextmenu', ({ node, e }) => {
showContextMenu(node, e)
})
return {
showContextMenu
}
}
}
关键点:
- 使用原生 DOM 创建菜单
- 根据鼠标位置定位菜单
- 删除节点后清空选中状态
4. 导入导出功能
实现流程图的保存和加载:
javascript
// ProcessToolbar.vue - 工具栏
export default {
setup() {
// 导出为 JSON
const exportJSON = () => {
const data = graph.value.toJSON()
// 创建下载链接
const blob = new Blob([JSON.stringify(data, null, 2)], {
type: 'application/json'
})
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `process-${Date.now()}.json`
link.click()
URL.revokeObjectURL(url)
}
// 导入 JSON
const importJSON = () => {
const input = document.createElement('input')
input.type = 'file'
input.accept = '.json'
input.onchange = (e) => {
const file = e.target.files[0]
if (!file) return
const reader = new FileReader()
reader.onload = (event) => {
try {
const data = JSON.parse(event.target.result)
// 清空画布并导入数据
graph.value.clearCells()
graph.value.fromJSON(data)
ElMessage.success('导入成功')
} catch (error) {
ElMessage.error('导入失败:' + error.message)
}
}
reader.readAsText(file)
}
input.click()
}
// 导出为图片
const exportImage = async () => {
try {
const dataURL = await graph.value.exportPNG('process-diagram', {
backgroundColor: '#f5f5f5',
padding: 20
})
// 下载图片
const link = document.createElement('a')
link.href = dataURL
link.download = `process-${Date.now()}.png`
link.click()
ElMessage.success('导出成功')
} catch (error) {
ElMessage.error('导出失败')
}
}
return {
exportJSON,
importJSON,
exportImage
}
}
}
核心方法:
toJSON()
: 将画布数据转换为 JSONfromJSON()
: 从 JSON 数据恢复画布exportPNG()
: 导出为图片格式
常见问题与解决方案
1. 内存泄漏问题
问题:组件销毁时 X6 实例没有正确清理。
解决方案:
javascript
// 在组件销毁时清理资源
onUnmounted(() => {
if (graph.value) {
graph.value.dispose() // 销毁 X6 实例
}
})
2. 响应式数据优化
问题:将整个 graph 实例设为响应式会影响性能。
解决方案:
javascript
// ❌ 错误做法
const graph = ref(new Graph(...))
// ✅ 正确做法
const graph = shallowRef(null) // 使用 shallowRef
const currentNode = ref(null) // 只将必要的状态设为响应式
3. 连接点不显示
问题:节点的连接点有时不显示。
解决方案:
javascript
// 在节点配置中确保连接点可见
ports: {
groups: {
default: {
attrs: {
circle: {
style: { visibility: 'visible' } // 关键设置
}
}
}
}
}
4. 删除节点后右侧配置未清空
问题:删除节点后,右侧属性配置面板仍显示已删除节点的信息。
解决方案:
javascript
// 删除节点时清空选中状态
const deleteNode = (node) => {
graph.value.removeNode(node)
emit('nodeSelected', null) // 清空当前选中节点
}
学习要点总结
1. 核心技术栈
- Vue 3: 使用 Composition API 组织代码逻辑
- AntV X6: 专业的图编辑引擎,功能强大
- Element Plus: 提供 UI 组件支持
2. 关键实现步骤
- 初始化画布: 配置 X6 实例,设置网格、缩放、连线规则
- 节点拖拽: 定义不同类型节点,实现从面板到画布的拖拽
- 事件处理: 监听节点点击、右键菜单等交互事件
- 数据持久化: 实现流程图的导入导出功能
3. 开发建议
- 使用
shallowRef
而不是ref
来存储 X6 实例 - 组件销毁时记得调用
graph.dispose()
清理资源 - 合理设置连接验证规则,避免无效连接
- 删除节点时要清空选中状态
4. 扩展方向
- 添加更多节点类型(网关、子流程等)
- 实现撤销/重做功能
- 支持节点分组和折叠
- 添加流程验证功能