> 文章列表 > Flowable6.x导出/查看/跟踪流程图(续)

Flowable6.x导出/查看/跟踪流程图(续)

Flowable6.x导出/查看/跟踪流程图(续)

书接上回

项目源码仓库

无论是待办、已办,亦或是流转中、已结束的流程实例,通过使用JS绘制SVG格式的交互式流程图,与以上篇博文中三种方式相比,在效果上都具有明显优势。
运行效果如下图所示:
运行效果图

整合、改造Flowable中displaymodel页面

从flowable官方发布包获取前端源码

  • 下载官方数据包flowable-6.4.1.zip
  • 从压缩包中解压出flowable-6.4.1\\wars下面的flowable-modeler.war
  • 从flowable-modeler.war中解压出WEB-INF\\classes\\static\\display 文件夹下的11个文件,如下图所示:
    {% asset_img trac.gif 流程跟踪 %}
  • 在前端vue-element-admin的public下创建display文件夹,将11个文件放入
  • 在前端vue-element-admin的public创建displayModel.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"><head><meta charset="utf-8" /><meta name="renderer" content="webkit|ie-comp|ie-stand" /><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /><meta name="viewport"content="width=device-width,initial-scale=1,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" /><meta http-equiv="Cache-Control" content="no-siteapp" /><link type="text/css" rel="stylesheet" href="./display/jquery.qtip.min.css" /><link type="text/css" rel="stylesheet" href="./display/displaymodel.css" /><script type="text/javascript" src="./editor-app/editor-utils.js"></script><script type="text/javascript" src="./jquery_1.11.0/jquery.min.js"></script><script type="text/javascript" src="./jquery_1.11.0/jquery.cookie.js"></script><script type="text/javascript" src="./display/jquery.qtip.min.js"></script><script type="text/javascript" src="./display/raphael.js"></script><script type="text/javascript" src="./display/bpmn-draw.js"></script><script type="text/javascript" src="./display/bpmn-icons.js"></script><script type="text/javascript" src="./display/Polyline.js"></script><script type="text/javascript" src="./display/displaymodel.js"></script></head><body><div id="bpmnModel" data-model-id="1"></div></body>
</html>

通过vue组件iframe方式,将displaymodel页面嵌入

  • 将Dialog封闭为vue组件
import request from '@/utils/request'
//获取流程办理历史记录
export function fetchFlowLog(data) {return request({url: '/api/workflow/auth/activiti/task/log',method: 'post',data})
}

流程跟踪组件代码:

<template><div class="app-container" style="background-color: #FFFFFF;"><el-dialog :visible.sync="dialogVisible" :close-on-click-modal="false" width="80%" height="100%" title="流程跟踪"><!-- 此处通过iframe显示流程图 --><div><iframe ref="IFrame" id="map" scrolling="auto" v-bind:src="contents"frameborder="0" style="top:0px;left: 0px;right:0px;bottom:0px;width: 100%;height: 300px;"></iframe></div><!-- 此处通过el-table显示流程任务办理日志 --><div><el-table v-loading="loading" :data="mainTableData" border fit style="width: 100%" max-height="500"><el-table-column type="index" label="序号" width="70"></el-table-column><el-table-column prop="name" label="名称" show-overflow-tooltip sortable></el-table-column><el-table-column prop="assignee" label="经办人" show-overflow-tooltip sortable></el-table-column><el-table-column prop="notes" label="批注" show-overflow-tooltip sortable></el-table-column><el-table-column prop="startTime" label="开始时间" show-overflow-tooltip sortable:formatter="(row,column,cellValue) => dateTimeColFormatter(row,column,cellValue)"></el-table-column><el-table-column prop="endTime" label="完成时间" show-overflow-tooltip sortable:formatter="(row,column,cellValue) => dateTimeColFormatter(row,column,cellValue)"></el-table-column></el-table></div></el-dialog></div>
</template>
<script>// 引入获取办理日志数据的方法import {fetchFlowLog} from '@/api/workflow-task'export default {name: 'iFrame',data() {return {dialogVisible: false,contents: "",loading: true,mainTableData: []}},mounted() {},methods: {// 设置iframe的srcsetSrc(src){this.contents=src},showDialog() {this.dialogVisible = true},// 装入办理日志loadLog(processInstanceId){this.fetchProcessLog(processInstanceId)},async fetchProcessLog(processInstanceId){const guidContainer = {guid: processInstanceId}this.loading = trueconst response = await fetchFlowLog(guidContainer)this.loading = falseif (100 !== response.code) {this.$message({message: response.message,type: 'warning'})return}const {data} = responsethis.mainTableData = data},dateTimeColFormatter(row, column, cellValue) {return this.$commonUtils.dateTimeFormat(cellValue)},}}
</script>
  • 以待办页面为例引入vue组件(概略)
<template><div class="app-container background-white"><!-- 省略其他代码 --><!-- 此处引入显示流程图和办理日志的子组件 --><IFrame ref="displayComponent" /></div>
</template>
<script>// 此处引入显示流程图和办理日志的子组件// 其他代码省略import IFrame from '@/views/.../flowLog'export default {name: 'todoList',components: {IFrame},data() {return {},},methods: {// 调用子组件,显示流程图和办理日志handleDisplay(row) {this.$nextTick(() => {//设置iframe的srcthis.$refs.displayComponent.setSrc("/displayModel.html?processInstanceId=" + row.processInstanceId +"&nocaching=" +new Date().getTime())//装入办理日志this.$refs.displayComponent.loadLog(row.processInstanceId)//显示dialogthis.$refs.displayComponent.showDialog()})},}}
</script>
<style>
</style>

改造display

  • 改造displaymodel.js中_showTip(htmlNode, element)方法,优化鼠标悬浮在任务上时Tip显示内容
function _showTip(htmlNode, element) {var documentation = undefined;if (customActivityToolTips) {if (customActivityToolTips[element.name]) {documentation = customActivityToolTips[element.name];} else if (customActivityToolTips[element.id]) {documentation = customActivityToolTips[element.id];} else {documentation = ''; // Show nothing if custom tool tips are enabled}}if (documentation === undefined) {var documentation = undefined;if (element.type === 'UserTask') { //仅用户任务显示tipdocumentation = "<div style=\\"padding: 0px; \\">";if (!element.completed) {element.endTime = '';}if (!element.completed && !element.current) {element.startTime = '';}if (element.startTime) {documentation = documentation +"<div style=\\"font-size:9px;height:13px;line-height:10px;border-bottom:0px solid #D3D3D3;white-space:nowrap\\">" +"<span style=\\"font-weight:bold\\">开始时间:</span><span style=\\"margin-left:10px\\">" + element.startTime +"</span></div>"}if (element.endTime) {documentation = documentation +"<div style=\\"font-size:9px;height:13px;line-height:10px;border-bottom:0px solid #D3D3D3;white-space:nowrap\\">" +"<span style=\\"font-weight:bold\\">结束时间:</span><span style=\\"margin-left:10px\\">" + element.endTime +"</span></div>";}if (element.assignee) {documentation = documentation +"<div style=\\"font-size:9px;height:13px;line-height:10px;border-bottom:0px solid #D3D3D3;white-space:nowrap\\">" +"<span style=\\"font-weight:bold\\">办理人员:</span><span style=\\"margin-left:10px\\">" + element.assignee +"</span></div>";}if (element.comments) {documentation = documentation +"<div style=\\"font-size:9px;height:13px;line-height:10px;border-bottom:0px solid #D3D3D3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;\\">" +"<span style=\\"font-weight:bold\\">办理批注:</span><span style=\\"margin-left:10px\\">" + element.comments +"</span></div>";}documentation = documentation + "</div>";}
  • 改造displaymodel.js中获取参数processInstanceId的代码
var processInstanceId = EDITOR.UTIL.getParameterByName('processInstanceId');

此处使用了flowable源码中EDITOR工具中getParameterByName方法,代码片断如下:

var EDITOR = EDITOR || {};
EDITOR.UTIL = {getParameterByName: function (name) {name = name.replace(/[\\[]/, "\\\\[").replace(/[\\]]/, "\\\\]");var regex = new RegExp("[\\\\?&]" + name + "=([^&#]*)"),results = regex.exec(location.search);return results == null ? "" : decodeURIComponent(results[1].replace(/\\+/g, " "));},
};
  • 改造displaymodel.js,添加获取令牌功能,通过令牌,可实现授权访问流程图的要求。
var token = 'Bearer' + $.cookie("Admin-Token");

读取cookie中的令牌。

  • 改造displaymodel.js的访问后台流程图数据服务的功能
var modelUrl = 'http://网关IP:网关端口/api/workflow/auth/activiti/task/process/instances';
var processInstanceId = EDITOR.UTIL.getParameterByName('processInstanceId');
var token = 'Bearer' + $.cookie("Admin-Token");
var request = jQuery.ajax({type: 'get',beforeSend: function(xhr) {xhr.setRequestHeader('X-Token', token);},url: modelUrl + '?processInstanceId=' + processInstanceId + '&nocaching=' + new Date().getTime()
});

后台代码(为前台准备数据)

  • 流程图数据获取源码
@Service
public class RuntimeDisplayJsonClientResourceImpl implements RuntimeDisplayJsonClientResource {@Autowiredprotected RepositoryService repositoryService;@Autowiredprotected RuntimeService runtimeService;@Autowiredprotected HistoryService historyService;@Autowiredprotected ManagementService managementService;protected ObjectMapper objectMapper = new ObjectMapper();protected List<String> eventElementTypes = new ArrayList<String>();protected Map<String, InfoMapper> propertyMappers = new HashMap<String, InfoMapper>();@Overridepublic JsonNode getModelJSON(String processInstanceId) throws Exception {String processDefinitionId="";/ 先取流转中的流程实例 /ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();if (processInstance == null) {/ 如果流程已结束,就取历史流程实例 /HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();if(historicProcessInstance == null) {throw new Exception("No process instance found with id " + processInstanceId);}else{processDefinitionId = historicProcessInstance.getProcessDefinitionId();}}else{processDefinitionId = processInstance.getProcessDefinitionId();}BpmnModel pojoModel = repositoryService.getBpmnModel(processDefinitionId);if (pojoModel == null || pojoModel.getLocationMap().isEmpty()) {throw new Exception("流程定义未找到:id " + processDefinitionId);}List<HistoricActivityInstance> activityInstances = historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).orderByHistoricActivityInstanceStartTime().asc().list();Map<String, TaskBO> taskInfo = this.getTaskInfo(activityInstances);Set<String> completedActivityInstances = new HashSet<String>();Set<String> currentElements = new HashSet<String>();if (CollectionUtils.isNotEmpty(activityInstances)) {for (HistoricActivityInstance activityInstance : activityInstances) {if (activityInstance.getEndTime() != null) {completedActivityInstances.add(activityInstance.getActivityId());} else {currentElements.add(activityInstance.getActivityId());}}}List<Job> jobs = managementService.createJobQuery().processInstanceId(processInstanceId).list();if (CollectionUtils.isNotEmpty(jobs)) {List<Execution> executions = runtimeService.createExecutionQuery().processInstanceId(processInstanceId).list();Map<String, Execution> executionMap = new HashMap<String, Execution>();for (Execution execution : executions) {executionMap.put(execution.getId(), execution);}for (Job job : jobs) {if (executionMap.containsKey(job.getExecutionId())) {currentElements.add(executionMap.get(job.getExecutionId()).getActivityId());}}}// 收集已完成的 flowsList<String> completedFlows = gatherCompletedFlows(completedActivityInstances, currentElements, pojoModel);Set<String> completedElements = new HashSet<String>(completedActivityInstances);completedElements.addAll(completedFlows);ObjectNode displayNode = processProcessElements(pojoModel, completedElements, currentElements,taskInfo);if (completedActivityInstances != null) {ArrayNode completedActivities = displayNode.putArray("completedActivities");for (String completed : completedActivityInstances) {completedActivities.add(completed);}}if (currentElements != null) {ArrayNode currentActivities = displayNode.putArray("currentActivities");for (String current : currentElements) {currentActivities.add(current);}}if (completedFlows != null) {ArrayNode completedSequenceFlows = displayNode.putArray("completedSequenceFlows");for (String current : completedFlows) {completedSequenceFlows.add(current);}}return displayNode;}
}
  • 办理日志数据获取方法源码
    @Overridepublic ResultDTO getLog(String processInstanceId) throws Exception {List<HistoricActivityInstance> activityInstances = historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).orderByHistoricActivityInstanceStartTime().asc().orderByHistoricActivityInstanceEndTime().asc().list();List<TaskBO> taskBOList = new ArrayList<>();for(HistoricActivityInstance historicActivityInstance:activityInstances){if(!"userTask".equals(historicActivityInstance.getActivityType()) && !"startEvent".equals(historicActivityInstance.getActivityType()) && !"endEvent".equals(historicActivityInstance.getActivityType())){continue;}TaskBO taskBO = new TaskBO();taskBO.setStartTime(historicActivityInstance.getStartTime());taskBO.setEndTime(historicActivityInstance.getEndTime());String assignee=historicActivityInstance.getAssignee();taskBO.setAssignee(WorkflowTool.getHumanAssignee(assignee));taskBO.setName(historicActivityInstance.getActivityName());if(org.apache.commons.lang3.StringUtils.isNotBlank(historicActivityInstance.getTaskId())){List<Comment> commList = taskService.getTaskComments(historicActivityInstance.getTaskId());String msg = StringTool.join(";",commList,"fullMessage");taskBO.setNotes(msg);}if("startEvent".equals(historicActivityInstance.getActivityType())){String initiator = flowableUtis.getInitiatorByProcessInstanceId(historicActivityInstance.getProcessInstanceId());String initatorHuman = WorkflowTool.getHumanAssignee(initiator);taskBO.setAssignee(initatorHuman);taskBO.setNotes("提交");}if("endEvent".equals(historicActivityInstance.getActivityType())){taskBO.setAssignee("系统");taskBO.setNotes("完成");}taskBOList.add(taskBO);}return ResultDTO.success(taskBOList);}
  • 相关bean对象定义
@Data
public class TaskBO {private String assignee;private Date startTime;private Date endTime;private String notes;private String name;
}

总结:通过对flowable源码中bpmnModel绘制功能的整合,可以较好的实现交互式的流程图跟踪展现功能。相较静态图方式展现的流程图,这种实现方式用户交互体验更好,获取信息更加方便,具有明显优势。
项目源码仓库
下一篇将介绍对flowable中模型制作editor-app功能的深度整合。