前言
最近工作需要,根据xml转成svg图片,用到了bpmn.js这个组件,故此记录一下。
bpmn.js官网
bpmn-js: BPMN 2.0 rendering toolkit and web modeler | Toolkits | bpmn.io
GitHub地址:bpmn.io · GitHub
安装
npm install bpmn-js --save
引入
import BpmnViewer from 'bpmn-js/lib/Viewer'
代码
<template>
<div class="bpmn-containers" ref="content">
<div class="bpmn-canvas" ref="canvas"></div>
<el-button size="small" type="primary" @click="downloadSvg" style="float:right;">
导出流程图
</el-button>
<a hidden ref="downloadLink"></a>
</div>
</template>
<script>
import BpmnModeler from 'bpmn-js/lib/Modeler';
import BpmnViewer from 'bpmn-js/lib/Viewer'
import {xmlStr} from '@/pages/mock/xmlStr2';
export default {
components: {},
data() {
return {
bpmnModeler: null,
container: null,
canvas: null,
uploadBpmnFileList: [],
scale: 1,
nodeDetail:{},
nodeDetailLoading:{},
detailInfo:{},
showToolTip:false,
tooltipStyle: {
position: 'absoulte',
top:0,
left:0,
}
}
},
created() {
},
mounted() {
this.init();
},
methods: {
setViewerStyle(canvas){
let highlightNode = ["Activity_0ud0he0"]
let highlightLine = ["Flow_042g57v","Flow_10ltfzt"]
canvas.addMarker("Activity_0ud0he0", 'highlight');
if(highlightNode && highlightNode.length > 0){
highlightNode.forEach(item=>{
canvas.addMarker(item, 'highlight');
const ele = document.querySelector('.highlight').querySelector('.djs-visual rect');
if(ele){
ele.setAttribute('stroke-dasharray', '4,4');
}
});
}
if(highlightLine && highlightLine.length > 0){
highlightLine.forEach(item=>{
canvas.addMarker(item, 'highlight-line');
});
}
},
// 以下代码为:为节点注册鼠标悬浮事件
bindEvents(){
const eventBus = this.bpmnModeler.get('eventBus');
const overlays = this.bpmnModeler.get('overlays');
let processInstanceId = '9df8a92b58e811eeab3e000c29421fbd'
eventBus.on('element.hover', (e) => {
if(e.element.type === "bpmn:UserTask" ){
if(processInstanceId){
const elementId = e.element.id;
const cacheKey = this.getNodeDetailKey(processInstanceId, elementId);
if(this.nodeDetail[cacheKey]){
this.genNodeDetailBox(this.nodeDetail[cacheKey], e, overlays);
}else{
const currentNodeLoading = this.nodeDetailLoading[cacheKey];
if(!currentNodeLoading || currentNodeLoading === false){
this.getNodePersonal(processInstanceId, e, overlays);
}
}
}
}
});
eventBus.on('element.out', (e) => {
this.showToolTip = false
if(e.element.type === "bpmn:UserTask" ){
// popoverVisible.value = false;
}
overlays.clear();
});
},
getNodePersonal(processInstanceId, e, overlays){
const elementId = e.element.id;
const cacheKey = this.getNodeDetailKey(processInstanceId, elementId);
this.nodeDetailLoading[cacheKey] = true;
// getOneActivityVoByProcessInstanceIdAndActivityId({procInstId:processInstanceId, elementId: e.element.id}).then(res=>{
let data ={"code":"100","msg":"OK","data":{"id":"Activity_0ud0he0","x":330.0,"y":138.0,"width":100.0,"height":80.0,"documentation":null,"description":null,"name":"主管审核","approver":null,"type":null,"nodeType":"必审","status":"处理中","startDate":"2023-09-22 09:37:40","endDate":null,"duration":null,"approverNo":null,"proceInsId":"9df8a92b58e811eeab3e000c29421fbd","proceDefId":"svn_application_form:11:1724651d584611ee8e2a000c29421fbd","taskDefKey":"Activity_0ud0he0"},"success":true}
let res = data.data
this.nodeDetail[cacheKey] = res;
this.nodeDetailLoading[cacheKey] = false;
this.genNodeDetailBox(res, e, overlays);
// });
},
getNodeDetailKey(processInstanceId, elementId){
return processInstanceId + '_' + elementId;
},
genNodeDetailBox(detail, e, overlays) {
this.showToolTip = true
const tempDiv = document.createElement("div");
this.detailInfo.value = detail;
setTimeout(()=>{
const popoverEl = document.getElementById('flowMsgPopover');
this.tooltipComponent = {
template: `
<h1 class="popover-title">${this.detailInfo.name||"-"}</h1>
<p>
审批人员:<span class="approvePersonCss">${this.detailInfo.approver||"666"}</span>
</p>
<p>节点类型:${this.detailInfo.nodeType||"-"}</p>
<p>节点状态:${this.detailInfo.status||"-"}</p>
<p>开始时间:${this.detailInfo.startDate||"-"}</p>
<p>结束时间:${this.detailInfo.endDate||"-"}</p>
<p>审批耗时:${this.detailInfo.duration||"-"}</p>
`
};
tempDiv.innerHTML= this.tooltipComponent.template;
// tempDiv.innerHTML= popoverEl.innerHTML;
tempDiv.className = 'tipBox';
tempDiv.style.width = '270px';
tempDiv.style.background = 'rgba(255, 255, 255, .6)'
overlays.add(detail.id, 'note', {
position: {top: e.element.height, left: 0},
html: tempDiv
});
});
},
/**
* 初始化流程设计器对象
* @returns {Promise<void>}
*/
init() {
// 获取到属性ref为“content”的dom节点
this.container = this.$refs.content
// 获取到属性ref为“canvas”的dom节点
const canvas = this.$refs.canvas
// 创建BpmnModeler
this.bpmnModeler = new BpmnViewer({
container: canvas,
// 加入工具栏支持
// propertiesPanel: {
// parent: '#js-properties-panel'
// },
additionalModules: [
{
paletteProvider:["value",''],//禁用/清空左侧工具栏
labelEditingProvider:["value",''],//禁用节点编辑
contextPadProvider:["value",''],//禁用图形菜单
bendpoints:["value",{}],//禁用连线拖动
zoomScroll:["value",''],//禁用滚动
moveCanvas:['value',''],//禁用拖动整个流程图
move:['value','']//禁用单个图形拖动
}
],
moddleExtensions: {
// bpmn: bpmnModdleDescriptor
}
});
// 创建新流程
this.createNewDiagram(xmlStr);
},
/**
* 创建新流程
* @param bpmn BPMN流程XML报文
* @returns {Promise<void>}
*/
createNewDiagram(bpmn) {
// 将字符串转换成图显示出来;
this.bpmnModeler.importXML(bpmn, err => {
if (err) {
this.$message.error('打开模型出错,请确认该模型符合Bpmn2.0规范');
} else {
this.setViewerStyle(this.bpmnModeler.get('canvas'))
this.bindEvents();
console.log("成功导入模型");
}
});
},
handlerZoom(radio) {
const newScale = !radio ? 1.0 : this.scale + radio;
this.bpmnModeler.get("canvas").zoom(newScale);
this.scale = newScale;
},
downloadSvg() {
this.bpmnModeler.saveXML({format: true}, (err, xml) => {
if (!err) {
// 获取文件名
const name = `${this.getFilename(xml)}.svg`;
// 从建模器画布中提取svg图形标签
let context = "";
const djsGroupAll = this.$refs.canvas.querySelectorAll(".djs-group");
for (let item of djsGroupAll) {
context += item.innerHTML;
}
// 获取svg的基本数据,长宽高
const viewport = this.$refs.canvas
.querySelector(".viewport")
.getBBox();
// 将标签和数据拼接成一个完整正常的svg图形
const svg = `
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="${viewport.width}"
height="${viewport.height}"
viewBox="${viewport.x} ${viewport.y} ${viewport.width} ${viewport.height}"
version="1.1"
>
${context}
</svg>
`;
// 将文件名以及数据交给下载方法
this.download({name: name, data: svg});
}
});
},
download({name = "diagram.bpmn", data}) {
// 这里就获取到了之前设置的隐藏链接
const downloadLink = this.$refs.downloadLink;
// 把输就转换为URI,下载要用到的
const encodedData = encodeURIComponent(data);
if (data) {
// 将数据给到链接
downloadLink.href = "data:application/bpmn20-xml;charset=UTF-8," + encodedData;
// 设置文件名
downloadLink.download = name;
// 触发点击事件开始下载
downloadLink.click();
}
},
getFilename(xml) {
let start = xml.indexOf("process");
let filename = xml.substr(start, xml.indexOf(">"));
filename = filename.substr(filename.indexOf("id") + 4);
filename = filename.substr(0, filename.indexOf('"'));
return filename;
},
},
computed: {}
}
</script>
<style lang="scss">
.bpmn-containers {
background-color: #ffffff;
width: 100%;
height: 420px;
}
.bpmn-canvas {
width: 100%;
height: 100%;
}
.highlight:not(.djs-connection) .djs-visual > :nth-child(1) {
fill: var(--theme-custom-color_2) !important; /* color elements as green */
}
.highlight g.djs-visual >:nth-child(1) {
stroke: var(--theme-custom-color) !important;
}
.highlight-line g.djs-visual >:nth-child(1) {
stroke: rgba(0, 190, 0, 1) !important;
}
.highlight-line{
path{
marker-end: url('#greenMarker') !important;
}
}
.highlight{
.djs-visual{
animation: dynamicNode 18S linear infinite;
-webkit-animation: dynamicNode 18S linear infinite;
-webkit-animation-fill-mode: forwards;
}
}
@keyframes dynamicNode {
to {
stroke-dashoffset: 100%;
}
}
@-webkit-keyframes dynamicNode {
to {
stroke-dashoffset: 100%;
}
}
.bjs-powered-by{
display: none;
}
.tipBox {
font-size: var(--font-size-6);
.ant-popover-arrow{
display: none;
}
}
.tipBox {
width: 300px;
background: #fff;
border-radius: 4px;
border: 1px solid #ebeef5;
padding: var(--spacing-normal-2);
.ant-popover-arrow{
display: none;
}
.popover-title {
font-size: 16px;
line-height: 12px;
margin: 0;
}
p{
line-height: 28px;
margin:0;
padding:0;
}
}
.approvePersonCss{
color: var(--theme-custom-color);
background-color: var(--theme-custom-color_1);
border: 1px solid var(--theme-custom-color_2);
padding: 4px;
}
</style>
xml文件
export const xmlStr = `<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:flowable="http://flowable.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://flowable.org/modeler" exporter="Flowable Open Source Modeler" exporterVersion="1.1.2-SNAPSHOT">
<process id="svn_application_form" name="SVN申请流程表" isExecutable="true">
<startEvent id="startEvent1"></startEvent>
<userTask id="Activity_0eofm22" name="提交人" flowable:candidateGroups="测试用角色" flowable:skipExpression="">
<extensionElements>
<flowable:assigneeType><![CDATA[idm]]></flowable:assigneeType>
<flowable:idmCandidateGroups><![CDATA[[{"id":"undefined_60070","name":"测试用角色","sn":"测试用角色"}]]]></flowable:idmCandidateGroups>
<flowable:isEditdata><![CDATA[true]]></flowable:isEditdata>
<flowable:nodeType><![CDATA[0]]></flowable:nodeType>
</extensionElements>
</userTask>
<sequenceFlow id="Flow_042g57v" sourceRef="startEvent1" targetRef="Activity_0eofm22"></sequenceFlow>
<userTask id="Activity_0ud0he0" name="主管审核">
<extensionElements>
<flowable:taskListener event="create" delegateExpression=""></flowable:taskListener>
<flowable:taskListener event="create" delegateExpression=""></flowable:taskListener>
<flowable:assigneeType><![CDATA[idm]]></flowable:assigneeType>
<flowable:isEditdata><![CDATA[true]]></flowable:isEditdata>
<flowable:nodeType><![CDATA[1]]></flowable:nodeType>
</extensionElements>
</userTask>
<sequenceFlow id="Flow_10ltfzt" sourceRef="Activity_0eofm22" targetRef="Activity_0ud0he0"></sequenceFlow>
<userTask id="Activity_0xhe8nm" name="经理审核" flowable:candidateUsers="JH70003">
<extensionElements>
<flowable:taskListener event="create" delegateExpression=""></flowable:taskListener>
<flowable:taskListener event="complete" delegateExpression=""></flowable:taskListener>
<flowable:assigneeType><![CDATA[idm]]></flowable:assigneeType>
<flowable:idmCandidateUsers><![CDATA[[{"id":"1147160053359714304","name":"SVN审核经理","code":"JH70003","sex":0,"mobile":"13311112222","companyId":null,"companyName":null,"deptId":null,"deptName":null}]]]></flowable:idmCandidateUsers>
<flowable:nodeType><![CDATA[1]]></flowable:nodeType>
</extensionElements>
</userTask>
<sequenceFlow id="Flow_1k7t103" sourceRef="Activity_0ud0he0" targetRef="Activity_0xhe8nm"></sequenceFlow>
<userTask id="Activity_1nim4j5" name="知会对接人" flowable:candidateUsers="JH70004">
<extensionElements>
<flowable:taskListener event="create" delegateExpression=""></flowable:taskListener>
<flowable:assigneeType><![CDATA[idm]]></flowable:assigneeType>
<flowable:idmCandidateUsers><![CDATA[[{"id":"1147160212286087168","name":"SVN知会对接人","code":"JH70004","sex":0,"mobile":"13312312333","companyId":null,"companyName":null,"deptId":null,"deptName":null}]]]></flowable:idmCandidateUsers>
<flowable:nodeType><![CDATA[4]]></flowable:nodeType>
</extensionElements>
</userTask>
<sequenceFlow id="Flow_0y1ezwh" sourceRef="Activity_0xhe8nm" targetRef="Activity_1nim4j5"></sequenceFlow>
<endEvent id="Event_1l2ddd0"></endEvent>
<sequenceFlow id="Flow_11rjv3u" sourceRef="Activity_1nim4j5" targetRef="Event_1l2ddd0"></sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_svn_application_form">
<bpmndi:BPMNPlane bpmnElement="svn_application_form" id="BPMNPlane_svn_application_form">
<bpmndi:BPMNShape bpmnElement="startEvent1" id="BPMNShape_startEvent1">
<omgdc:Bounds height="30.0" width="30.0" x="100.0" y="163.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="Activity_0eofm22" id="BPMNShape_Activity_0eofm22">
<omgdc:Bounds height="80.0" width="100.0" x="180.0" y="138.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="Activity_0ud0he0" id="BPMNShape_Activity_0ud0he0">
<omgdc:Bounds height="80.0" width="100.0" x="330.0" y="138.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="Activity_0xhe8nm" id="BPMNShape_Activity_0xhe8nm">
<omgdc:Bounds height="80.0" width="100.0" x="480.0" y="138.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="Activity_1nim4j5" id="BPMNShape_Activity_1nim4j5">
<omgdc:Bounds height="80.0" width="100.0" x="630.0" y="138.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="Event_1l2ddd0" id="BPMNShape_Event_1l2ddd0">
<omgdc:Bounds height="36.0" width="36.0" x="782.0" y="160.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="Flow_10ltfzt" id="BPMNEdge_Flow_10ltfzt" flowable:sourceDockerX="50.0" flowable:sourceDockerY="40.0" flowable:targetDockerX="50.0" flowable:targetDockerY="40.0">
<omgdi:waypoint x="279.0" y="178.0"></omgdi:waypoint>
<omgdi:waypoint x="329.0" y="178.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="Flow_0y1ezwh" id="BPMNEdge_Flow_0y1ezwh" flowable:sourceDockerX="50.0" flowable:sourceDockerY="40.0" flowable:targetDockerX="50.0" flowable:targetDockerY="40.0">
<omgdi:waypoint x="579.0" y="178.0"></omgdi:waypoint>
<omgdi:waypoint x="629.0" y="178.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="Flow_11rjv3u" id="BPMNEdge_Flow_11rjv3u" flowable:sourceDockerX="50.0" flowable:sourceDockerY="40.0" flowable:targetDockerX="18.0" flowable:targetDockerY="18.0">
<omgdi:waypoint x="729.0" y="178.0"></omgdi:waypoint>
<omgdi:waypoint x="782.0" y="178.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="Flow_1k7t103" id="BPMNEdge_Flow_1k7t103" flowable:sourceDockerX="50.0" flowable:sourceDockerY="40.0" flowable:targetDockerX="50.0" flowable:targetDockerY="40.0">
<omgdi:waypoint x="429.0" y="178.0"></omgdi:waypoint>
<omgdi:waypoint x="479.0" y="178.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="Flow_042g57v" id="BPMNEdge_Flow_042g57v" flowable:sourceDockerX="15.0" flowable:sourceDockerY="15.0" flowable:targetDockerX="50.0" flowable:targetDockerY="40.0">
<omgdi:waypoint x="129.0" y="178.0"></omgdi:waypoint>
<omgdi:waypoint x="180.0" y="178.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>`
效果图
其中悬浮提示的效果,是通过为节点注册鼠标悬浮的事件实现的。