基于Vue3与Ant Design Vue的审批流设计器组件封装实践
2026/9/20 23:03:47 网站建设 项目流程

简介:一套基于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 class="canvas-wrapper" @wheel.prevent="onWheel"> <div class="canvas-inner" :style="{ transform: `translate(${pan.x}px, ${pan.y}px) scale(${scale})` }"> <svg class="flow-svg"> <path v-for="edge in edges" :key="edge.id" :d="edge.path" fill="none" stroke="#4f6fed" stroke-width="2" /> </svg> <FlowNode v-for="node in positionedNodes" :key="node.id" :node="node" @click="openConfig(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 一个真实审批流示例

我做了一个“费用报销”的示例流程来验证整套方案:

  1. 开始节点。
  2. 直属主管审批。
  3. 条件分支:金额小于等于5000,走财务审批;金额大于5000,走总监审批后回到财务复核。
  4. 审批结束后抄送申请人。
  5. 结束。

在界面上,这个流程会渲染成一条主树干带一个条件分支的结构。用户可以在条件节点上点“添加分组”,把大于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 就知道怎么落库”。流程引擎、权限模型、消息通知这些都可以各管各的,前端把流程设计这件事集中收敛好,整个系统复杂度会降一个档次。后续如果想继续做,我会考虑加入流程版本对比、节点级复制粘贴、审批链路模拟这三个能力,它们对真实业务场景的价值比继续加更多节点类型要大得多。

本文还有配套的精品资源,点击获取

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

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

立即咨询