从零实现一个工艺图编辑器:基于 AntV G 的实战
背景
在能源管理系统中,工艺流程图是展示热力站、换热站等设施运行状态的重要载体。传统的做法是用 Visio 或 CAD 绘制静态图片,但这样无法实时展示设备运行数据。
我们需要一个能够:
- 可视化编辑工艺流程图
- 支持设备、管道、文本等元素
- 支持实时数据绑定和展示
- 导出数据供其他系统使用
最终效果 :在线 Demo
示意图:

实现以下功能
- 画布移动和缩放:鼠标左键按住画布任意位置拖动和鼠标滚轮缩放
- 添加元件:在元件面板上把鼠标放在元件上按住左键不放拖拽到画布上即可
- 添加管道:在元件面板顶部单击画线按钮。然后去画布用鼠标左键双击开始画线,单击一次生成一个折线点位。双击结束生成管道
- 删除元件和管道:右键元件或管道选择删除(或按键盘的delete键删除选中的元素)
- 移动元件和管道:鼠标左键按住元件或管道拖动即可
- 旋转元件:鼠标左键按住元件右上角的旋转图标拖动即可
- 缩放元件:鼠标左键按住元件右边的小点拖动即可
- 管道节点拖拽:点击一下管道,出现管道节点。鼠标左键按住管道节点拖动即可
- 支持样式修改:鼠标左键选中对应元件后,在右上角的样式修改框中修改即可
- 支持文本添加:元件面板上的文字按钮拖动到画布上即可
- 支持复制粘贴:鼠标左键选中对应元件后,ctrl+c复制,然后把鼠标移动到要放置的位置ctrl+v粘贴
- 层级调整:鼠标右键菜单选择置顶或置底
- 删除画布上所有元素:在元件库面板上点击清空画布按钮
- 前进后退:ctrl+z 撤回;ctrl+y 重做
- 支持外部数据渲染:网processDrawEdit/index.vue中传入canvasData即可
- 支持画布数据导出:点击元件面板上的提交按钮即可
技术选型
经过调研,我们选择了 AntV G 作为底层渲染引擎:
| 方案 | 优点 | 缺点 |
|---|---|---|
| Canvas 2D 原生 | 性能好、可控性强 | 开发成本高,需要自己实现所有交互 |
| SVG | 交互简单、事件丰富 | 大量元素时性能下降 |
| Fabric.js | 封装完善、文档齐全 | 包体积大,定制性一般 |
| AntV G | 轻量、渲染抽象、支持 Canvas/SVG 切换 | 文档相对较少 |
AntV G 是 AntV 系列图表的底层渲染引擎,提供了统一的渲染 API、完善的元素生命周期管理、相机系统(缩放、平移)和事件系统。
整体架构
模块划分
bash
processDrawEdit/
├── index.vue # 主组件入口
├── comm.ts # 核心交互逻辑
├── data.ts # 数据状态管理
├── dataType.ts # TypeScript 类型定义
├── attr.vue / attr.ts # 属性面板
├── iconPanel.vue # 元件面板
├── contextMenu.ts # 右键菜单
└── retreatAndAdvance.ts # 撤回/前进
架构图
iconPanel.vue] -->|拖拽添加| D[画布 Canvas] B[属性面板
attr.vue] -->|修改样式| E[数据状态] C[右键菜单
contextMenu.ts] -->|操作指令| F[交互逻辑] end subgraph 核心层 D -->|渲染| G[AntV G 渲染引擎] F -->|拖拽/旋转/缩放| D F -->|管道绘制| D H[相机控制
hammerjs + wheel] -->|平移/缩放| D end subgraph 数据层 E[canvasDataRef
data.ts] I[操作历史
retreatAndAdvance.ts] J[外部数据
props.canvasData] end subgraph 输出 E -->|导出| K[JSON 数据] E -->|数据绑定| L[实时数据展示] end J -->|初始化| E F -->|更新| E I -->|撤回/前进| E E -->|驱动渲染| D style D fill:#e1f5fe style E fill:#fff3e0 style G fill:#f3e5f5
数据流
imgData / lineData / textData] D[操作历史栈] end subgraph 渲染 E[AntV G Canvas] F[设备元件] G[管道] H[文本/数据框] end subgraph 输出 I[导出 JSON] J[实时数据绑定] end A --> C B -->|新增/删除/更新| C B -->|记录操作| D D -->|撤回/前进| C C -->|驱动渲染| E E --> F E --> G E --> H C --> I C --> J
元件内部结构
数据流采用单向设计:外部数据进入 canvasDataRef,驱动渲染层绘制;用户交互事件更新数据,属性面板响应数据变化。
核心实现
1. 坐标系统
工艺图编辑器涉及三套坐标系统,理解它们的关系是实现所有交互的基础:
屏幕坐标 :浏览器窗口的像素坐标,鼠标事件的 clientX/clientY 就是这个。
视口坐标:Canvas 元素内的像素坐标,考虑了 Canvas 相对于视口的位置。
画布坐标:虚拟的无限画布坐标,考虑了相机的平移和缩放。
当用户在屏幕上点击时,需要依次转换:屏幕坐标 → 视口坐标 → 画布坐标。AntV G 提供了 client2Viewport 和 viewport2Canvas 两个方法完成这个转换链。
反过来,当我们要把元素定位到特定位置时,需要考虑相机的缩放比例。比如用户拖拽元件移动了 10 个屏幕像素,如果当前缩放比例是 0.5,那么在画布坐标系中实际移动了 20 个单位。
2. 相机控制
工艺图通常较大,需要支持画布缩放和平移。
滚轮缩放 的核心是以鼠标位置为中心进行缩放,而不是以画布中心。这需要两步:先计算鼠标在视口中的位置,然后调用 setZoomByViewportPoint 方法。同时要限制缩放范围(比如 0.1 到 5 倍),避免用户缩放到极端值。
拖拽平移使用 hammerjs 实现手势识别。这里有个细节:平移距离需要除以当前缩放比例,因为用户在屏幕上移动 100 像素,在 2 倍缩放下画布只移动 50 单位。
3. 元件的嵌套结构
一个看似简单的设备图标,内部其实是一个三层嵌套结构:
外层 Group:控制元件的位置。设置原点在元件中心,这样旋转时绕中心转而不是绕左上角转。
内层 Group:控制元件的旋转。为什么要单独一层?因为旋转不影响位置,如果在外层同时处理位置和旋转,变换矩阵会变得复杂。
内容层:实际的图片或 Path 图形,以及选中时的边框、旋转手柄、缩放手柄等辅助元素。
这种分层设计的好处是职责清晰:外层只管位置,内层只管旋转,内容层只管渲染。当用户拖拽元件时,只需要修改外层的位置;当用户旋转时,只需要修改内层的角度。
4. 拖拽交互
AntV G 本身不提供拖拽功能,我们使用 interact.js 来实现。
拖拽的核心难点是冲突处理 :用户拖拽元件时,不应该触发画布的平移;用户画管道时,不应该触发元件的拖拽。解决方案是维护几个状态标志:disableDragCamera 和 disableDragDevice,在交互开始时设置标志,结束时延迟重置(延迟是为了避免事件穿透)。
另一个细节是操作历史记录:每次拖拽结束时,需要记录操作前后的状态快照,用于撤回/前进功能。快照要深拷贝,避免引用问题。
5. 旋转计算
旋转功能的核心是计算鼠标移动的角度。我们采用三点角度法:
- 中心点:元件的中心(旋转轴)
- 起点:鼠标按下时的位置
- 终点:鼠标移动后的位置
计算步骤:
- 用向量叉积判断旋转方向(顺时针还是逆时针)
- 用余弦定理计算三点形成的夹角
- 将角度叠加到元件的初始角度上
这里有个坑:Canvas 的 Y 轴向下,所以向量叉积的符号判断要反过来。
6. 管道绘制
管道是折线,绘制流程是:单击添加节点 → 双击结束绘制。
90 度转角是工艺图的常见需求。实现思路是:当用户点击位置与上一个节点既不在同一水平线也不在同一垂直线时,自动插入一个转角节点。具体来说,如果水平距离更大,就先画水平线再画垂直线;反之亦然。
管道绘制完成后,每个节点都是一个可拖拽的圆点。拖拽节点时,需要同步更新折线的 points 属性,让管道形状实时变化。
7. 撤回与前进
采用经典的命令模式实现,维护两个栈:
- 撤回栈:记录可以撤回的操作
- 前进栈:记录可以重做的操作
每次操作都会生成一个历史记录,包含:
- 操作类型(新增/删除/更新)
- 操作前的状态快照(用于撤回)
- 操作后的状态快照(用于前进)
撤回时:从撤回栈弹出记录,执行反向操作,将记录压入前进栈。 前进时:从前进栈弹出记录,执行正向操作,将记录压入撤回栈。
新增操作时,前进栈清空------这是编辑器的标准行为。
边界情况:新增后删除的元素,撤回时应该如何处理?我们的方案是区分两种情况:
- 如果是本次编辑会话新增的元素,撤回删除时直接移除
- 如果是原本就存在的元素,撤回删除时恢复到删除前的状态
8. 数据绑定
工艺图需要展示实时数据,我们设计了"数据框"概念。
数据框本质上是一个文本元素,但额外绑定了数据源配置:
key:数据在接口返回中的字段名label:展示时的标签文字unit:单位后缀equation:单位转换公式(如x * 1.8 + 32用于摄氏转华氏)decimal:保留小数位
渲染时,用实际数据替换文本中的占位符 --。比如文本模板是"压力 -- MPa",当接口返回 pressure: 0.5 时,渲染为"压力 0.5 MPa"。
9. 右键菜单
右键菜单通过 AntV G 的 HTML 元素实现。思路是:
- 监听元素的
rightup事件 - 计算右键位置在画布坐标系中的位置
- 创建一个 HTML 元素,渲染菜单 DOM
- 监听菜单项的点击事件,执行对应操作
- 点击画布其他位置时移除菜单
注意 HTML 元素需要根据相机缩放比例调整自身的缩放,否则在放大画布时菜单会显得很小。
10. 层级管理
当多个元件重叠时,需要支持调整层级。我们通过 zIndex 属性控制,右键菜单提供"置顶"和"置底"选项。
置顶:将元素的 zIndex 设为当前最大值 + 1 置底:将元素的 zIndex 设为当前最小值 - 1
由于 AntV G 会根据 zIndex 自动排序渲染顺序,修改后立即生效。
数据结构
最终导出的数据分为三类:
imgData:设备元件,包含位置、尺寸、旋转角度、元件类型标识等。
lineData:管道,包含节点坐标数组、线条样式(颜色、粗细、是否虚线)等。
textData:文本和数据框,包含位置、文本内容、样式、数据绑定配置等。
所有元素都有唯一 ID、类型标识和编辑状态标记(新增/删除/更新),便于增量同步到后端。
小结
这个工艺图编辑器的核心价值在于:
- 可视化编辑:非专业人员也能绘制工艺流程图
- 数据绑定:静态图变成动态数据展示
- 数据导出:编辑结果可序列化存储
技术上的关键点:
- 坐标系统:三套坐标的转换是所有交互的基础
- 分层结构:外层管位置、内层管旋转,职责清晰
- 状态管理:通过标志位解决交互冲突
- 命令模式:撤回/前进的标准实现
- 数据驱动:数据与视图分离,便于扩展