Vue仿钉钉审批流程:从零搭建可视化工作流引擎
1. 为什么需要可视化审批流程
最近在做一个企业内部管理系统,客户明确提出要仿照钉钉的审批流程功能。刚开始我觉得这有什么难的,不就是几个表单提交吗?真正动手做的时候才发现,钉钉的审批流设计确实精妙。比如部门经理审批后自动流转到财务,金额超过1万需要总经理签字,这些规则配置起来非常灵活。
传统审批流程的代码通常硬编码在系统里,每次业务规则变更都需要开发人员修改代码。而可视化工作流引擎允许非技术人员通过拖拽方式配置流程,就像搭积木一样简单。我见过最夸张的例子是某公司HR小姐姐自己配置了20多种请假审批流程,完全没麻烦技术部门。
用Vue来实现这类功能特别合适,它的响应式特性和组件化开发模式,能让我们快速构建可交互的节点元素。下面这个最简单的审批流示例,包含开始、审批人和结束三个节点:
<template> <div class="workflow-container"> <StartNode /> <ApproverNode :approvers="['张三','李四']" /> <EndNode /> </div> </template>2. 项目初始化与基础结构搭建
2.1 创建Vue项目与安装依赖
建议使用Vue CLI脚手架初始化项目,我习惯用Vite因为它启动速度更快。必须安装的依赖有:
vuedraggable:实现节点拖拽功能的核心库element-ui:采用它的弹窗和表单组件jsplumb:连接线的绘制工具
npm create vite@latest workflow-engine --template vue cd workflow-engine npm install vuedraggable element-plus jsplumb2.2 设计页面布局结构
参考钉钉的界面,我把工作区划分为三个区域:
- 左侧面板:存放各种节点类型的组件
- 中间画布:拖拽编排流程的主要区域
- 右侧配置面板:点击节点时显示详细设置
关键CSS技巧是给画布容器设置transform-origin: 0 0配合缩放控制,这样放大缩小时所有元素能保持一致比例。下面是核心布局代码:
<div class="designer-container"> <div class="node-panel"> <NodeItem v-for="item in nodeTypes" :type="item.type" /> </div> <div class="flow-canvas" ref="canvas"> <Draggable v-model="nodes" @end="onDragEnd"> <TransitionGroup> <NodeWrapper v-for="node in nodes" :node="node" /> </TransitionGroup> </Draggable> </div> <div class="config-panel" v-if="selectedNode"> <NodeConfig :node="selectedNode" /> </div> </div>3. 核心功能实现细节
3.1 实现节点拖拽与连线
拖拽功能用vuedraggable实现起来特别简单,但有几个坑我踩过:
- 需要给拖拽容器设置
group属性区分来源 - 拖拽结束时需要计算在画布上的准确位置
- 用
jsplumb连线时要等DOM渲染完成
节点连线的核心代码:
import { jsPlumb } from 'jsplumb' export default { mounted() { this.jsPlumb = jsPlumb.getInstance() this.jsPlumb.ready(() => { this.jsPlumb.setContainer(this.$refs.canvas) // 设置连线样式 this.jsPlumb.importDefaults({ Connector: ['Flowchart', { stub: 20 }], PaintStyle: { strokeWidth: 2 } }) }) }, methods: { connectNodes(sourceId, targetId) { this.jsPlumb.connect({ source: sourceId, target: targetId, anchors: ['Bottom', 'Top'] }) } } }3.2 审批人设置模块开发
钉钉最实用的功能是可以按组织架构选择审批人,我们简化实现如下功能:
- 指定具体人员
- 按角色选择(如部门主管)
- 连续多级审批
用Element UI的Tree组件展示组织架构:
<el-tree :data="deptTree" show-checkbox node-key="id" :props="{label: 'name'}" @check-change="handleCheckChange" />后台需要返回这样的树形数据:
[ { "id": 1, "name": "研发部", "children": [ {"id": 101, "name": "前端组", "type": "dept"}, {"id": 102, "name": "后端组", "type": "dept"} ] } ]4. 条件分支与流程控制
4.1 实现条件分支逻辑
钉钉的条件分支可以基于表单字段值进行路由判断,比如:
- 请假类型为年假 → 人事备案
- 金额大于5000 → 财务审核
我们通过动态渲染条件表单来实现:
<template v-if="node.type === 'condition'"> <el-form> <el-form-item label="条件名称"> <el-input v-model="node.name" /> </el-form-item> <el-form-item label="判断字段"> <el-select v-model="node.field"> <el-option v-for="field in formFields" :label="field.label" :value="field.prop" /> </el-select> </el-form-item> <el-form-item label="比较方式"> <el-select v-model="node.operator"> <el-option value=">" label="大于" /> <el-option value="<" label="小于" /> </el-select> </el-form-item> </el-form> </template>4.2 流程校验与保存
在保存前需要做这些验证:
- 所有审批节点必须设置审批人
- 条件分支必须有默认路由
- 不能存在孤立节点
校验通过后生成的流程数据格式示例:
{ "nodes": [ { "id": "node1", "type": "approver", "approvers": ["user1", "user2"], "position": { "x": 100, "y": 50 } } ], "edges": [ { "source": "node1", "target": "node2", "condition": "amount > 5000" } ] }5. 性能优化与实践建议
5.1 大型流程的性能问题
当节点超过50个时,我遇到过这些性能瓶颈:
- jsPlumb连线渲染卡顿
- Vue响应式数据更新变慢
- 缩放操作不流畅
优化方案:
- 使用
v-memo缓存静态节点 - 对jsPlumb采用批量渲染
- 缩放时隐藏连线
const onZoom = (scale) => { this.jsPlumb.setSuspendDrawing(true) this.scale = scale this.$nextTick(() => { this.jsPlumb.setSuspendDrawing(false) }) }5.2 与后端交互的注意事项
前后端协作时容易出问题的地方:
- 节点ID生成策略(建议前端生成UUID)
- 位置信息保存比例而非绝对值
- 版本控制机制
推荐的后端接口设计:
// 保存流程 POST /api/workflow/:id { "name": "请假审批", "nodes": [...], "edges": [...], "version": "2023-08-20" } // 获取流程 GET /api/workflow/:id/version/:version6. 扩展功能开发思路
6.1 抄送与通知功能
除了审批节点,还可以实现:
- 邮件通知抄送人
- 企业微信消息推送
- 审批超时提醒
通知配置组件示例:
<el-form-item label="通知方式"> <el-checkbox-group v-model="notifyTypes"> <el-checkbox label="email">邮件</el-checkbox> <el-checkbox label="sms">短信</el-checkbox> </el-checkbox-group> </el-form-item>6.2 移动端适配方案
为了让流程在手机上也能配置,需要:
- 简化拖拽操作(改为点击添加)
- 优化连线显示方式
- 使用手势缩放
媒体查询示例:
@media (max-width: 768px) { .node { width: 80px; padding: 5px; } .node-config { position: fixed; top: 0; width: 100%; } }这个项目从零开始搭建用了两周时间,最复杂的部分是条件分支的逻辑处理。建议先用简单的直线流程跑通核心功能,再逐步添加高级特性。如果遇到jsPlumb连线异常的问题,记住一定要在DOM更新完成后调用repaintEverything方法。
