资讯详情

基于Vue3+VueFlow构建企业级审批流程图可视化设计器

📅 2026/10/14 4:31:40 | 华诺云谱 👁 阅读
基于Vue3+VueFlow构建企业级审批流程图可视化设计器
最近在搞一个公司内部的审批流程系统改造最核心的一块就是把原来靠表单字段硬凑的审批链路改成可视化的流程设计器。技术栈定的是 Vue3 JavaScript画布这块我选了 VueFlow。整套方案落地之后身边不少同事跑来问实现细节我干脆把整个项目的关键路径整理成一篇博文覆盖选型、数据模型、交互设计、运行态联动、性能优化这几个最痛的环节希望对你也有参考价值。先说结论VueFlow 这套库配合 Vue3 的 Composition API做企业级审批流程图可视化非常顺手。它不像一些重型低代码引擎那样侵入业务也不像自己从零写 SVG 那样耗时属于框架给你画布能力、业务规则你掌控的中间态。适合的场景包括审批流设计器、工单流转配置、节点编排工具等前端团队如果有 Vue3 基础基本一周内能摸熟。1. 为什么偏偏是 VueFlow选型心路与业务权衡白板拖拽类的可视化需求市面上能选的路其实不少。这里我不打算列一个泛泛的对比清单而是直接聊一聊在审批场景里我当时是怎么做取舍的。1.1 审批可视化真正要解决的三类问题很多刚接触审批流程可视化的同学第一反应是不就是画个流程图嘛D3 也行。但真到了企业级场景需求会拆成三类每一类的技术侧重点都不一样流程设计态业务人员要能在画布上拖出节点、连线、删改、配置审批人本质是一个可视化编辑器。流程实例运行态某个审批单走到了哪个节点哪些人待办哪些分支被命中需要在同一张图上高亮显示。流程数据交换画布上的图数据必须能转成后端引擎认识的 JSON 结构同时也能把引擎返回的过程数据还原成图。这三类需求加在一起意味着选型时不能只看能不能画图还得看有没有完整的交互事件体系和数据是否可受控。VueFlow 刚好在这三块都有对口的 API。1.2 主流方案对比自研画布、其他图形库、VueFlow我按实际体验聊几个方向。自研 SVG / Canvas 画布优点是完全可控但对团队来说成本极高。光是拖拽缩放、坐标换算、连接线命中检测、撤销重做这一套至少要两到三周的人力而且后期边缘问题特别多。审批流程这种业务没必要把时间砸在画布基建上。其他图形库比如一些偏向拓扑编排的库适配网络拓扑场景很专业但审批节点需要的业务属性面板、条件分支、会签规则等都要自己再造一轮抽象相当于把 VueFlow 已经做好的节点即组件模式重新发明一遍。VueFlow它最大的特点是节点渲染完全组件化每个业务节点就是一个 Vue 组件节点上带几个 Handle 作为连接锚点图数据就是普通数组。这意味着我们把节点的业务属性直接塞进data字段画布完全不知道业务规则但可以实时承载业务规则的结果。1.3 VueFlow 核心运行机制速览用一句话概括 VueFlow 的运行机制它内部维护了一个 graph store保存了nodes和edges两个响应式数组每次拖拽、连线、缩放本质都是在更新这两个数组。视图层则根据数组内容去渲染对应的节点组件和连线路径。画布本身是一个带viewport视口的概念所有节点坐标是画布内部的逻辑坐标而屏幕坐标需要通过screenToFlowCoordinate做换算。这个机制决定了一个重要事实节点位置是绝对坐标而不是相对定位。保存流程时直接把position存下来就行下次恢复时放回原处用户看到的布局不会乱。在这个基础上去看后面的数据模型和交互设计思路就会清晰很多。2. 搭骨架从空页面到第一个可拖拽的流程图这节我把从零到能跑通的最小步骤列出来包含环境初始化和最基础的代码骨架。已经跑过 Vue3 项目的可以直接跳过 2.1。2.1 初始化 Vue3 Vite 环境项目前端用的是 Vite搭建命令很简单npm create vitelatest flow-designer -- --template vue cd flow-designer npm install npm install vue-flow/core vue-flow/controls vue-flow/minimap这里有个小提醒VueFlow 的核心包是vue-flow/core控制栏、小地图、背景网格都是独立子包按需安装。我见不少人只装了 core结果发现没有放大缩小按钮和小地图其实是漏了vue-flow/controls。我们把设计器主文件放在src/views/FlowDesigner.vue结构上分为三块左侧物料面板、中间画布、右侧属性面板。画布部分先用最原始的 VueFlow 组件渲染。2.2 用 VueFlow 渲染第一批节点和连线先造两个最简单的测试节点一个开始节点一个审批节点看看组件能否渲染。script setup import { ref } from vue; import { VueFlow } from vue-flow/core; import vue-flow/core/dist/style.css; import vue-flow/core/dist/theme-default.css; const nodes ref([ { id: start-1, type: start, position: { x: 100, y: 200 }, data: { label: 发起申请 } }, { id: approve-1, type: approve, position: { x: 400, y: 200 }, data: { label: 部门主管审批 } }, ]); const edges ref([ { id: e1-2, source: start-1, target: approve-1 }, ]); /script template div classflow-container VueFlow v-model:nodesnodes v-model:edgesedges / /div /template style scoped .flow-container { width: 100%; height: 800px; border: 1px solid #eee; } /style很多教程到这里就结束了但实际项目里你会发现直接这样渲染节点是带默认样式的灰色盒子。我们后续要做的自定义审批节点是靠nodeTypes注册进去的。简单说节点组件才是我们真正需要花时间的部分比如审批节点要展示当前处理人、抄送人、会签数等等。2.3 为什么 nodeTypes 不能放在渲染函数里这里有一个新手必踩的坑把nodeTypes定义在setup或者组件内部的对象上有时会导致节点不更新或者组件被反复重渲染。// 错误示例示意 function makeNodeTypes() { return { start: StartNode, approve: ApproveNode }; }原因是nodeTypes的值如果每次渲染都重新生成VueFlow 内部对比到引用变化就会重新挂载节点组件导致状态丢失。正确的做法是把它定义在script setup顶层的常量里import StartNode from ./nodes/StartNode.vue; import ApproveNode from ./nodes/ApproveNode.vue; const nodeTypes { start: StartNode, approve: ApproveNode };这样既能稳定引用又能按需加载。这一点对后面性能优化也非常关键。3. 企业级审批流的数据模型节点、边与业务语义画图只是表象真正难的是画布背后的数据模型。这节是整个项目最核心的部分我会拆开讲。3.1 审批业务节点类型表在设计画布之前先和业务方对齐节点类型。常见的审批流节点大致有六种节点类型节点标识核心能力画布表现开始节点start流程入口无入边圆角矩形审批节点approve指定审批人/角色支持会签或签矩形用户头像条件分支condition按表达式决定走哪条分支菱形抄送节点cc只读通知不阻塞流程矩形通知图标子流程节点subflow调用另一个流程实例矩形嵌套标识结束节点end流程结束无出边圆角矩形这个表定了节点组件设计也定了。我建议把节点类型和业务类型解耦节点类型负责长什么样业务类型负责字段含义。比如approve节点后端关心的是assigneeType角色还是人、assignee具体值、signType会签/或签这些全部塞进data。3.2 nodes / edges 的 JSON 结构设计先看我们最终保存到后端的结构真实结构比 demo 多了很多{ nodes: [ { id: node_start_001, type: start, position: { x: 120, y: 160 }, data: { label: 发起申请, formSchemaId: comp_leave, visible: true } }, { id: node_approve_001, type: approve, position: { x: 420, y: 160 }, data: { label: 部门主管审批, assigneeType: role, assignee: dept_manager, signType: single, timeout: 24 } } ], edges: [ { id: edge_001, source: node_start_001, target: node_approve_001, sourceHandle: source-right, targetHandle: target-left } ] }有几个设计细节值得注意。第一id必须有稳定的前缀。比如审批节点就用node_approve_*这样在日志排查和错误上报时一眼能看出是哪个类型出了问题。第二position一定用相对坐标。如果你把坐标存成相对画布中心的偏移在用户缩放后恢复会产生错位。直接存x/y配合fitView自动居中即可。第三data字段是业务配置的万能口袋。画布不关心你要会签还是要抄送它只负责把这包数据随节点一起渲染。我们约定data.label是所有节点组件都必须展示的字段这样属性面板可以统一处理名称修改。3.3 连线合法性校验把业务规则写进画布事件这个环节特别容易被忽略。流程图画出来当然好看但如果用户把结束节点又拉出一条线接到开始节点上后端引擎拿到这种图一定会炸。所以必须在前端拦截。VueFlow 提供了一个isValidConnection属性把它放到画布上VueFlow :is-valid-connectionvalidateConnection connecthandleConnect /校验函数的核心逻辑如下const nodeTypeRule { start: { in: 0, out: 1 }, end: { in: 1, out: 0 }, approve: { in: 1, out: 1 }, condition: { in: 1, out: 2 }, cc: { in: 1, out: 1 }, subflow: { in: 1, out: 2 }, }; function countConnected(nodeId, direction) { const edges getEdges.value; return edges.filter(e direction out ? e.source nodeId : e.target nodeId).length; } function validateConnection(connection) { const { source, target } connection; const srcNode getNode(source.value); const tgtNode getNode(target.value); if (!srcNode || !tgtNode) return false; if (srcNode.type end) return false; if (tgtNode.type start) return false; const srcOutCount countConnected(source.value, out); const tgtInCount countConnected(target.value, in); if (srcOutCount nodeTypeRule[srcNode.type].out) return false; if (tgtInCount nodeTypeRule[tgtNode.type].in) return false; return true; }这段代码把开始节点不能被连入、结束节点不能再连出、条件分支最多两条出边这类业务约束固化在画布交互层。用户拉线时如果不符合规则VueFlow 会自动让连线回弹并给出错误提示。这个体验比保存后再校验好得多。还有一个很实际的问题审批流里条件分支的出边和普通出边语义不一样。条件分支的两条出边每一条边上要做条件表达式标注。我的做法不是把表达式放到节点里而是放到边的data.condition里{ id: edge_cond_001, source: node_condition_001, target: node_approve_002, data: { condition: { field: amount, op: , value: 5000 } } }这样当后端引擎执行到条件分支时能准确知道哪条线是金额大于 5000哪条是小于等于 5000。3.4 嵌套树结构与引擎数据互相转换审批系统里后端引擎通常不认画布的平铺图结构而是树形嵌套结构。因此我们需要两个转换函数flowToBackend(nodes, edges): 从画布图结构转成树形结构。backendToFlow(backendTree): 从树形结构还原成画布图结构。核心转化逻辑就是从开始节点出发按出边深度遍历function flowToBackend(edges) { // 找到入度为 0 的节点作为根 const sourceIds new Set(edges.map(e e.source)); const targetIds new Set(edges.map(e e.target)); const rootId [...sourceIds].find(id !targetIds.has(id)); const buildTree (nodeId) { const outEdges edges.filter(e e.source nodeId); return { nodeId, condition: outEdges[0]?.data?.condition ?? null, children: outEdges.map(e buildTree(e.target)), }; }; return buildTree(rootId); }这个函数只是示意真实项目中还要处理条件分支的多线路、循环保护、孤儿节点检测等。但核心思路是一样的画布图数据是 UI 形态后端树形数据是执行形态两者之间要有明确的转换层而不是直接让后端去解析 UI 结构。转换层还有一个作用就是做schema 版本兼容。比如第一版data.assigneeType只有person第二版要加role转换层可以在新旧版本之间做字段映射避免因为前端迭代导致旧流程打不开。4. 交互核心从拖拽建节点到完整设计器体验数据模型设计好之后就要开始打磨交互体验。这节的重点是怎么让业务方能自己拖出一个流程包括左侧物料面板、右键菜单、撤销重做。4.1 左侧物料面板拖拽创建节点物料面板本质是一个节点类型清单用户把某个类型拖到画布上系统按坐标生成一个新节点。先给物料面板的每一项设置draggable拖拽时把节点类型信息塞进dataTransferdiv v-foritem in materialList :keyitem.type draggabletrue dragstart(e) onDragStart(e, item.type) {{ item.label }} /divfunction onDragStart(event, type) { event.dataTransfer.setData(application/flow-node, type); event.dataTransfer.effectAllowed move; }然后画布容器监听drop事件拿到鼠标位置后通过screenToFlowCoordinate转成画布坐标import { useVueFlow } from vue-flow/core; const { screenToFlowCoordinate, addNodes, getNodes } useVueFlow(); function onDrop(event) { const type event.dataTransfer.getData(application/flow-node); if (!type) return; const position screenToFlowCoordinate({ x: event.clientX, y: event.clientY, }); const newNode createNodeByType(type, position); addNodes([newNode]); }createNodeByType根据类型返回默认数据和默认尺寸比如审批节点默认宽 220 高 80条件分支默认宽 160 高 80。注意拖拽释放位置的坐标一定要用screenToFlowCoordinate做换算否则在画布缩放或平移之后新节点会出现在错误的位置。这个坑我项目上线后还被业务方反馈过一次后来在拖拽落点加了坐标偏移修正才彻底解决。4.2 选中节点后右侧属性面板联动编辑节点组件本身只负责渲染业务配置项统一放在右侧属性面板。实现思路是在画布上监听node-click把当前节点 id 存到selectedNodeId右侧面板通过getNode(selectedNodeId)拿到节点数据用v-model直接编辑node.data。const selectedNodeId ref(null); const { getNode, updateNode } useVueFlow(); function handleNodeClick({ node }) { selectedNodeId.value node.id; }属性面板模板大致是这样的div v-ifselectedNode label节点名称/label el-input v-modelselectedNode.data.label changepersistNode / template v-ifselectedNode.type approve label审批人类型/label el-select v-modelselectedNode.data.assigneeType el-option label指定成员 valueperson / el-option label指定角色 valuerole / /el-select label审批方式/label el-radio-group v-modelselectedNode.data.signType el-radio labelsingle单人审批/el-radio el-radio labelorSign或签/el-radio el-radio labelcountersign会签/el-radio /el-radio-group /template /div这里要注意对象的响应式问题。selectedNode如果直接赋值为node对象在部分场景下修改node.data.label不会触发视图更新。我在项目中遇到了这个问题解决方法是把属性面板绑定到一个 computed 上用setNodes统一更新const selectedNode computed(() { const nodes getNodes.value; return nodes.find(n n.id selectedNodeId.value); }); function persistNode(path, value) { const nodes getNodes.value.map(n { if (n.id selectedNodeId.value) { // 深拷贝一份再改确保响应式 const newNode { ...n, data: { ...n.data, [path]: value } }; return newNode; } return n; }); setNodes(nodes); }setNodes是 VueFlow 中替换整个节点数组的 API对大图会稍微重一点但在设计器场景几十到几百个节点完全可以接受换来的是响应式不出幺蛾子。4.3 操作容错与撤销重做撤销重做是可视化编辑器的标配但一直是被低估的难点。我的方案很简单粗暴在每次变更前深拷贝一份 nodes 和 edges 快照存到栈里。const undoStack []; const redoStack []; const MAX_STACK 30; function snapshot() { undoStack.push({ nodes: JSON.parse(JSON.stringify(getNodes.value)), edges: JSON.parse(JSON.stringify(getEdges.value)), }); if (undoStack.length MAX_STACK) undoStack.shift(); redoStack.length 0; } function undo() { const prev undoStack.pop(); if (!prev) return; redoStack.push({ nodes: JSON.parse(JSON.stringify(getNodes.value)), edges: JSON.parse(JSON.stringify(getEdges.value)), }); setNodes(prev.nodes); setEdges(prev.edges); } function redo() { const next redoStack.pop(); if (!next) return; undoStack.push({ nodes: JSON.parse(JSON.stringify(getNodes.value)), edges: JSON.parse(JSON.stringify(getEdges.value)), }); setNodes(next.nodes); setEdges(next.edges); }这个方案性能不算顶级但胜在简单可靠。JSON 深拷贝在处理大图时可能有一点卡可实际业务中一节线性审批流程最多二三十个节点完全没问题。有了这套快照机制后我在connect、node-drag-stop、删除节点、属性面板变更这些事件里统一调用snapshot()撤销覆盖了绝大多数编辑场景。4.3.1 保存校验防止画了半截的流程提交还有一个容易被忽略的环节保存前校验。用户可能拖了一个节点出来但还没连线或者漏了某个节点属性就直接点保存。这种脏数据一旦入库线上流程会无法启动。我的校验函数做三件事检查每个节点的入边出边是否满足规则表中的数量检查条件分支的每条出边是否配置了条件表达式检查是否存在孤立节点既没有入边也没有出边。如果有问题用一个 dialog 列表把具体节点和错误原因列出来引导用户回到画布修正。这个功能上线之后后端运维的同学松了一口气因为流程配置错误的工单少了大概一半。4.4 自动布局让人脑少算一层坐标真实业务中用户画出来的流程往往是歪歪扭扭的。他们自己看没问题但运营同学的领导喜欢看整齐的流程图。所以我在设计器里加了一个自动整理布局按钮用简单的分层算法来重排坐标。实现思路从开始节点出发做 BFS按节点在流程中的深度分层同层节点放在同一行y 坐标相同层内按照节点顺序从左往右排x 坐标递增节点之间的间距固定比如垂直方向 120水平方向 200。function autoLayout() { const nodes getNodes.value; const edges getEdges.value; const indegree new Map(nodes.map(n [n.id, 0])); edges.forEach(e indegree.set(e.target, (indegree.get(e.target) || 0) 1)); const queue nodes.filter(n indegree.get(n.id) 0); const layers []; const layerMap new Map(); while (queue.length) { const size queue.length; const layer []; for (let i 0; i size; i) { const node queue.shift(); layer.push(node); edges.filter(e e.source node.id).forEach(e { indegree.set(e.target, indegree.get(e.target) - 1); if (indegree.get(e.target) 0) queue.push(getNode(e.target)); }); } layers.push(layer); } layers.forEach((layer, index) { layer.forEach((node, i) { node.position { x: 100 i * 240, y: 80 index * 140, }; }); }); setNodes(nodes); fitView({ padding: 0.2 }); }这个算法对付绝大多数审批流够了。如果要更精细的减少连线交叉可以引入 dagre但对我来说投入产出比不高。自动整理之后画布好看太多而且每次新画流程都先用它兜底一劳永逸。5. 审批运行态可视化节点高亮与轨迹回放设计态搞定之后下一阶段就是把正在审批中的流程实例实时映射到图上。这也是业务方最看重的功能点开一个审批单一眼看出流程卡在哪个节点。5.1 从设计态切到实例运行态同一个流程图组件要同时服务设计态和运行态。我的切换方式是在 FlowDesigner 组件上加一个modepropconst props defineProps({ mode: { type: String, default: design }, // design | runtime flowData: { type: Object, default: () ({}) }, // 运行时后端数据 });设计态时画布可拖拽、可连线、可编辑运行态时所有节点组件变成只读展示但增加了状态颜色。VueFlow 本身没有只读模式这个整体开关所以我用两个思路组合实现画布上设置nodes-draggablefalse、nodes-connectablefalse、elements-selectabletrue自定义节点组件内部根据mode决定是否渲染操作按钮如删除、复制。5.2 节点状态映射与高亮样式后端返回的实例数据大致是这样的{ instanceId: flow_inst_001, currentNodeIds: [node_approve_001], finishedNodeIds: [node_start_001], cancelledNodeIds: [], records: [ { nodeId: node_start_001, status: completed, time: 2024-06-01 10:00:00 }, { nodeId: node_approve_001, status: processing, time: null } ] }前端拿到数据后统一把这些状态映射到节点数据里const runtimeStatusMap { completed: success, processing: running, cancelled: danger, waiting: default, }; function applyRuntimeData(runtimeData) { const nodes getNodes.value.map(node { const status runtimeData.records.find(r r.nodeId node.id)?.status || waiting; return { ...node, data: { ...node.data, runtimeStatus: runtimeStatusMap[status], }, }; }); setNodes(nodes); }节点组件里根据data.runtimeStatus渲染不同颜色template div :class[approve-node, approve-node--${data.runtimeStatus}] !-- 内容 -- /div /template style scoped .approve-node--running { border: 2px solid #f5a623; background: #fff7e6; } .approve-node--success { border: 2px solid #52c41a; background: #f6ffed; } .approve-node--danger { border: 2px solid #ff4d4f; background: #fff1f0; } /style连线也要跟着状态高亮。VueFlow 的边样式通过edge.style传递我们可以在运行时动态改线的颜色const edges getEdges.value.map(edge { const sourceNode runtimeData.finishedNodeIds.includes(edge.source); const targetNode runtimeData.currentNodeIds.includes(edge.target); if (sourceNode targetNode) { return { ...edge, animated: true, style: { stroke: #52c41a } }; } return edge; }); setEdges(edges);这样一眼就能看出哪个待办在闪动体验很直观。5.3 轨迹回放用计时器模拟审批流转除了实时状态我还做了一个轨迹回放小功能。用户点按钮后节点状态会按照后端记录的审批时间序列一步步推进。技术上很简单就是定时器配合状态切换let playTimer null; function replayFlow(records) { let index 0; if (playTimer) clearInterval(playTimer); playTimer setInterval(() { if (index records.length) { clearInterval(playTimer); return; } const current records[index]; const nodes getNodes.value.map(node { if (node.id current.nodeId) { return { ...node, data: { ...node.data, runtimeStatus: runtimeStatusMap[current.status] }, }; } return node; }); setNodes(nodes); index 1; }, 600); }回放的速度可以做成可调选项默认 600ms 一个节点。这个功能对业务方理解整个审批链路帮助很大尤其是做流程优化时可以在不真实发起审批的情况下一秒看出节点等待时间分布。6. 性能调优与后端链路落地的几个硬茬可视化编辑器做得再花哨到了生产环境就会被后面几个硬茬狠狠教育。这节聊聊我在性能和后端集成上踩过的坑。6.1 大流程图卡顿的排查与优化我的项目早期遇到过一个性能问题当流程节点数量超过 200 个或者边上挂了很多自定义属性时拖拽节点会有明显掉帧。排查后主要问题出在响应式滥用。VueFlow 的nodes和edges是响应式数组如果这些数组被外部组件以v-model方式双向绑定任何一丁点变更都会触发全量 diff。对于大图来说这个代价非常高。我的优化策略是设计器场景用小图展示器场景用大图两种模式用不同数据传递方式。设计器节点数一般 50 以内用v-model:nodes/v-model:edges开发体验最好。展示器 / 运行态大图节点数几百用:nodes/:edges单向传值然后手动监听nodes-change事件在外部同步数据避免双向绑定的全链路响应。对于自定义节点组件另一个优化点是避免所有节点都订阅全局状态。VueFlow 的每个节点组件的data都是响应式的如果业务在节点组件里使用了useVueFlow()或依赖了全局 store会因为细粒度响应而拖慢渲染。我的方案是节点组件尽量哑组件只接收id和data所有业务操作通过事件抛出去由父组件处理。还有一个小技巧如果节点类型很稳定可以用component :is配合defineAsyncComponent做异步组件懒加载。比如子流程节点只有在用户真正用到时才会加载对应的配置面板从而减少首屏体积。6.2 序列化恢复与脏检查前面提到过flowToBackend和backendToFlow转换但还有一个隐藏问题千万不能把运行时状态存回后端。比如用户打开了一个运行中的流程实例画布上节点是绿色的running状态。如果用户此时不小心点了一下保存而后端保存接口又恰好接收完整nodes数组那么这个绿色状态就被持久化到流程定义了。下次流程启动数据里会莫名多出运行时字段。我的解决办法是保存前做一次清洗只保留设计态需要的字段把runtimeStatus、active、selected这些瞬时字段全部剔除。function sanitizeBeforeSave(nodes) { return nodes.map(node { const { runtimeStatus, active, selected, ...rest } node; return { id: rest.id, type: rest.type, position: rest.position, data: rest.data, }; }); }同样的道理恢复流程时如果后端返回的数据里没有position字段就用fitView自动布局兜底。这样即使老流程的布局数据缺失也能在画布上正常展示。6.3 组件封装与多人协作开发落地最后一个环节是工程化落地。审批设计器不是一次性页面它往往会被多个模块复用比如报销审批配置、请假审批配置、合同审批配置。所以我把 FlowDesigner 封装成独立组件对外只暴露三个核心方法getSchema()获取当前画布对应的树形后端数据结构loadSchema(schema)载入一份后端数据并渲染成画布validate()执行一次全套校验。封装方式用defineExposedefineExpose({ getSchema: () flowToBackend(getEdges.value, getNodes.value), loadSchema: (schema) backendToFlow(schema), validate: () validateFlow(), });父组件只需要一个 ref 就能完成存取值和校验不需要关心画布内部的状态管理。多人协作方面我把nodeTypes对应的节点组件目录做成约定式结构src/flow/nodes/{TypeName}/index.vue每个节点目录自带config.js定义属性面板字段这样新增一种节点类型时开发人员几乎只写一个 Vue 组件加一份配置文件不需要改动主设计器代码。关于状态管理我一开始想把设计器状态放到 Pinia 里后来发现 VueFlow 内部本身已经有 store如果再用 Pinia 包一层同步逻辑很容易混乱。最后我选择让 FlowDesigner 组件自己持有一份状态只在保存和加载时通过getSchema/loadSchema与外部交换数据。这个模式在多个业务模块复用下来基本没有遇到状态不同步的问题。最后分享一个关于自定义节点设计的体会审批节点组件一定要预留状态插槽。业务方今天可能只要一个runtimeStatus颜色区分明天就可能要在节点上展示审批人头像、红色待办数字角标、甚至点击节点弹出审批记录抽屉。组件设计时把状态和业务内容分开后期扩展就不会伤筋动骨。如果你也在做类似的审批流程可视化改造我建议不要一上来就追求大而全的编辑器先把数据模型这只牛鼻子牵好再逐步叠交互和运行态能力。VueFlow 提供了很不错的画布底座但要让它服务于真正的企业级审批业务关键还是前置的流程规则梳理和结构设计。这一轮做完之后我对业务复杂度最终都会转化为数据模型复杂度这句话的理解又深了一层。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑