一个完整的流程表单流转

1.写在前面

一个完整的流程表单审批(起表单-->各环节审批-->回退-->重新审批-->完成),前端由Vue2+js+Element UI升级为Vue3+ts+Element Plus,后端流程框架使用Flowable,项目参考了ruoyi-vue-pro(https://gitee.com/zhijiantianya/ruoyi-vue-pro)项目。

2.视频演示

3.表单

3.1表单的设计

依据业务需求,完全自定义表单,可以依据流程节点设置表单中每个属性的读写,实现原理是读取审批节点的编码,依据编码控制每个属性的读写。

<el-tree-select:disabled="!required"v-model="form.deptCode":data="deptList":props="defaultPropsForData"check-strictlynode-key="id"placeholder="请选择集团成员单位"/>/*** 根据不同参数控制表单属性的可读可写* @param pattern*/
const changeRequiredByPattern = (pattern: string) => {if (pattern === 'create') {//说明增加required.value = true} else if (pattern === 'update' || pattern === 'starter') {//修改required.value = truegetBid()} else {//只读required.value = falsegetBid()}
}

3.2.表单与流程关联

通过设计流程时定义的流程编码,在创建表单的后端服务调用中,实现表单与流程的绑定。

/*** 投标对应的流程定义 KEY*/public static final String PROCESS_KEY = "bidApproval";//发起BPM流程Map<String, Object> processInstanceVariables = new HashMap<>();processInstanceVariables.put("deptId", bidDO.getDeptId());processInstanceVariables.put("bidMoney", bidDO.getBidMoney());String processInstanceId = processInstanceApi.createProcessInstance(getLoginUserId(),new BpmProcessInstanceCreateReqDTO().setProcessDefinitionKey(PROCESS_KEY).setVariables(processInstanceVariables).setBusinessKey(bidDO.getProjectName()));

3.3.表单与前端流程实例的关联

在通过待办打开需要审批的表单时,不同的流程实例如何对应不同的表单,并在页面展示具体的表单数据了?答案是通过定义流程时填写的表单组件名称,利用Vue的<component>元组件来实现。

<componentref="formDetailRef"v-if="processInstance.id !== undefined":is="processInstance.processDefinition.formComponentName":processInstanceId="processInstance.id":pattern="runningTasks.length > 0 ? runningTasks[0].definitionKey : 'readOnly'"@success="getDetail"/>

3.4.表单的保存

表单的保存分为提交时的保存与不提交的保存。不提交的保存用于修改数据但流程不需要提交到下一节点审批的情况,方便保存数据进入待办里面进行后续的修改。提交的时,流程会自动调用保存接口,先进行业务数据的保存,然后再进行流程的提交。

<el-button color="#626aef" @click="handleSave"><Icon icon="ep:coin" />保存</el-button><el-button type="success" @click="handleApproval(item)"><Icon icon="ep:select" />提交</el-button>/** 处理保存表单的操作 只更新表单数据 不提交流程任务 */
const formDetailRef = ref()
const handleSave = () => {formDetailRef.value.submitForm()
}/** 处理审批通过的操作 */
const approvalRef = ref()
const handleApproval = async (item) => {approvalRef.value.open(item)
}
@Override@Transactional(rollbackFor = Exception.class)public Long updateBid(BidUpdateReqVO updateReqVO) {BidDO bidDO = BidConvert.INSTANCE.convert3(updateReqVO);//checkDeptIsMateTendererDept(deptRespDTO, tendererDeptRespDTO);bidDO.setDeptId(findDeptByCode(updateReqVO.getDeptCode()).getId());bidDO.setTendererId(findDeptByName(updateReqVO.getTendererName()).getId());fileApi.deleteFile(bidMapper.selectById(bidDO.getId()).getFiles(), bidDO.getFiles());bidMapper.updateById(bidDO);return bidDO.getId();}@Override@Transactional(rollbackFor = Exception.class)public void approveProcessTask(BidUpdateReqVO bidUpdateReqVO, BpmProcessTaskApprovalDTO bpmProcessTaskApprovalDTO) {//需要根据流程中不同的节点 更改对应的表单信息 比如 需要在流程最后一个节点点击提交时 更改流程状态为完成BidDO bidDO = BidConvert.INSTANCE.convert3(bidUpdateReqVO);bidDO.setDeptId(findDeptByCode(bidUpdateReqVO.getDeptCode()).getId());bidDO.setTendererId(findDeptByName(bidUpdateReqVO.getTendererName()).getId());updateFlowInfoByProcessInstanceState(bidDO);fileApi.deleteFile(bidMapper.selectById(bidDO.getId()).getFiles(), bidDO.getFiles());bidMapper.updateById(bidDO);bpmProcessTaskApi.approvalTask(bpmProcessTaskApprovalDTO);}

4.流程的审批

流程的审批按照设计流程的审批节点依次进行流转,不支持夸环节提交,支持流程的自由回退。提交当前审批任务时,进行下一节点的人员选择,在流程设计时,每个节点审批的人员的选择逻辑已经确定,也支持自由选择组织中的所有人员。

4.1.流程审批人员的设置

流程审批节点的人员设置主要思路为给定一个角色,让审批人员提交任务时,从角色中选择一个人员,这样可以缩小选择的范围。如果表单有对应的部门属性,可以设置审批人员是某个角色中且部门与表单部门属性相同的人员。

// 选择角色中的人员
private Set<Long> calculateTaskCandidateUsersByRole(BpmTaskAssignRuleDO rule) {return permissionApi.getUserRoleIdListByRoleIds(rule.getOptions());}//依据角色选择表单部门中的所属人员
private Set<Long> calculateTaskCandidateUsersByRolePerson(Map<String, Object> variables, BpmTaskAssignRuleDO rule) {Long deptId = (Long) variables.get("deptId");//获取流程实例变量的部门//从角色中获取属于该部门的人员Set<Long> userIdsByRoleId = permissionApi.getUserRoleIdListByRoleIds(rule.getOptions());Set<Long> userIdsByDeptId = userApi.getUsersByDeptId(deptId);return new HashSet<>(CollUtil.intersection(userIdsByDeptId, userIdsByRoleId));}

4.2.流程的流转

流程的流转其实没有什么好说的,就是按照流程设计的审批节点依次往下走,遇到网关时,根据前期设计好的条件读取对应的属性跳转到不同审批支线。本例中,会根据投标金额是否大于500万做判断,走不同的分支,而500万的属性,在创建流程时就已经传入了流程实例的变量中。

流程的回退,流程的回退依据流程的节点图,不管流程流转了多少圈,回退只允许回退当前审批节点的前面节点。

public Set<BpmDoneUserTaskNodeRespVO> getDoneUserTaskNodes(String taskId) {Set<BpmDoneUserTaskNodeRespVO> resultList = new HashSet<>();//获取流程实例idTask task = getTask(taskId);// 校验流程实例存在ProcessInstance instance = processInstanceService.getProcessInstance(task.getProcessInstanceId());//获取历史任务实例 条件为 流程实例 未完成 按照任务开始时间降序排列List<HistoricTaskInstance> historicTaskInstances = historyService.createHistoricTaskInstanceQuery().processInstanceId(instance.getProcessInstanceId()).finished().orderByHistoricTaskInstanceEndTime().desc().list();//需要做一个筛选,只能选择当前任务节点之前的节点进行回退// 1. 获取流程模型实例 BpmnModelBpmnModel bpmnModel = bpmProcessDefinitionService.getBpmnModel(task.getProcessDefinitionId());// 2. 通过任务节点id,来获取当前节点信息FlowElement flowElement = bpmnModel.getFlowElement(task.getTaskDefinitionKey());// 3.获取下一个节点(或者多个节点的)信息,需要去重,因为并行的节点之前的节点会找多遍Set<FlowElement> flowElements = new HashSet<>();// 4.获取流程实例的变量Map<String, Object> variables = taskService.getVariables(taskId);getBeforeNodes(flowElement, flowElements, variables);if (flowElements.isEmpty()){//说明处于第一个节点,此时不能回退throw exception(TASK_ROLLBACK_FORBIDDEN);}//找交集for(FlowElement f: flowElements) {HistoricTaskInstance h = historicTaskInstances.stream().filter( hi -> hi.getTaskDefinitionKey().equals(f.getId())).findFirst().orElse(null);if (h == null){//没有获取到最新的的节点审批信息,是不正常的情况throw exception(TASK_ROLLBACK_APPROVED_INFO_NULL);}//获取 审批人员的编号Long assignee = Long.valueOf(h.getAssignee());//获取人员信息UserRespDTO userRespDTO = adminUserApi.getUser(assignee);//获取部门信息DeptRespDTO deptRespDTO = deptApi.getDept(userRespDTO.getDeptId());resultList.add(BpmTaskConvert.INSTANCE.convertBpmDoneUserTaskNodeRespVO(h, userRespDTO, deptRespDTO));}return resultList;}

5.流程审批详情

流程审批详情包括审批记录与流程图的展示。

5.1.审批记录详情

审批记录根据审批的先后顺序展示数据,状态根据提交回退的不同使用不同颜色的标签显示。

<el-table v-loading="loading" :data="tasks" border><el-table-column align="center" prop="name" label="审批环节" width="200" /><el-table-column align="center" prop="assigneeUser.nickname" label="审批人" width="180" /><el-table-columnlabel="任务开始时间"align="center"prop="createTime"width="180":formatter="dateFormatter"/><el-table-columnlabel="任务结束时间"align="center"prop="endTime"width="180":formatter="dateFormatter"/><el-table-column label="任务耗时" align="center" prop="durationInMillis" width="150"><template #default="scope"><span>{{ formatPast2(scope.row.durationInMillis) }}</span></template></el-table-column><el-table-column label="审批结果" align="center" prop="result" width="150"><template #default="scope"><el-tag :type="getTimelineItemType(scope.row)">{{ getDictLabel(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT, scope.row.result) }}</el-tag></template></el-table-column><el-table-column label="审批意见" align="center" prop="reason" width="250" /></el-table>
public List<BpmTaskRespVO> getTaskListByProcessInstanceId(String processInstanceId) {// 获得任务列表List<HistoricTaskInstance> tasks = historyService.createHistoricTaskInstanceQuery().processInstanceId(processInstanceId).orderByHistoricTaskInstanceStartTime().desc() // 创建时间倒序.list();if (CollUtil.isEmpty(tasks)) {return Collections.emptyList();}// 获得 TaskExtDO MapList<BpmTaskExtDO> bpmTaskExtDOs = taskExtMapper.selectListByTaskIds(convertSet(tasks, HistoricTaskInstance::getId));Map<String, BpmTaskExtDO> bpmTaskExtDOMap = convertMap(bpmTaskExtDOs, BpmTaskExtDO::getTaskId);// 获得 ProcessInstance MapHistoricProcessInstance processInstance = processInstanceService.getHistoricProcessInstance(processInstanceId);// 获得 User MapSet<Long> userIds = convertSet(tasks, task -> NumberUtils.parseLong(task.getAssignee()));userIds.add(NumberUtils.parseLong(processInstance.getStartUserId()));Map<Long, UserRespDTO> userMap = adminUserApi.getUserMap(userIds);// 获得 Dept MapMap<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), UserRespDTO::getDeptId));// 拼接数据return BpmTaskConvert.INSTANCE.convertList3(tasks, bpmTaskExtDOMap, processInstance, userMap, deptMap);}

5.2.流程图显示

流程图显示比较复杂,可以查看对应的代码,主要就是使用了bpmn-js库,根据后端的数据,进行不同的展示,核心代码个人理解是这一块。

const highlightDiagram = async () => {const activityList = activityLists.valueif (activityList.length === 0) {return}// 参考自 https://gitee.com/tony2y/RuoYi-flowable/blob/master/ruoyi-ui/src/components/Process/index.vue#L222 实现// 再次基础上,增加不同审批结果的颜色等等let canvas = bpmnModeler.get('canvas')let todoActivity: any = activityList.find((m: any) => !m.endTime) // 找到待办的任务let endActivity: any = activityList[activityList.length - 1] // 获得最后一个任务// debuggerbpmnModeler.getDefinitions().rootElements[0].flowElements?.forEach((n: any) => {let activity: any = activityList.find((m: any) => m.key === n.id) // 找到对应的活动if (!activity) {return}if (n.$type === 'bpmn:UserTask') {// 用户任务// 处理用户任务的高亮const task: any = taskList.value.find((m: any) => m.id === activity.taskId) // 找到活动对应的 taskIdif (!task) {return}// 高亮任务canvas.addMarker(n.id, getResultCss(task.result))// 如果非通过,就不走后面的线条了if (task.result !== 2) {return}// 处理 outgoing 出线const outgoing = getActivityOutgoing(activity)outgoing?.forEach((nn: any) => {// debuggerlet targetActivity: any = activityList.find((m: any) => m.key === nn.targetRef.id)// 如果目标活动存在,则根据该活动是否结束,进行【bpmn:SequenceFlow】连线的高亮设置if (targetActivity) {canvas.addMarker(nn.id, targetActivity.endTime ? 'highlight' : 'highlight-todo')} else if (nn.targetRef.$type === 'bpmn:ExclusiveGateway') {// TODO 芋艿:这个流程,暂时没走到过canvas.addMarker(nn.id, activity.endTime ? 'highlight' : 'highlight-todo')canvas.addMarker(nn.targetRef.id, activity.endTime ? 'highlight' : 'highlight-todo')} else if (nn.targetRef.$type === 'bpmn:EndEvent') {// TODO 芋艿:这个流程,暂时没走到过if (!todoActivity && endActivity.key === n.id) {canvas.addMarker(nn.id, 'highlight')canvas.addMarker(nn.targetRef.id, 'highlight')}if (!activity.endTime) {canvas.addMarker(nn.id, 'highlight-todo')canvas.addMarker(nn.targetRef.id, 'highlight-todo')}}})} else if (n.$type === 'bpmn:ExclusiveGateway') {// 排它网关// 设置【bpmn:ExclusiveGateway】排它网关的高亮canvas.addMarker(n.id, getActivityHighlightCss(activity))// 查找需要高亮的连线let matchNN: any = undefinedlet matchActivity: any = undefinedconst outgoing = getActivityOutgoing(activity)outgoing.forEach((nn: any) => {let targetActivity = activityList.find((m: any) => m.key === nn.id)if (!targetActivity) {return}// 特殊判断 endEvent 类型的原因,ExclusiveGateway 可能后续连有 2 个路径://  1. 一个是 UserTask => EndEvent//  2. 一个是 EndEvent// 在选择路径 1 时,其实 EndEvent 可能也存在,导致 1 和 2 都高亮,显然是不正确的。// 所以,在 matchActivity 为 EndEvent 时,需要进行覆盖~~if (!matchActivity || matchActivity.type === 'endEvent') {matchNN = nnmatchActivity = targetActivity}})if (matchNN && matchActivity) {canvas.addMarker(matchNN.id, getActivityHighlightCss(matchActivity))}} else if (n.$type === 'bpmn:ParallelGateway') {// 并行网关// 设置【bpmn:ParallelGateway】并行网关的高亮canvas.addMarker(n.id, getActivityHighlightCss(activity))const outgoing = getActivityOutgoing(activity)outgoing.forEach((nn: any) => {// 获得连线是否有指向目标。如果有,则进行高亮const targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id)if (targetActivity) {canvas.addMarker(nn.id, getActivityHighlightCss(targetActivity)) // 高亮【bpmn:SequenceFlow】连线// 高亮【...】目标。其中 ... 可以是 bpm:UserTask、也可以是其它的。当然,如果是 bpm:UserTask 的话,其实不做高亮也没问题,因为上面有逻辑做了这块。canvas.addMarker(nn.targetRef.id, getActivityHighlightCss(targetActivity))}})} else if (n.$type === 'bpmn:StartEvent') {// 开始节点 流程只要发起 开始节点就是完成状态let targetActivity = activityList.find((m) => m.key === n.id)if (targetActivity) {canvas.addMarker(n.id, 'highlight') // 高亮【bpmn:StartEvent】开始节点(自己)}// 开始节点const outgoing = getActivityOutgoing(activity)outgoing.forEach((nn) => {// outgoing 例如说【bpmn:SequenceFlow】连线// 获得连线是否有指向目标。如果有,则进行高亮let targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id)if (targetActivity) {canvas.addMarker(nn.id, 'highlight') // 高亮【bpmn:SequenceFlow】连线canvas.addMarker(n.id, 'highlight') // 高亮【bpmn:StartEvent】开始节点(自己)}})} else if (n.$type === 'bpmn:EndEvent') {// 结束节点if (!processInstance.value || processInstance.value.result === 1) {return}canvas.addMarker(n.id, getResultCss(processInstance.value.result))} else if (n.$type === 'bpmn:ServiceTask') {//服务任务if (activity.startTime > 0 && activity.endTime === 0) {//进入执行,标识进行色canvas.addMarker(n.id, getResultCss(1))}if (activity.endTime > 0) {// 执行完成,节点标识完成色, 所有outgoing标识完成色。canvas.addMarker(n.id, getResultCss(2))const outgoing = getActivityOutgoing(activity)outgoing?.forEach((out) => {canvas.addMarker(out.id, getResultCss(2))})}}})
}

6.写在最后

本文简单的介绍了一个OA办公系统表单审批的全过程,行文比较粗糙,代码只展示了很少的一部分,如果有兴趣一起研究讨论的,欢迎留言批评指教。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/342032.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【2024】OAK智能深度相机校准教程

编辑&#xff1a;OAK中国 首发&#xff1a;oakchina.cn 喜欢的话&#xff0c;请多多&#x1f44d;⭐️✍ 内容可能会不定期更新&#xff0c;官网内容都是最新的&#xff0c;请查看首发地址链接。 ▌前言 Hello&#xff0c;大家好&#xff0c;这里是OAK中国&#xff0c;我是Ash…

Dubbo的SPI机制

Dubbo SPI的基本工作流程&#xff1a; 加载扩展点配置&#xff1a;Dubbo SPI首先会加载所有的扩展点配置&#xff0c;这些配置通常是在META-INF/dubbo目录下的properties文件中定义的。每个配置文件的名称就是扩展点接口的全限定名&#xff0c;文件内容是扩展点实现的键值对&am…

1、理解Transformer:革新自然语言处理的模型

目录 一、论文题目 二、背景与动机 三、卖点与创新 四、解决的问题 五、具体实现细节 0. Transformer 架构的主要组件 1. 注意力、自注意力&#xff08;Self-Attention&#xff09;到多头注意力&#xff08;Multi-Head Attention&#xff09; 注意力到底是做什么的&…

Vue、uniApp、微信小程序、Html5等实现数缓存

此文章带你实现前端缓存&#xff0c;利用时间戳封装一个类似于Redis可以添加过期时间的缓存工具 不仅可以实现对缓存数据设置过期时间&#xff0c;还可以自定义是否需要对缓存数据进行加密处理 工具介绍说明 对缓存数据进行非对称加密处理 对必要数据进行缓存&#xff0c;并…

微信小程序开发学习笔记《7》全局配置以及小程序窗口

微信小程序开发学习笔记《7》全局配置以及小程序窗口 博主正在学习微信小程序开发&#xff0c;希望记录自己学习过程同时与广大网友共同学习讨论。全局配置官方文档 一、全局配置文件及常用的配置项 小程序根目录下的app.json 文件是小程序的全局配置文件。 常用的配置项如…

数据库期末复习重点总结

数据库期末复习重点总结 本文为总结&#xff0c;如有不对的地方请指针 第2章 关系模型的介绍 名称符号选择σ投影∏笛卡儿积连接并∪集差-交∩赋值<-更名ρ 除操作 设R和S除运算的结果为T&#xff0c;则T包含所有在R中但不在S中的属性和值&#xff0c;且T的元组与S的元…

C语言理解

目录 计算机语言算法C项目创建C程序框架经典实例 计算机语言 程序是用特殊的编程语言&#xff08;这里是C语言&#xff09;写出来表达如何解决问题的不是用编程语言来和计算机交谈&#xff0c;而是描述要求它如何做事情的过程或方法程序是问题的载体&#xff0c;程序的执行就是…

leetcode面试经典150题——50 快乐数

题目&#xff1a;快乐数 描述&#xff1a; 编写一个算法来判断一个数 n 是不是快乐数。 「快乐数」 定义为&#xff1a; 对于一个正整数&#xff0c;每一次将该数替换为它每个位置上的数字的平方和。 然后重复这个过程直到这个数变为 1&#xff0c;也可能是 无限循环 但始终变…

读写锁(arm)

参考文章读写锁 - ARM汇编同步机制实例&#xff08;四&#xff09;_汇编 prefetchw-CSDN博客 读写锁允许多个执行流并发访问临界区。但是写访问是独占的。适用于读多写少的场景 另外好像有些还区分了读优先和写优先 读写锁定义 typedef struct {arch_rwlock_t raw_lock; #if…

Vue.js设计与实现阅读-2

Vue.js设计与实现阅读-2 1、前言2、框架设计的核心要素2、1 提升用户体验2、2 控制代码体积2、3 Tree-Shaking2、4 特性开关2、5 错误处理 1、前言 上一篇我们了解到了 命令式和声明式的区别&#xff0c;前者关注过程&#xff0c;后者关注结果了解了虚拟dom存在的意义&#x…

数据结构(三)堆和哈希表

目录 哈希表和堆什么是哈希表 &#xff1f;什么是堆 &#xff1f;什么是图 &#xff1f;案例一&#xff1a;使用python实现最小堆案例二 &#xff1a; 如何用Python通过哈希表的方式完成商品库存管理闯关题 &#xff08;包含案例三&#xff1a;python实现哈希表&#xff09; 本…

vue知识-05

聊天室案例(django接口) # chat.hetm<<script src"/static/axios.js"></script><script src"/static/vue.js"></script><body> <div id"app"><h1>聊天室</h1><button click"handleS…