尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Vue3与Ant Design Vue的审批流设计器组件封装实践

基于Vue3与Ant Design Vue的审批流设计器组件封装实践 简介一套基于Antdv的中国式工作流组件面向需要实现钉钉/飞书/雀书风格审批流程的中后台开发人员解决流程设计、审批办理、任务流转与状态跟踪等常见业务难题。支持在线流程设计器、会签/并行/串行/自由流、退回/转办/委托/撤回/作废等操作并具备智能提交、自由指定下一步处理人、全局表单与节点表单配置等能力可让同一流程模型挂接多种业务单据。压缩包共116个文件以48个Vue组件、21个JavaScript脚本为主辅以PNG预览图、JSON配置、样式表与说明文档整体仅709KB轻量易集成。已有1237人学习下载。通过源码目录可快速获取流程设计器、事件脚本、表单配置等模块便于直接复用或二次开发适合需要快速搭建合规、灵活的国产化审批流程的团队。1. 需求拆解这不是画一个流程图而是把审批习惯做成通用能力前几天有同事拿着内部 OA 的审批截图来找我发起一个报销单选好“部门主管审批”后系统还得支持“抄送财务”、“金额大于5000走总监审批”、“不加签不改单”这一连串规则。他问我能不能在现有后台里把这一套流程做成一个可拖拽、可配置的组件最好界面一眼看过去就让人想起钉钉、飞书、雀书里那种审批流设计器。这其实就是很多业务系统都踩过的坑做审批流时要么直接上一个厚重的流程引擎要么写死在代码里改一次流程要发一次版。对于前端团队来说与其买整套 BPM 平台不如基于 Vue 3 和 Ant Design Vue 封装一个“中国式工作流组件”会更可控。这篇文章记录的就是我最近这个组件从设计到落地的全过程重点讲数据模型怎么设计、自动布局怎么做、SVG 连线怎么算以及那些踩完才知道的坑。项目还在持续迭代但核心玩法已经跑通适合正在做审批、OA、低代码平台的团队参考。1.1 钉钉、飞书、雀书在交互上到底做对了什么我花了几天时间去梳理这三款产品的审批设计器表面看都是“拖几个节点、拉几条线”但真正做得好的是把复杂流程藏在简单的交互后面钉钉最擅长的是一种“极简模板感”。用户不需要理解什么 BPMN、什么事件网关打开就是一个从上往下的节点流审批人、抄送人、办理规则全部放在右侧配置面板里小白也能上手。飞书把“条件分支”做得很轻像在填一张表单而不是在画一张工程图。它允许你把不同分支并列排开视觉上不吓人。雀书则更偏企业垂直场景强调审批表单和流程的强绑定节点属性更细比如“同一部门自动跳过”“审批人为空时转交管理员”。落到我们自己的组件里可以提取三个共性有明确的纵向流程感、有轻量的条件分支表达、有可收敛的配置面板。这三条也直接决定了后续数据结构的设计方向。1.2 组件的能力边界和选型结论在动手前我给自己定了几个边界避免做成一锅粥不打算做成完整流程引擎。负责前端展示和编辑后端的流程引擎可以自己实现状态机或对接第三方。不打算复刻 BPMN 规范。BPMN 虽然标准但对普通业务人员来说太抽象我们要的是“看得懂、改得动”。不打算绑定后端字段格式。组件只产出和消费一份 JSON 结构后端只要按约定解析即可。技术选型上我最终选了 Vue 3 Ant Design Vue 4 TypeScript SVG。Antdv 负责表单控件、抽屉、按钮这些基础交互流程图和连线全部用 SVG 手工绘制。没有引入 dagre、antv X6 这类重型图引擎因为审批流的布局比通用图简单很多树状结构手动算坐标反而更可控包体积也更小。2. 数据模型设计用一棵树承载所有节点流程可视化只是表象真正决定组件好不好扩展的是数据模型。如果这一步走歪了后面加节点类型、加分支条件都会很痛。2.1 节点、边、条件分支该怎么抽象我用了一套很直观的模型每个节点是一个Node节点之间有Edge连接条件分支不是独立的边而是挂在节点内部的一个branches数组。type NodeType start | approval | cc | condition | end interface FlowNode { id: string type: NodeType name: string // 审批人、抄送人配置放在 attributes 里 attributes: { approvers?: string[] mode?: one | all | sequence // 或签、会签、依次审批 ccUsers?: string[] conditionGroups?: ConditionGroup[] } children?: FlowNode[] } interface ConditionGroup { id: string expression: string child: FlowNode }为什么用children而不是单独的edges因为审批流大多数情况是“一个节点往后只有一条主干只有条件节点才分叉”。用树结构表达天然符合人的阅读习惯遍历和布局都不需要做拓扑排序。真正的同层并行分支我也通过“多叉树”处理而不是引入“并行网关”这种概念。一个典型的流程用这份数据结构表示出来是这样{ id: node_start, type: start, name: 开始, children: [ { id: node_approval_1, type: approval, name: 直属主管审批, attributes: { mode: one }, children: [ { id: node_condition_1, type: condition, name: 金额判断, attributes: { conditionGroups: [ { id: g1, expression: amount 5000, child: { id: node_approval_2, type: approval, name: 财务审批 } }, { id: g2, expression: amount 5000, child: { id: node_approval_3, type: approval, name: 总监审批 } } ] } } ] } ] }2.2 为什么不用传统 BPMN 而用“极简 DSL”市面上成熟的工作流引擎大多基于 BPMN 2.0有 startEvent、userTask、exclusiveGateway、parallelGateway 等一套标准。标准是好但对一个内部管理系统来说往往会造成“杀鸡用牛刀”的尴尬。团队成员要额外学习一堆概念连线时还要避免不合法拓扑最后画出来的图未必符合业务直觉。我的做法是直接定义一套“极简 DSL”。它只有五种节点没有单独的网关节点条件分支被当成条件节点的一个属性。这样带来的好处很明显前端渲染时不需要解析复杂图结构递归 Tree 就能完成。后端如果要做持久化直接把 JSON 存库即可。用户拿到这套 JSON 即使不看文档也能猜出流程大致走向。代价是表达能力不如 BPMN 完整。比如并行分支、循环、子流程用这套模型实现起来会比较绕。但如果你的业务就是审批流、报销流、合同流这套模型已经覆盖了90%的日常场景。做组件不是越标准越好而是越贴业务越好。3. 渲染与交互核心难点拆解数据模型定了之后真正的硬骨头在渲染层。流程设计器不是把节点从上到下铺开就完事还得考虑分支错开、线条平滑、节点拖动、条件配置面板。这里挑三个最难的点展开说。3.1 审批流的自动布局算法布局算法决定了整个组件看起来专不专业。我的方案是深度优先遍历树先计算每个子树的宽度再决定父节点水平居中位置。节点宽度固定为160px节点高度48px垂直间距56px水平间距40px。伪代码大概是这样的function layout(node: FlowNode): LayoutResult { if (!node.children?.length) { return { width: NODE_WIDTH, positions: [{ id: node.id, x: 0, y: 0 }] } } // 先给所有子节点布局 const childResults node.children.map((child) layout(child)) const totalWidth childResults.reduce((sum, r) sum r.width H_GAP, 0) - H_GAP let offsetX -totalWidth / 2 const positions [] for (const childResult of childResults) { // 把子节点坐标平移到正确 offset childResult.positions.forEach((p) { positions.push({ ...p, x: p.x offsetX, y: p.y V_GAP NODE_HEIGHT }) }) offsetX childResult.width H_GAP } positions.push({ id: node.id, x: 0, y: 0 }) return { width: Math.max(totalWidth, NODE_WIDTH), positions } }拿到这棵树的坐标后再根据根节点坐标做一次全局平移让所有节点落在画布正中心。这里有个容易被忽略的细节如果某个分支节点特别深子树会非常宽容易出现节点超出可视区域。所以我在外层套了一个transform容器支持缩放和平移初始缩放比例根据画布宽度动态计算。3.2 用 SVG 画连接线(曲线怎么算)节点坐标有了画线就用SVG path。我实现了两种线型垂直折线和平滑曲线。默认用曲线曲线在视觉上更柔和和钉钉的交互风格更贴近。曲线我用的是三次贝塞尔function buildCurvePath(from: Point, to: Point): string { const offset Math.max(20, Math.abs(to.y - from.y) / 2) return M ${from.x} ${from.y} C ${from.x} ${from.y offset}, ${to.x} ${to.y - offset}, ${to.x} ${to.y} }起点和终点都取节点左右两侧的中点。如果from.x to.x贝塞尔曲线看起来接近直线如果父子和子节点的 x 坐标相差比较大曲线会自动出现一个弯曲过渡。条件分支的三叉线我单独做了处理从条件节点底部引出一条竖线到分支高度后水平分成左右两段再分别往下进入子节点。这种“先汇总、再分流”的视觉语言用户很熟悉不用多解释。实现时其实就是在branchY高度上算几个折点然后拼成M ... L ... L ...的路径。3.3 节点编辑器与操作按钮闭环流程设计器不能光能看还得能改。我参照飞书的交互把节点编辑器做成右侧抽屉点击节点后弹出。编辑器内容根据节点类型变化approval节点选择审批人支持用户、角色、部门、选择多人审批模式或签/会签/依次审批。cc节点选择抄送人。condition节点配置多个条件分组每个分组里可以填表达式也可以内嵌子节点。start/end节点只做展示不做配置。节点下方的操作栏放了四个按钮添加审批人、添加抄送人、添加条件分支、删除节点。点击“添加条件分支”时会自动在条件节点下追加一个分组并给新分组初始化一个子节点。这些操作本质都是对树结构做增删改操作完重新调用一次布局函数即可不需要额外维护复杂的状态。4. 实操手记从 Demo 到可复用组件理论讲完我们直接看代码。这个组件我没有拆得特别碎核心就是一个ApprovalFlowDesigner.vue内部由FlowCanvas,FlowNode,ConfigDrawer,useFlowLayout四块组成。4.1 组件目录与 Antdv 封装粒度src/components/ApprovalFlowDesigner/ ├── index.vue # 对外入口 ├── types.ts # 数据类型定义 ├── useFlowLayout.ts # 布局逻辑 ├── FlowCanvas.vue # 画布负责 SVG 和节点渲染 ├── FlowNode.vue # 单个节点卡片 └── ConfigDrawer.vue # 右侧属性配置抽屉index.vue对外暴露的 Props 很简单defineProps{ modelValue: FlowNode users?: UserOption[] roles?: RoleOption[] }() const emit defineEmits{ (e: update:modelValue, value: FlowNode): void }()通过v-model双向绑定整个流程树使用方不用关心内部怎么改。这样封装的好处是组件可以嵌入到任何表单弹窗或页面里不侵入业务代码。4.2 关键代码片段与配置项画布容器用了一个transform状态来控制缩放和平移div classcanvas-wrapper wheel.preventonWheel div classcanvas-inner :style{ transform: translate(${pan.x}px, ${pan.y}px) scale(${scale}) } svg classflow-svg path v-foredge in edges :keyedge.id :dedge.path fillnone stroke#4f6fed stroke-width2 / /svg FlowNode v-fornode in positionedNodes :keynode.id :nodenode clickopenConfig(node) / /div /div滚轮缩放时我把wheel事件默认行为拦掉以光标位置为中心缩放。这里有个细节缩放中心不能直接用鼠标在页面上的坐标要先换算成画布坐标系否则会出现“越缩放节点跑得越远”的诡异现象。换算公式如下const rect wrapper.getBoundingClientRect() const mouseX e.clientX - rect.left const mouseY e.clientY - rect.top const worldX (mouseX - pan.x) / scale const worldY (mouseY - pan.y) / scale scale newScale pan.x mouseX - worldX * newScale pan.y mouseY - worldY * newScale配置项我预留了defaultZoom,canZoom,canPan,showMiniMap几个开关后续还可以扩展主题配色和节点尺寸。但要注意组件刚上线时少搞配置先满足一个场景等第二个场景需要时再抽接口。4.3 一个真实审批流示例我做了一个“费用报销”的示例流程来验证整套方案开始节点。直属主管审批。条件分支金额小于等于5000走财务审批金额大于5000走总监审批后回到财务复核。审批结束后抄送申请人。结束。在界面上这个流程会渲染成一条主树干带一个条件分支的结构。用户可以在条件节点上点“添加分组”把大于5000的分支再拆成“总监审批”和“财务复核”两个连续节点。整棵树的 JSON 直接作为v-model的值提交给后端后端按节点类型解析即可。5. 常见问题与排查技巧实录这个组件从雏形到现在我踩了不少坑。有些问题一开始很难定位但只要理解了布局和状态的底层逻辑基本都能解决。5.1 节点坐标错乱或重叠怎么办常见原因有两个一是布局时没有为每个节点生成独立 ID节点更新后v-for的 key 复用导致 DOM 状态错乱二是条件分支里如果child节点为空布局函数会默认给它一个空宽度导致两侧分支重叠。我的解决办法是所有节点包括条件分组里的子节点都要保证在createNode()时生成唯一id。布局函数里遇到空子节点先补一个占位宽度避免计算总宽度时出错。另一个经验是布局后做一次节点位置去重同一个坐标上如果出现两个节点立即抛警告方便尽早发现问题。5.2 SVG 线条方向不对怎么调用贝塞尔曲线时最怕父子节点高度差很小但水平间距很大。此时曲线会在中间出现“反向弯曲”看起来像打了个结。我的经验是曲线偏移量不要只按高度差算还要加上水平距离的权重。更稳妥的做法是设置一个最小偏移值比如Math.max(40, Math.abs(dy) / 2 Math.abs(dx) / 4)曲线基本不会出现畸变。另外如果你后续想支持节点拖动位置线方向会跟随实时变化这时建议用requestAnimationFrame批量更新 path避免每移动一像素就触发一次 SVG 重绘。5.3 审批人选择和表单联动要注意什么很多审批流不是画完图就完事还得跟表单字段联动。比如“金额大于5000”的条件判断需要能选择表单里的某个字段。这里有个容易踩的坑不要在组件内部硬编码业务字段而是通过配置项传入可用的表单字段列表让用户在条件编辑器里自己选择。我在ConditionEditor里只负责渲染字段下拉框、运算符下拉框和值输入框生成一个表达式字符串语义校验全部交给后端。前端如果要做实时校验也只需要检查表达式是否完整不要试图去解析所有表达式逻辑不然这个组件会越做越重。还有一个细节审批人配置往往是“用户/角色/部门”混合的这里建议提交时用统一结构{ type: user | role | dept, id: string, name: string }不要只存一个字符串 ID否则回显的时候很痛苦。5.4 与外部系统集成时的状态边界最后提一点很多人都忽略的问题流程设计器拿到的树结构可能来自不同版本的后端接口。做组件时一定要在props入口做一次数据清洗和版本兼容比如把旧的branchs字段自动转成新版的conditionGroups。这部分代码虽然不显眼但能省掉很多线上问题。如果是和钉钉这类平台对接还要注意人员数据同步的频率。我在项目里每隔十分钟同步一次组织和人员缓存组件里只做搜索和展示不直接调平台接口避免前端出现权限或频率限制问题。在我自己的实践体会里这个组件最让我满意的不是渲染效果而是最后能做到“后端只需要看 JSON 就知道怎么落库”。流程引擎、权限模型、消息通知这些都可以各管各的前端把流程设计这件事集中收敛好整个系统复杂度会降一个档次。后续如果想继续做我会考虑加入流程版本对比、节点级复制粘贴、审批链路模拟这三个能力它们对真实业务场景的价值比继续加更多节点类型要大得多。本文还有配套的精品资源点击获取
返回列表