☰
Vue3 + VueFlow 企业级审批流程设计器实战:从选型到落地
2026/10/14 7:02:27 网站建设 项目流程

刚开始接到这个需求的时候,我其实有点犯嘀咕。审批流程图这玩意儿,市面上现成的方案不少,轻一点的用antv X6,重一点的上LogicFlow或者bpmn-js,咋就偏偏选了VueFlow?但真正把第一个原型跑起来之后,我理解了——如果你要的是一个纯粹面向Vue3生态、上手成本低、还能被业务快速改造的流程设计器,VueFlow确实是个很聪明的选择。

这篇文章我会从项目选型、数据模型设计、核心组件封装,到拖动连线、属性配置、状态回显这些完整环节,把整个实现过程拆开讲透。里面有大量我实际踩过的坑和验证过的写法,适合三类人看:一是公司要自建审批中台的架构师,二是准备从零搞流程设计器的前端开发,三是对Vue3生态组件二次开发有兴趣的同学。项目整体基于Vue3 + JavaScript,不用TypeScript,也不引入重型流程引擎,完全是前端侧的展示与交互设计器,后端的流程引擎跑在服务端,前端只负责流程的绘制、校验和序列化。

1. 项目方案选型:为什么是VueFlow而不是其它流程图库

1.1 业务诉求与选型背景

先还原一下项目最开始的需求。当时业务方给的原始描述是:要做一套企业级的审批流程配置界面,支持拖拽审批节点、连接顺序、配置审批人和条件分支,并且要能回显已经发起的历史审批流程。项目栈是Vue3 + Element Plus,整体还是JavaScript,没有上TS。

这个需求翻译成技术语言就是三件事:第一,需要一个流程图渲染引擎;第二,这套引擎要能支持高度自定义的节点(因为审批节点跟普通图形节点不一样,它要展示审批人、状态、耗时,甚至要加操作按钮);第三,交互要跟业务深度绑定,比如节点上要能弹配置面板。

当时摆在面前的有几个选项:

  • antv X6:功能非常强大,图编辑能力一流,但它的心智模型偏底层,自定义节点要写的内容多,而且它的API设计跟Vue的组合式API风格差异较大,在Vue3项目里总感觉有点“外来户”。
  • LogicFlow:京东开源,主打流程图编辑,交互细节做得好,但那时候它的Vue3版本还在迭代中,我们不太敢在生产环境冒险。
  • bpmn-js:标准BPMN2.0规范,功能最完整,但问题恰恰是太完整了。企业审批中很多业务并不需要完全符合BPMN规范,它的工具栏、属性面板、节点样式都很难改,改造工作量巨大。
  • VueFlow:基于Vue3的响应式系统原生构建,节点就是Vue组件,所有的props和事件都遵循Vue习惯,自定义节点就跟写普通组件一样简单。

最终拍板用VueFlow的核心原因有三个。第一,原生Vue3响应式:VueFlow的节点和连线默认就是响应式的,改一个节点的style,视图立刻更新,没有任何中间层。第二,自定义节点成本极低:审批节点就是普通Vue组件,给它props它就能渲染,内部想放按钮、标签、进度条随便放。第三,没有过度设计:VueFlow不做业务约束,它只解决“怎么把图画出来、怎么拖拽连线”这个基础问题,这正是我们需要的。它把一个非常灵活的画布引擎交到你手里,业务语义自己定义就够了。

1.2 VueFlow核心概念快速扫盲

如果你之前没用过VueFlow,我建议你先搞懂三个核心概念,后面所有代码都是建立在这三个概念之上。

第一个是Node,节点。在VueFlow里,一个节点就是一个配置对象,核心字段包括id、type、position(坐标)、data(业务数据)。其中type决定了渲染哪个组件,data是传给组件的props。审批节点的所有业务信息,比如审批人、审批类型、状态,全部塞进data里。

第二个是Edge,连线。Edge连接两个节点,核心字段包括source(起点节点id)、target(终点节点id)、type(连线类型,比如默认的贝塞尔曲线或者自定义的直角折线),还有label可以在线上显示文字。审批流程里最喜欢的“条件分支”,比如金额大于1000走A分支,小于1000走B分支,这个“金额大于1000”的条件文案,就是放在Edge的label上。

第三个是Graph,图实例。它是整个画布的控制器,通过useVueFlow这个组合式函数拿到。Graph提供了一系列方法,比如addNodes、addEdges、toObject(序列化data)、project(坐标转换)。记住一句话:useVueFlow()返回的那个对象,就是你在组件里操作一切的总入口。

这三句话理解清楚,后面看任何VueFlow代码你都不会觉得陌生。

2. 企业审批流程的数据模型设计

2.1 审批流数据结构的抽象

很多人在写流程图功能时容易忽略数据模型设计,一上来就堆UI。这是个大坑。数据结构没想清楚,后面每一步都在打补丁。我当时的设计思路是把一个流程定义抽象成这样一个JSON结构:

{ "id": "flow_001", "name": "报销审批流程", "nodes": [ { "id": "node_start", "type": "startNode", "position": { "x": 200, "y": 100 }, "data": { "name": "开始", "nodeType": "start", "approver": "", "conditionRule": null, "status": "success" } }, { "id": "node_approve_1", "type": "approveNode", "position": { "x": 200, "y": 250 }, "data": { "name": "部门经理审批", "nodeType": "approve", "approver": "部门经理", "conditionRule": null, "status": "warning" } } ], "edges": [ { "id": "edge_1", "source": "node_start", "target": "node_approve_1", "type": "default", "label": "", "data": { "conditionText": "" } } ] }

这个结构的核心是:节点和连线完全分离,节点只关心自己是什么、在哪个位置、要带什么业务数据;连线只关心我从哪里来、到哪里去、要挂什么条件文案。

在审批业务里,节点类型我归纳成四类:

节点类型标识用途允许的出边数量
开始节点start流程发起1
审批节点approve单人/多人审批1
条件节点condition分支判断2个及以上
结束节点end流程结束0

这个设计把审批流的所有情况都覆盖了:一条直链的线性审批、带条件分支的复杂路由、多人会签、或者通知抄送之类需要扩展的场景,后面加一个notifyNode类型就行,完全不用动引擎层。

2.2 审批节点与条件分支的语义绑定

设计完数据结构之后,最需要考虑的是怎么把“业务语义”和“图形结构”对应起来。

审批节点,我让它携带这些业务字段:

  • approver:审批人/角色,可以是具体人,也可以是角色编码;
  • mode:单人审批、或签(一人通过即可)、会签(所有人必须通过);
  • timeout:超时时间,超时自动通过还是自动驳回;
  • status:这个节点当前的处理状态,用于流程回放时渲染不同颜色。

条件分支是最容易画错的地方。请不要把“条件判断”做成一个独立节点。我当时看到不少设计稿把条件判断画成一个菱形节点,然后从这个菱形节点分出两条线。这在VueFlow里也能做,但会给后端解析带来巨大的麻烦——后端要区分“节点”和“判断逻辑”两类元素,逻辑判断没有执行者,本质上不应该占用节点身份。

我推荐的做法是这样:条件逻辑直接挂在Edge上。例如,从审批节点A到节点B的这条连线,它的label写“金额≤1000”,从A到C的连线label写“金额>1000”。这个语义在后端解析时就是现成的条件表达式,不需要额外维护一套规则节点。

所以在我的项目里,条件节点的定义其实是这样的:一个审批节点或条件节点如果有多条出边,那么每条出边上的label就是这个分支的进入条件。前端设计器不需要做复杂的规则校验,只需要保证:这个节点出边数量大于等于2时,每条线必须有条件文字。

2.3 流程状态与节点状态的设计

企业级审批有个特征:除了设计流程,你还要能回放流程。也就是说,一个审批单正在流转中,管理员要能看到它走到了哪个节点,哪些节点已经审批通过,哪些节点卡住了。这个需求直接决定了节点的状态渲染。

我给节点数据设计了status字段,取值如下:

  • success:审批通过,节点绿色边框
  • warning:待审批,橙色边框
  • error:审批驳回,红色边框
  • default:未流转到,灰色边框
  • processing:当前正在审批中,蓝色加粗边框

这个状态字段不需要在用户拖拽节点时手动设置,它是在流程实例启动后,由后端推送或前端轮询接口获取的。设计器要做的只是:接收状态数据,把status标签打到对应节点的data上,自定义节点组件根据status渲染不同样式。这样同一个组件,既能当设计器用,也能当流程跟踪视图用,一套代码两种场景,很划算。

提示:状态样式不一定要用字体颜色区分,配合节点边框颜色、左上角状态角标和底部时间戳,信息密度会高很多。只靠改字体颜色,在复杂流程图里非常容易被忽略。

3. 核心实现:拖拽面板、自定义节点与画布交互

3.1 环境搭建与VueFlow引入

项目基于Vite + Vue3 + Element Plus,JavaScript环境。VueFlow引入非常简单:

npm install @vue-flow/core @vue-flow/background @vue-flow/controls @vue-flow/minimap

我多装了三个扩展包,它们的用途分别是:

  • @vue-flow/background:画布背景网格,用于辅助对齐;
  • @vue-flow/controls:左下角的缩放、全屏控制按钮;
  • @vue-flow/minimap:小地图,流程一长,小地图对于全局定位特别有用。

在main.js或者组件里全局注册:

import { VueFlow } from '@vue-flow/core' import '@vue-flow/core/dist/style.css' import '@vue-flow/core/dist/theme-default.css' // 如果用了扩展,需要引入对应样式 import '@vue-flow/controls/dist/style.css' import '@vue-flow/minimap/dist/style.css' import '@vue-flow/background/dist/style.css'

这里有个细节要提醒:theme-default.css不要漏掉。VueFlow默认是无主题的,不引入这个样式,节点不会有阴影、圆角和边框,看起来会非常“裸”。

然后在模板中使用:

<template> <div class="flow-container"> <VueFlow v-model:nodes="nodes" v-model:edges="edges" :node-types="nodeTypes" :min-zoom="0.2" :max-zoom="2" @connect="onConnect" @node-click="onNodeClick" @node-drag-stop="onNodeDragStop" > <Background /> <Controls /> <MiniMap /> </VueFlow> </div> </template>

核心的props和事件就这几个。注意v-model:nodes和v-model:edges双向绑定,这是VueFlow数据响应的精髓。你直接改这个数组里的对象属性,画布上就跟着变;你在画布上拖拽节点,这个数组里的position也会自动更新。前提条件是nodes和edges必须是响应式数据,我用ref包裹,完美满足。

3.2 左侧拖拽面板实现:从组件库拖出节点

审批流程设计器,标配功能是从左侧拖一个节点到画布上。这个功能实现起来比很多人想的要简单。

方案是这样的:左侧面板里放一些可拖拽的“块”,每个块携带一个节点类型信息。拖拽用HTML原生的draggable属性实现,不需要引入任何拖拽库。

节点块的模板大概长这样:

<template> <div class="draggable-node" draggable="true" @dragstart="onDragStart($event, 'approveNode')" > 审批节点 </div> </template>

然后在onDragStart里把类型写到dataTransfer上:

const onDragStart = (event, nodeType) => { event.dataTransfer.setData('application/vueflow', nodeType) event.dataTransfer.effectAllowed = 'move' }

画布接受拖拽,核心逻辑在VueFlow的@drop和@drag-over事件里。先要阻止默认行为,然后利用VueFlow暴露的screenToFlowCoordinate方法,把鼠标在屏幕上的坐标转换成画布坐标。

为什么必须做坐标转换?因为画布有缩放(zoom)和平移(pan),你鼠标在屏幕上的位置直接用于画布坐标是不准的,比如画布缩放到50%时,同样一个鼠标位置,对应的画布坐标应该放大一倍。screenToFlowCoordinate正好帮我们算好这个转换。

const onDrop = (event) => { const type = event.dataTransfer.getData('application/vueflow') if (!type) return // 获取画布坐标 const position = screenToFlowCoordinate({ x: event.clientX, y: event.clientY, }) const newNode = createNode(type, position) nodes.value.push(newNode) }

createNode函数负责生成新的节点数据,它会根据类型填充不同的默认data:

const createNode = (type, position) => { const base = { position, data: { name: '新节点', nodeType: type, status: 'default' } } if (type === 'startNode') { return { id: `node_${Date.now()}`, type, data: { ...base.data, name: '开始' } } } if (type === 'approveNode') { return { id: `node_${Date.now()}`, type, data: { ...base.data, name: '请选择审批人' } } } // 其它类型类似 return { id: `node_${Date.now()}`, type, ...base } }

这个逻辑里有两个小坑,都是我实测踩过的:

第一,dataTransfer的 key 不要随便取,我用application/vueflow这样一个带命名空间的自定义MIME类型。有些浏览器对自定义类型传值有限制,用标准化格式最稳妥。

第二,drop事件不一定触发在VueFlow组件上。如果你的设计器还需要支持从左侧面板拖到右侧属性区这些位置,注意事件目标判断,避免误触发。

3.3 自定义节点组件:审批节点长什么样

VueFlow的核心优势是自定义节点。每个节点本质上就是一个Vue组件,你传给它data,它在内部自己渲染。

我的审批节点组件长这样:

<template> <div class="approve-node" :class="[`status-${data.status}`]"> <div class="node-header"> <span class="node-icon">审</span> <span class="node-title">{{ data.name || '审批节点' }}</span> <el-tag v-if="data.status === 'success'" size="small" type="success">通过</el-tag> <el-tag v-else-if="data.status === 'warning'" size="small" type="warning">待审</el-tag> <el-tag v-else-if="data.status === 'error'" size="small" type="danger">驳回</el-tag> </div> <div class="node-body"> <div v-if="data.approver" class="node-row">审批人:{{ data.approver }}</div> <div v-else class="node-row placeholder">未设置审批人</div> <div v-if="data.timeout" class="node-row">超时:{{ data.timeout }}小时</div> </div> <Handle v-if="data.nodeType !== 'startNode'" type="target" :position="Position.Left" /> <Handle v-if="data.nodeType !== 'endNode'" type="source" :position="Position.Right" /> </div> </template>

这段代码里最关键的是最后两行:Handle组件。Handle就是节点上的连接锚点。带type="target"的Handle负责接收连线的入箭头,带type="source"的Handle负责发出连线。一个节点可以有多个Handle,比如条件节点左右各一个,或者上下各一个,都行。

审批节点是线性流程的典型代表,所以我让target在左,source在右,节点之间自然形成从左到右的流向。

这里有个容易忽略的细节:Handle必须设置Position,不设置的话默认锚点在节点中心,连线会乱掉。Position从@vue-flow/core里导出,有Left、Right、Top、Bottom四个方向。

另外,整个节点外层要设置固定宽度,比如220px。不设置的话,节点宽度由内容撑开,内容一变宽,节点在画布上就跳来跳去,连线锚点位置全部错乱。设置固定宽度是画布布局稳定性的基础。

3.4 节点类型注册与属性配置面板

有了自定义节点组件还不够,你得告诉VueFlow:data里type为approveNode的节点,渲染哪个组件。注册方式是通过nodeTypes对象:

import { MarkerType } from '@vue-flow/core' import StartNode from '@/components/flow/nodes/StartNode.vue' import ApproveNode from '@/components/flow/nodes/ApproveNode.vue' import ConditionNode from '@/components/flow/nodes/ConditionNode.vue' import EndNode from '@/components/flow/nodes/EndNode.vue' const nodeTypes = { startNode: StartNode, approveNode: ApproveNode, conditionNode: ConditionNode, endNode: EndNode, }

然后把nodeTypes传给<VueFlow :node-types="nodeTypes">。这里有一个性能优化点:nodeTypes不要放在响应式数据里,也不要在每次render时重建对象。官网推荐用普通const定义,因为它一旦变化,VueFlow会对所有节点做全量重新渲染,大图会卡。

接下来是点击节点弹出属性配置面板。实现方式很直觉:监听node-click事件,把当前节点的数据填充到一个抽屉或者弹出层里。

const onNodeClick = (event) => { const { node } = event currentEditNode.value = node editPanelVisible.value = true }

属性面板的编辑逻辑,核心是直接改响应式节点的data。因为VueFlow的nodes数组是响应式的,你把node.data.approver重新赋个值,画布上的自定义节点组件会立刻收到props更新,视觉上马上反映出来。

const updateNodeData = (key, value) => { if (!currentEditNode.value) return currentEditNode.value.data[key] = value }

这种方式叫“直接数据驱动视图”,超级好用。但有个限制:如果你给node.data整体重新赋值,比如currentEditNode.value.data = someNewObject,这样反而可能不触发视图更新,因为引用没变。所以优先重置对象内部的属性,不要整个替换data对象。

属性面板的字段要根据节点类型动态渲染。审批节点显示审批人、审批方式、超时时间;条件节点显示分支条件列表。用v-if判断currentEditNode.data.nodeType即可,这里不展开。

3.5 连线交互:连接限制与条件标签编辑

连线是审批流程设计器的另一个核心交互。VueFlow默认支持从一个source拖到target,并触发connect事件。默认情况下,任何source都可以连到任何target,这在审批流里显然不行,要加约束。

先实现基础的连接逻辑:

import { MarkerType } from '@vue-flow/core' const onConnect = (params) => { // params: { source: 'node_xxx', target: 'node_xxx', sourceHandle, targetHandle } if (!canConnect(params)) { ElMessage.warning('不允许这样连接') return } const newEdge = { id: `edge_${Date.now()}`, source: params.source, target: params.target, type: 'default', markerEnd: MarkerType.ArrowClosed, label: '', } edges.value.push(newEdge) }

然后写canConnect判断,这个函数里承载三个业务规则:

  1. 开始节点不能作为target,它只能当起点;
  2. 结束节点不能作为source,它只能接收连线,不能再出去;
  3. 不能形成环,这个审批流是DAG(有向无环图),从A连回A或者A->B->A都不行。

环的判断是这里面最麻烦的。简单做法是遍历当前edges,从目标节点往回找,如果能走到source节点,就说明会形成环。我是直接用递归DFS写的,几十行代码,不复杂:

const canConnect = ({ source, target }) => { const sourceNode = nodes.value.find(n => n.id === source) const targetNode = nodes.value.find(n => n.id === target) if (!sourceNode || !targetNode) return false if (targetNode.data.nodeType === 'start') return false if (sourceNode.data.nodeType === 'end') return false if (hasCycle(source, target)) return false return true }

为什么审批流程要严防成环?因为后端引擎执行流程时,如果是死循环,可能造成流程实例卡死或者无限路由。前端就把这种非法情况拦下来,能省掉一堆后端错误处理。

连线创建之后,还有两个交互体验要重点处理:

第一,连线上的条件编辑。当一条edge被点击时,弹出小输入框,让用户输入“金额>1000”这样的条件文本,然后把这个文本贴到edge的label上,也同步到edge.data.conditionText。

第二,条件分支的强制校验。如果一个节点有多条出边,那么每条出边都必须有label。没有label的连线,你可以设置线条为红色,或者在点击保存时做整体校验。

3.6 拖拽与连线数据同步,序列化与回显

下面是一个容易被忽视但对企业级应用非常重要的环节:拖拽后的位置同步与流程定义保存。

VueFlow默认帮你维护node.position,也就是说你拖拽节点,view会更新,但如果你刷新页面或者重新加载数据,位置可能会丢失,原因是VueFlow内部有一个“同步状态和外部状态”的过程。严格来说,v-model:nodes双向绑定是生效的,但拖拽过程频繁触发position更新,外部数组要确保始终同步。

我的做法是监听node-drag-stop事件,拖拽结束瞬间手动同步一次:

const onNodeDragStop = (event) => { const { node } = event if (!node) return // 手动把最新位置同步到响应式数据里 node.position = { x: Math.round(node.position.x), y: Math.round(node.position.y) } // 这里可以把整个流程数据拉出来存到后端 }

然后整体保存的时候,用VueFlow提供的toObject()方法:

import { useVueFlow } from '@vue-flow/core' const { toObject } = useVueFlow() const saveFlow = () => { const flowData = toObject() // flowData = { nodes: [], edges: [], viewport: {x, y, zoom} } // 调用后端接口保存 saveFlowApi(flowData) }

这里的viewport很重要,它记录了用户当前在画布上看的视角位置和缩放比例,回显时需要把这个viewport也带回去,才能保证流程打开时和保存时看到的视角完全一致。

回映更是简单,把后端返回的数据直接赋值给nodes和edges:

const loadFlow = (flowId) => { const res = await getFlowApi(flowId) nodes.value = res.data.nodes || [] edges.value = res.data.edges || [] // 如果有viewport,回到上次视角 setViewport(res.data.viewport) }

这只是一个基础回显版本。在企业级场景里,还应该支持“回放模式”:把流程实例的status打进对应节点的data里,然后自动根据当前状态高亮相关节点和连线,这个我在第2节已经设计好了status字段,这里直接填充即可。

4. 常见问题与排查技巧实录

4.1 连线拉到节点上没反应

这个是我见过最多的问题。现象是:拖动手柄,鼠标经过目标节点时毫无反馈,松手后连线消失。

排查步骤:

  • 先确认目标节点组件里有<Handle type="target" />。缺了它,目标节点不接收连线。
  • 再确认Handle的Position设置。如果你把Position.Left写成了Position.Right,连线确实能连上,但箭头穿到另一侧,看起来像是连不上。
  • 最后确认样式。Handle的可悬停区域默认比较小,如果节点内部有元素遮挡,鼠标会被挡住。给Handle设置较大的style保证点击区域不要被遮挡。

4.2 节点内容更新了,但画布不刷新

出现这个问题的原因,90%是节点的data数据失去响应式。

// 错误示例 const currentNode = nodes.value.find(n => n.id === 'node_1') currentNode.data = { name: '新名字' } // 整体替换,可能不触发更新 // 正确示例 currentNode.data.name = '新名字' // 修改内部属性,保证响应式

记住一条规则:画布中某个节点发生视觉变化时,直接操作node对象的内部字段,不要替换node整体对象。如果实在要整体替换,先把旧对象删除,再push一个新对象进去。

4.3 初始加载流程时,节点位置都是0

这个问题特别容易出现。你从后端拿到了nodes数据,里面明明有position,但渲染出来所有节点叠在左上角。

原因很可能是数据里position的单位或者字段名不对。VueFlow的position字段是{ x, y },单位是像素。如果后端返回的是字符串"200,100",你要手动转换成数字。另外,确保你回填nodes时,把position设置了,有些老的数据结构可能位置信息存在data里,要做一个映射。

const loadedNodes = (res.data.nodes || []).map(n => ({ ...n, position: { x: Number(n.position.x), y: Number(n.position.y) } }))

4.4 复杂流程渲染卡顿、拖拽掉帧

随着节点数量增多,VueFlow的性能会下降,尤其是你还在每个节点里塞了复杂的表单展示、echarts图表之类的东西。

优化手段按性价比排序:

  1. 关闭无用的动画和过渡,比如节点的默认拖拽动画,可以设置:elevate-edge-on-select="false"。
  2. 使用nodeTypes之外的v-if尽量避免,不要在每个节点组件里做大量动态组件切换。
  3. 不要把整个Element Plus表格或者弹窗放在节点内部,节点内只放摘要信息,完整编辑交给右侧属性面板。
  4. 如果流程图真的很长(超过100个节点),可以考虑拆分视图,加载时用懒渲染。

我实测下来,30个节点以内的审批流程,VueFlow的流畅度是完全没问题的。再往上,就要考虑只渲染可视区域内的节点,VueFlow官方文档里有onlyRenderVisibleElements这个属性,打开后性能提升非常明显。

4.5 样式被污染或者节点乱了

项目里如果用的是Element Plus,VueFlow节点内嵌el-tag、el-button时,偶尔会遇到全局样式冲突。解决办法:

  • 给自定义节点组件加scoped。
  • 如果必用:deep()穿透,注意范围,别把样式改到全局。
  • 画布容器建议设置overflow: hidden,避免节点拖出去后,出现大屏滚动条。

5. 从设计器到落地:数据校验与工程化收尾

5.1 流程合法性的前端校验

拖拽保存容易,但这只是“画出来了”,不等于“流程能跑”。一个流程要能被后端执行引擎接受,至少要满足这几个条件:

  1. 有且只有一个开始节点和一个结束节点。
  2. 开始节点没有入边,结束节点没有出边。
  3. 所有节点至少有一条路径从开始节点可达(不能有游离节点)。
  4. 所有节点的出边数量,与它的节点类型匹配。比如开始节点只能有一条出边,结束节点必须零出边,条件节点至少两条出边。
  5. 条件节点的所有出边都要有条件label,且条件不能重复。

我在保存按钮的点击事件里做校验,使用遍历+DFS:

const validateFlow = () => { const nodeList = nodes.value const edgeList = edges.value const startNodes = nodeList.filter(n => n.data.nodeType === 'start') const endNodes = nodeList.filter(n => n.data.nodeType === 'end') if (startNodes.length !== 1) return '流程必须且只能有一个开始节点' if (endNodes.length !== 1) return '流程必须且只能有一个结束节点' // 更多校验... return '' } const saveFlow = () => { const error = validateFlow() if (error) { ElMessage.error(error) return } // 通过校验,保存 }

5.2 数据埋点与自定义数据扩展

企业级项目里,流程设计器往往不只是配置流程,它还要跟权限系统、消息通知、甚至内部BI系统对接。我的实践是,在设计器里留好自定义扩展字段。

每个节点的data里加一个ext字段,可以塞任意JSON。比如某个节点需要关联部门ID或者特定人员列表,就放ext: { departmentId: 'xxx', notifyUserIds: [] }。这样后端处理的时候有依据,前端也不用在设计器里写死每种业务类型。

同样的,edge对象也加data字段存储条件表达式。前端的校验、后端的路由判断,都从这个字段读取。

5.3 发布与版本管理的简单实现

这可能是审批流程设计器跟普通画图工具最大的差异。企业审批流程是业务资产,不能随意改。我的做法是:设计器里的流程保存分为两个动作——保存草稿和发布。

  • 保存草稿:只把JSON存到后端,不生效。
  • 发布:创建一个新的版本号,同时把当前画布内容锁定成历史快照,之后的审批实例走到一半时,不会因为流程被改而错乱。

前端设计器只需要在保存时传入version和status字段即可,版本管理主要是后端的工作。但前端要预留一个“版本列表”入口,让管理员能对比历史版本。这个功能可以用一个抽屉实现,拉取历史版本的时候把每个版本的node和edge重新推入画布,或者用只读模式展示。VueFlow对只读支持很好:把节点的draggable和connectable设成false,节点就只能看不能动。

5.4 针对不同角色的交互细节

企业级应用离不开权限。流程设计器通常是管理员专属功能,但考虑到运维、审计等场景,有些角色需要“只读查看流程”,有些角色需要“编辑但不可发布”,还有些角色要能“导入导出流程”。我做了三个权限级别:

  • 只读:关闭所有拖拽和连线,显示状态和流程跟踪信息。
  • 编辑:可以拖拽和连线,但不能保存到正式版本。
  • 发布:可以保存并发布新版本。

在代码层面,权限控制一般通过全局store里的角色字段判断,然后动态设置VueFlow的props:

const canEdit = computed(() => store.role === 'admin' || store.role === 'editor')

在模板里:

<VueFlow v-model:nodes="nodes" v-model:edges="edges" :nodes-draggable="canEdit" :nodes-connectable="canEdit" :delete-key-code="canEdit ? ['Delete'] : []" > </VueFlow>

这个思路成本很低,但价值很高,它让你的设计器天然适配了不同岗位的使用需求,不用为每种角色单独做一套界面。

6. 项目回顾与扩展方向

这个项目从立项到第一个可用版本,我大概花了一周多的时间。真正在写VueFlow核心部分的时间其实不多,大部分精力都花在了:数据模型设计、自定义节点排版、业务规则校验、以及和后端接口的联调上。这也再次证明了一个观点:可视化引擎只是底料,业务懂多少,决定这道菜能做多好吃。

再分享两个细节经验。

第一个是关于团队协作的。这个设计器一上线,业务方马上提出要支持“子流程嵌套”,就是某个审批节点点进去,还能再编辑一套子流程。VueFlow做这个扩展很容易,因为节点本身就是一个独立的Vue组件,我在审批节点上加了一个“子流程”配置项,传入子流程ID,后端执行的时候递归展开即可。如果一开始没用VueFlow这种组件化思路,这个需求我都不敢接。

第二个是性能优化心得。真实企业流程很少像Demo那样只有五六个节点,我见过一个OA审批流程上有六十多个节点,且每个节点都有复杂的多人会签配置。到了这个量级,你一定要把onlyRenderVisibleElements打开,同时对于非激活的节点组件,用v-memo缓存。实测打开之后渲染帧率从20多fps直接飙到接近60fps,体感天差地别。

最后讲一下VueFlow这个库目前的适用边界。如果你的项目是标准BPMN规范驱动,后端引擎也是基于BPMN模型的,我建议你直接上bpmn-js,别在这上面纠结;但如果你的后端流程引擎本身就是一套私有模型,前端需要一个灵活的画布编辑器来配合,VueFlow几乎是当前Vue3生态下综合体验最好的选择。它没有捆绑业务模型,把所有语义层留给你自己定义,这种自由度恰恰是企业级应用最需要的。

整个流程设计器的后续扩展上,我个人最推荐先做的是“流程仿真模拟”和“节点模板沉淀”这两个方向。前者能给业务方在配置阶段就看到流程走向,减少发布后发现问题的概率;后者则是把高频的审批链沉淀成模板,让新流程的配置时间从半小时压缩到三分钟。这些方向在VueFlow的体系下都有成熟的落地路径,如果你也有类似的项目正在推进,欢迎在实际开发中参考这篇文章里的踩坑记录,能少走不少弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询