发布时间:2023-11-12 10:30
为了让大家看到最后的效果是否是自己想要的,所以我们先来看最后的效果图:
我们都知道activiti在5.22的时候就有了流程图跟踪组件Diagram Viewer
,如图(图片来源《Activiti实战》):
但是,到6.0就找不到该组件了,可能因为6.0的api改动很大,有些类和包完全舍弃了,该组件就不适用了。所以在6.0的时候,我们要想展示高亮流程图,往往都是通过后端来生成图片,返回给前端展示。这种方式也是我们项目之前所用的,生成图片代码繁琐,在生产环境还有字体问题,我们就不多介绍。
无意中了解到bpmn.js
可以设计和展示流程图(本篇只讲解展示流程图),所以就去搜索这方面的资料。找到了这篇博客:
在他的基础上,进行了改造和优化,就有了开头的效果图。
"bpmn-js": "^8.7.1",
"bpmn-js-properties-panel": "^0.44.0",
"bpmn-js-token-simulation": "^0.21.1",
"camunda-bpmn-moddle": "^5.1.2",
"xml-js": "^1.6.11"
引入这些依赖就可以了。然后就是页面,基本上与 bpmn整合流程图高亮显示流程进度图 一样,但是加入了自己的逻辑,修复了一些显示问题:
<template>
<div>
<div class="bpmn-viewer-container">
<div
style="width:100%;height:20px;position: absolute; left: 20px; top: 10px; color: #000000D9;font-size: 16px;font-weight: 500">
{{title}}
</div>
<div style="position: absolute; left: 10px; top: 40px;z-index: 999">
<el-button-group key="scale-control">
<el-tooltip effect="light" content="缩小视图">
<el-button :size="headerButtonSize" :disabled="defaultZoom < 0.2" icon="el-icon-zoom-out"
@click="processZoomOut()"/>
</el-tooltip>
<el-button :size="headerButtonSize">{{ Math.floor(this.defaultZoom * 10 * 10) + '%' }}</el-button>
<el-tooltip effect="light" content="放大视图">
<el-button :size="headerButtonSize" :disabled="defaultZoom > 4" icon="el-icon-zoom-in"
@click="processZoomIn()"/>
</el-tooltip>
<el-tooltip effect="light" content="重置视图并居中">
<el-button :size="headerButtonSize" icon="el-icon-c-scale-to-original" @click="processReZoom()"/>
</el-tooltip>
</el-button-group>
</div>
<div id="bpmnCanvas" style="width:100%;height:500px;margin: 0 auto;"></div>
<div v-for="item in detailInfo" :key="item.activityId" style="width: 90%;margin: 0 auto;border-bottom: 1px dashed #333;">
<el-row>
<el-col :span="12">
<p>节点名称:{{item.activityName}}</p>
<p>审批人:{{item.assignee}}</p>
<p>审批状态:{{item.approvalStatus}}</p>
</el-col>
<el-col :span="12">
<p>审批结果:{{item.result}}</p>
<p>审批意见:{{item.comment}}</p>
<p>审批时间:{{item.endTime}}</p>
</el-col>
</el-row>
</div>
</div>
</div>
</template>
<script>
import BpmnViewer from 'bpmn-js'
import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas'
import { checkSpeed, getOneActivity } from '@/api/approval/build'
let bpmnViewer = null
export default {
props: {
headerButtonSize: {
type: String,
default: 'small',
validator: value => ['default', 'medium', 'small', 'mini'].indexOf(value) !== -1
},
reviewObj: {
type: Object
}
},
name: 'reviewRuningFlow',
components: {
},
data() {
return {
detailInfo: [],
executedLightNode: [],
highlightLine: [],
activeLightNode:[],
defaultZoom: 1,
nodeDetail: {},
scale: 1,
title: '流程预览',
showViewDialog: false,
instanceId: undefined
}
},
mounted() {
// this.initPage()
},
methods: {
initPage(instanceId, procDefId) {
bpmnViewer && bpmnViewer.destroy()
bpmnViewer = new BpmnViewer({
container: '#bpmnCanvas',
width: '100%',
additionalModules: [
MoveCanvasModule // 移动整个画布
]
})
this.instanceId = instanceId
let len = document.getElementsByTagName('svg').length
document.getElementsByTagName('svg')[len-2].setAttribute('display', 'none')
if(instanceId || procDefId) {
checkSpeed({'instanceId':instanceId, 'procDefId':procDefId}).then(res => {
if (res.code === 200) {
this.title = res.data.modelName
this.highlightLine = res.data.highlightedFlowIds
this.executedLightNode = res.data.executedActivityIds
this.activeLightNode = res.data.activeActivityIds
if (bpmnViewer) {
this.importXml(res.data.modelXml)
}
} else {
this.$message({
message: res.data.msg,
type: 'error'
})
}
})}
//以下注释代码是只展示流程图不需要高亮展示
/*if(bpmnViewer){
this.importXml(this.reviewObj.modelXml);
} else {
console.error('bpmnViewer is null or undefined!');
}*/
},
getHtmlAttr(source, element, attr) {
let result = []
let reg = '<' + element + '[^<>]*?\\\\s' + attr + '=[\\'"]?(.*?)[\\'"]?\\\\s.*?>'
let matched = source.match(new RegExp(reg, 'gi'))
matched && matched.forEach(item => {
item && result.push(item)
})
return result
},
importXml(modelXml) {
// 处理排他网关, 注:流程图预览时,排他网关需要在对应的<bpmndi:BPMNShape>节点上添加属性isMarkerVisible="true"
let gatewayIds = this.getHtmlAttr(modelXml, 'exclusiveGateway', 'id')
let modelXmlTemp = modelXml
if (gatewayIds && gatewayIds.length > 0) {
gatewayIds.forEach(item => {
const result = new RegExp('id="(.+?)"').exec(item)
if (result && result[1]) {
modelXmlTemp = modelXmlTemp.replace('bpmnElement="' + result[1] + '"', 'bpmnElement="' + result[1] + '" isMarkerVisible="true"')
}
})
}
bpmnViewer.importXML(modelXmlTemp, (err) => {
if (err) {
console.error(err, 1111)
} else {
this.importXmlSuccess()
}
})
},
importXmlSuccess() {
// 使流程图自适应屏幕
let canvas = bpmnViewer.get('canvas')
canvas.zoom('fit-viewport', 'auto')
//设置高亮线和高亮节点,需要配合style中的css样式一起使用,否则没有颜色
this.setViewerStyle(canvas)
//给任务节点加聚焦和离焦事件
this.bindEvents()
},
setViewerStyle(canvas) {
//已完成节点高亮
let executedLightNode = this.executedLightNode
if (executedLightNode && executedLightNode.length > 0) {
executedLightNode.forEach(item => {
canvas.addMarker(item, 'highlight-executed')
})
}
//顺序线高亮
let highlightLines = this.highlightLine
if (highlightLines && highlightLines.length > 0) {
highlightLines.forEach(item => {
canvas.addMarker(item, 'highlight-line')
})
}
//正在执行节点高亮
let activeLightNode = this.activeLightNode
if (activeLightNode && activeLightNode.length > 0) {
activeLightNode.forEach((item,index) => {
canvas.addMarker(item, 'highlight')
})
document.querySelectorAll('.highlight').forEach((item,index)=>{
item.querySelector('.djs-visual rect').setAttribute('stroke-dasharray', '4,4')
})
}
},
// 以下代码为:为节点注册鼠标悬浮事件
bindEvents() {
let eventBus = bpmnViewer.get('eventBus')
eventBus.on('element.hover', (e) => {
if (e.element.type === 'bpmn:UserTask') {
if (this.nodeDetail[e.element.id]) {
this.detailInfo = this.nodeDetail[e.element.id]
} else {
getOneActivity({
instanceId: this.instanceId,
activityId: e.element.id
}).then(res => {
this.nodeDetail[e.element.id] = res.data;
this.detailInfo = res.data
})
}
}
})
eventBus.on('element.out', (e) => {
if (e.element.type === 'bpmn:UserTask') {
this.detailInfo = []
}
})
},
//悬浮框设置
/*genNodeDetailBox(e, overlays) {
let tempDiv = document.createElement('div')
//this.detailInfo = detail;
let popoverEl = document.querySelector('.flowMsgPopover')
//let popoverEl = this.$refs.flowMsgPopover;
console.log(this.detailInfo)
tempDiv.innerHTML = popoverEl.innerHTML
tempDiv.className = 'tipBox'
tempDiv.style.width = '260px'
tempDiv.style.background = 'rgba(255, 255, 255)'
overlays.add(e.element.id, {
position: { top: e.element.height, left: 0 },
html: tempDiv
})
},*/
processZoomIn(zoomStep = 0.1) {
let newZoom = Math.floor(this.defaultZoom * 100 + zoomStep * 100) / 100
if (newZoom > 4) {
throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4')
}
this.defaultZoom = newZoom
bpmnViewer.get('canvas').zoom(this.defaultZoom)
},
processZoomOut(zoomStep = 0.1) {
let newZoom = Math.floor(this.defaultZoom * 100 - zoomStep * 100) / 100
if (newZoom < 0.2) {
throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2')
}
this.defaultZoom = newZoom
bpmnViewer.get('canvas').zoom(this.defaultZoom)
},
processReZoom() {
this.defaultZoom = 1
bpmnViewer.get('canvas').zoom('fit-viewport', 'auto')
}
}
}
</script>
<style lang="scss">
@import '../../../../node_modules/bpmn-js/dist/assets/diagram-js.css';
@import '../../../../node_modules/bpmn-js/dist/assets/bpmn-font/css/bpmn.css';
@import '../../../../node_modules/bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css';
@import '../../../../node_modules/bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css';
@import '../../../../node_modules/bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css';
/*.bjs-powered-by {
display: none;
}*/
.flowMsgPopover {
display: none;
}
.highlight:not(.djs-connection) .djs-visual > :nth-child(1) {
fill: rgb(251, 233, 209) !important; /* color elements as green */
}
.highlight g.djs-visual > :nth-child(1) {
stroke: rgb(214, 126, 125) !important;
}
.highlight-executed g.djs-visual > :nth-child(1) {
stroke: rgb(0, 190, 0, 1) !important;
fill: rgb(180, 241, 180) !important;
}
.highlight-line g.djs-visual > :nth-child(1) {
stroke: rgb(0, 190, 0) !important;
}
@-webkit-keyframes dynamicNode {
to {
stroke-dashoffset: 100%;
}
}
.highlight {
.djs-visual {
-webkit-animation: dynamicNode 18S linear infinite;
-webkit-animation-fill-mode: forwards;
}
}
.tipBox {
width: 300px;
background: #fff;
border-radius: 4px;
border: 1px solid #ebeef5;
padding: 12px;
/*.ant-popover-arrow{
display: none;
}*/
p {
line-height: 28px;
margin: 0;
padding: 0;
}
}
</style>
这样前端的工作就完成了。
我们从图片可以看出,前端渲染图片需要4种数据:
基本上我们原先后端的代码也能用,我们就不用通过代码去画图了,只需要将查找出的4种数据返回给前端。这部分的代码就不贴了,大家东拼西凑都能找到,我这边用的也上一个负责工作流的伙伴东拼西凑留下来的。
返回实体:
@Data
public class ProcessHighlightEntity {
/**
* 当前正执行节点id
*/
private Set<String> activeActivityIds;
/**
* 已执行节点id
*/
private Set<String> executedActivityIds;
/**
* 高亮线id(流程已走过的线)
*/
private Set<String> highlightedFlowIds;
/**
* 流程xml文件 字符串
*/
private String modelXml;
/**
* 流程名称
*/
private String modelName;
}
获取流程图高亮所需数据:
public ProcessHighlightEntity getActivitiProcessHighlight(String instanceId, String procDefId) {
ProcessDefinition processDefinition = getProcessDefinition(procDefId, instanceId);
procDefId = processDefinition.getId();
BpmnModel bpmnModel = getBpmnModel(procDefId);
List<HistoricActivityInstance> histActInstances = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(instanceId).orderByHistoricActivityInstanceId().asc().list();
ProcessHighlightEntity highlightEntity = getHighLightedData(bpmnModel.getMainProcess(), histActInstances);
highlightEntity.setModelName(processDefinition.getName());
// Map缓存,提高获取流程文件速度
if (ActivitiConstants.BPMN_XML_MAP.containsKey(procDefId)) {
highlightEntity.setModelXml(ActivitiConstants.BPMN_XML_MAP.get(procDefId));
} else {
InputStream bpmnStream = repositoryService.getResourceAsStream(processDefinition.getDeploymentId(), processDefinition.getResourceName());
try (Reader reader = new InputStreamReader(bpmnStream, StandardCharsets.UTF_8)) {
String xmlString = IoUtil.read(reader);
highlightEntity.setModelXml(xmlString);
ActivitiConstants.BPMN_XML_MAP.put(procDefId, xmlString);
} catch (IOException e) {
log.error("[获取流程数据] 失败,{}", e.getMessage());
throw new CustomException("获取流程数据失败,请稍后重试");
}
}
return highlightEntity;
}
获取流程定义数据:
public ProcessDefinition getProcessDefinition(String procDefId, String instanceId) {
if (StrUtil.isBlank(procDefId)) {
if (StrUtil.isBlank(instanceId)) {
throw new CustomException("流程实例id,流程定义id 两者不能都为空");
}
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
.processInstanceId(instanceId)
.singleResult();
if (processInstance == null) {
HistoricProcessInstance histProcInst = historyService.createHistoricProcessInstanceQuery()
.processInstanceId(instanceId)
.singleResult();
if (histProcInst == null) {
throw new CustomException("查询失败,请检查传入的 instanceId 是否正确");
}
procDefId = histProcInst.getProcessDefinitionId();
} else {
procDefId = processInstance.getProcessDefinitionId();
}
}
try {
return repositoryService.getProcessDefinition(procDefId);
} catch (ActivitiObjectNotFoundException e) {
throw new CustomException("该流程属于之前流程,已删除");
}
}
获取Bpmn模型数据:
public BpmnModel getBpmnModel(String procDefId) {
try {
return repositoryService.getBpmnModel(procDefId);
} catch (ActivitiObjectNotFoundException e) {
throw new CustomException("流程定义数据不存在");
}
}
获取需要高亮的流程数据:
private ProcessHighlightEntity getHighLightedData(Process process,
List<HistoricActivityInstance> historicActInstances) {
ProcessHighlightEntity entity = new ProcessHighlightEntity();
// 已执行的节点id
Set<String> executedActivityIds = new HashSet<>();
// 正在执行的节点id
Set<String> activeActivityIds = new HashSet<>();
// 高亮流程已发生流转的线id集合
Set<String> highLightedFlowIds = new HashSet<>();
// 全部活动节点
List<FlowNode> historicActivityNodes = new ArrayList<>();
// 已完成的历史活动节点
List<HistoricActivityInstance> finishedActivityInstances = new ArrayList<>();
for (HistoricActivityInstance historicActivityInstance : historicActInstances) {
FlowNode flowNode = (FlowNode) process.getFlowElement(historicActivityInstance.getActivityId(), true);
historicActivityNodes.add(flowNode);
if (historicActivityInstance.getEndTime() != null) {
finishedActivityInstances.add(historicActivityInstance);
executedActivityIds.add(historicActivityInstance.getActivityId());
} else {
activeActivityIds.add(historicActivityInstance.getActivityId());
}
}
FlowNode currentFlowNode = null;
FlowNode targetFlowNode = null;
// 遍历已完成的活动实例,从每个实例的outgoingFlows中找到已执行的
for (HistoricActivityInstance currentActivityInstance : finishedActivityInstances) {
// 获得当前活动对应的节点信息及outgoingFlows信息
currentFlowNode = (FlowNode) process.getFlowElement(currentActivityInstance.getActivityId(), true);
List<SequenceFlow> sequenceFlows = currentFlowNode.getOutgoingFlows();
/**
* 遍历outgoingFlows并找到已已流转的 满足如下条件认为已已流转:
* 1.当前节点是并行网关或兼容网关,则通过outgoingFlows能够在历史活动中找到的全部节点均为已流转
* 2.当前节点是以上两种类型之外的,通过outgoingFlows查找到的时间最早的流转节点视为有效流转
*/
if ("parallelGateway".equals(currentActivityInstance.getActivityType()) || "inclusiveGateway".equals(currentActivityInstance.getActivityType())) {
// 遍历历史活动节点,找到匹配流程目标节点的
for (SequenceFlow sequenceFlow : sequenceFlows) {
targetFlowNode = (FlowNode) process.getFlowElement(sequenceFlow.getTargetRef(), true);
if (historicActivityNodes.contains(targetFlowNode)) {
highLightedFlowIds.add(sequenceFlow.getId());
}
}
} else {
List<Map<String, Object>> tempMapList = new ArrayList<>();
for (SequenceFlow sequenceFlow : sequenceFlows) {
for (HistoricActivityInstance historicActivityInstance : historicActInstances) {
if (historicActivityInstance.getActivityId().equals(sequenceFlow.getTargetRef())) {
Map<String, Object> map = new HashMap<>();
map.put("highLightedFlowId", sequenceFlow.getId());
map.put("highLightedFlowStartTime", historicActivityInstance.getStartTime().getTime());
tempMapList.add(map);
}
}
}
if (!CollectionUtils.isEmpty(tempMapList)) {
// 遍历匹配的集合,取得开始时间最早的一个
long earliestStamp = 0L;
String highLightedFlowId = null;
for (Map<String, Object> map : tempMapList) {
long highLightedFlowStartTime = Long.parseLong(map.get("highLightedFlowStartTime").toString());
if (earliestStamp == 0 || earliestStamp >= highLightedFlowStartTime) {
highLightedFlowId = map.get("highLightedFlowId").toString();
earliestStamp = highLightedFlowStartTime;
}
}
highLightedFlowIds.add(highLightedFlowId);
}
}
}
entity.setActiveActivityIds(activeActivityIds);
entity.setExecutedActivityIds(executedActivityIds);
entity.setHighlightedFlowIds(highLightedFlowIds);
return entity;
}