<template> <div v-loading="isView" class="flow-containers" :class="{ 'view-mode': isView }"> <el-container style="height: 100%"> <el-header style="border-bottom: 1px solid rgb(218 218 218);height: auto;"> <div style="display: flex; padding: 10px 0px; justify-content: space-between;"> <div> <el-upload action="" :before-upload="openBpmn" style="margin-right: 10px; display:inline-block;"> <el-tooltip effect="dark" content="加载xml" placement="bottom"> <el-button size="mini" icon="el-icon-folder-opened" /> </el-tooltip> </el-upload> <el-tooltip effect="dark" content="新建" placement="bottom"> <el-button size="mini" icon="el-icon-circle-plus" @click="newDiagram" /> </el-tooltip> <el-tooltip effect="dark" content="自适应屏幕" placement="bottom"> <el-button size="mini" icon="el-icon-rank" @click="fitViewport" /> </el-tooltip> <el-tooltip effect="dark" content="放大" placement="bottom"> <el-button size="mini" icon="el-icon-zoom-in" @click="zoomViewport(true)" /> </el-tooltip> <el-tooltip effect="dark" content="缩小" placement="bottom"> <el-button size="mini" icon="el-icon-zoom-out" @click="zoomViewport(false)" /> </el-tooltip> <el-tooltip effect="dark" content="后退" placement="bottom"> <el-button size="mini" icon="el-icon-back" @click="modeler.get('commandStack').undo()" /> </el-tooltip> <el-tooltip effect="dark" content="前进" placement="bottom"> <el-button size="mini" icon="el-icon-right" @click="modeler.get('commandStack').redo()" /> </el-tooltip> </div> <div> <el-button size="mini" icon="el-icon-view" @click="showXML">查看xml</el-button> <el-button size="mini" icon="el-icon-download" @click="saveXML(true)">下载xml</el-button> <el-button size="mini" icon="el-icon-picture" @click="saveImg('svg', true)">下载svg</el-button> <el-button size="mini" type="primary" @click="save">保存模型</el-button> </div> </div> </el-header> <el-container style="align-items: stretch"> <el-main style="padding: 0;"> <div ref="canvas" class="canvas" /> </el-main> <el-aside style="width: 400px; min-height: 650px; background-color: #f0f2f5"> <panel v-if="modeler" :modeler="modeler" :users="users" :groups="groups" :categorys="categorys" @dataType="dataType" /> </el-aside> </el-container> </el-container> </div> </template> <script> // 汉化 import customTranslate from './common/customTranslate' import Modeler from 'bpmn-js/lib/Modeler' import panel from './PropertyPanel' import BpmData from './BpmData' import getInitStr from './flowable/init' // 引入flowable的节点文件 import flowableModdle from './flowable/flowable.json' export default { name: 'WorkflowBpmnModeler', components: { panel }, props: { xml: { type: String, default: '' }, users: { type: Array, default: () => [] }, groups: { type: Array, default: () => [] }, categorys: { type: Array, default: () => [] }, isView: { type: Boolean, default: false }, taskList: { type: Array, default: () => [] } }, data() { return { modeler: null, // taskList: [], zoom: 1 } }, watch: { xml: function(val) { if (val) { this.createNewDiagram(val) } } }, mounted() { // 生成实例 this.modeler = new Modeler({ container: this.$refs.canvas, additionalModules: [ { translate: ['value', customTranslate] } ], moddleExtensions: { flowable: flowableModdle } }) // 新增流程定义 if (!this.xml) { this.newDiagram() } else { this.createNewDiagram(this.xml) } }, methods: { newDiagram() { this.createNewDiagram(getInitStr()) }, // 让图能自适应屏幕 fitViewport() { this.zoom = this.modeler.get('canvas').zoom('fit-viewport') const bbox = document.querySelector('.flow-containers .viewport').getBBox() const currentViewbox = this.modeler.get('canvas').viewbox() const elementMid = { x: bbox.x + bbox.width / 2 - 65, y: bbox.y + bbox.height / 2 } this.modeler.get('canvas').viewbox({ x: elementMid.x - currentViewbox.width / 2, y: elementMid.y - currentViewbox.height / 2, width: currentViewbox.width, height: currentViewbox.height }) this.zoom = bbox.width / currentViewbox.width * 1.8 }, // 放大缩小 zoomViewport(zoomIn = true) { this.zoom = this.modeler.get('canvas').zoom() this.zoom += (zoomIn ? 0.1 : -0.1) this.modeler.get('canvas').zoom(this.zoom) }, async createNewDiagram(data) { // 将字符串转换成图显示出来 // data = data.replace(/<!\[CDATA\[(.+?)]]>/g, '<![CDATA[$1]]>') data = data.replace(/<!\[CDATA\[(.+?)]]>/g, function(match, str) { return str.replace(/</g, '<') }) try { await this.modeler.importXML(data) this.adjustPalette() this.fitViewport() if (this.taskList !==undefined && this.taskList.length > 0 ) { debugger this.fillColor() } } catch (err) { console.error(err.message, err.warnings) } }, // 调整左侧工具栏排版 adjustPalette() { try { // 获取 bpmn 设计器实例 const canvas = this.$refs.canvas const djsPalette = canvas.children[0].children[1].children[4] const djsPalStyle = { width: '130px', padding: '5px', background: 'white', left: '20px', borderRadius: 0 } for (var key in djsPalStyle) { djsPalette.style[key] = djsPalStyle[key] } const palette = djsPalette.children[0] const allGroups = palette.children allGroups[0].style['display'] = 'none' // 修改控件样式 for (var gKey in allGroups) { const group = allGroups[gKey] for (var cKey in group.children) { const control = group.children[cKey] const controlStyle = { display: 'flex', justifyContent: 'flex-start', alignItems: 'center', width: '100%', padding: '5px' } if ( control.className && control.dataset && control.className.indexOf('entry') !== -1 ) { const controlProps = new BpmData().getControl( control.dataset.action ) control.innerHTML = `<div style='font-size: 14px;font-weight:500;margin-left:15px;'>${ controlProps['title'] }</div>` for (var csKey in controlStyle) { control.style[csKey] = controlStyle[csKey] } } } } } catch (e) { console.log(e) } }, fillColor() { const canvas = this.modeler.get('canvas') this.modeler._definitions.rootElements[0].flowElements.forEach(n => { const completeTask = this.taskList.find(m => m.key === n.id) const todoTask = this.taskList.find(m => !m.completed) const endTask = this.taskList[this.taskList.length - 1] if (n.$type === 'bpmn:UserTask') { if (completeTask) { canvas.addMarker(n.id, completeTask.completed ? 'highlight' : 'highlight-todo') n.outgoing?.forEach(nn => { const targetTask = this.taskList.find(m => m.key === nn.targetRef.id) if (targetTask) { if (todoTask && completeTask.key === todoTask.key && !todoTask.completed){ canvas.addMarker(nn.id, todoTask.completed ? 'highlight' : 'highlight-todo') canvas.addMarker(nn.targetRef.id, todoTask.completed ? 'highlight' : 'highlight-todo') }else { canvas.addMarker(nn.id, targetTask.completed ? 'highlight' : 'highlight-todo') canvas.addMarker(nn.targetRef.id, targetTask.completed ? 'highlight' : 'highlight-todo') } } }) } } // 排他网关 else if (n.$type === 'bpmn:ExclusiveGateway') { if (completeTask) { canvas.addMarker(n.id, completeTask.completed ? 'highlight' : 'highlight-todo') n.outgoing?.forEach(nn => { const targetTask = this.taskList.find(m => m.key === nn.targetRef.id) if (targetTask) { canvas.addMarker(nn.id, targetTask.completed ? 'highlight' : 'highlight-todo') canvas.addMarker(nn.targetRef.id, targetTask.completed ? 'highlight' : 'highlight-todo') } }) } } // 并行网关 else if (n.$type === 'bpmn:ParallelGateway') { if (completeTask) { canvas.addMarker(n.id, completeTask.completed ? 'highlight' : 'highlight-todo') n.outgoing?.forEach(nn => { debugger const targetTask = this.taskList.find(m => m.key === nn.targetRef.id) if (targetTask) { canvas.addMarker(nn.id, targetTask.completed ? 'highlight' : 'highlight-todo') canvas.addMarker(nn.targetRef.id, targetTask.completed ? 'highlight' : 'highlight-todo') } }) } } else if (n.$type === 'bpmn:StartEvent') { n.outgoing.forEach(nn => { const completeTask = this.taskList.find(m => m.key === nn.targetRef.id) if (completeTask) { canvas.addMarker(nn.id, 'highlight') canvas.addMarker(n.id, 'highlight') return } }) } else if (n.$type === 'bpmn:EndEvent') { if (endTask.key === n.id && endTask.completed) { canvas.addMarker(n.id, 'highlight') return } } }) }, // 对外 api getProcess() { const element = this.getProcessElement() return { id: element.id, name: element.name, category: element.$attrs['flowable:processCategory'] } }, getProcessElement() { const rootElements = this.modeler.getDefinitions().rootElements for (let i = 0; i < rootElements.length; i++) { if (rootElements[i].$type === 'bpmn:Process') return rootElements[i] } }, async saveXML(download = false) { try { const { xml } = await this.modeler.saveXML({ format: true }) if (download) { this.downloadFile(`${this.getProcessElement().name}.bpmn20.xml`, xml, 'application/xml') } return xml } catch (err) { console.log(err) } }, async showXML() { try { const { xml } = await this.modeler.saveXML({ format: true }) debugger this.$emit('showXML',xml) } catch (err) { console.log(err) } }, async saveImg(type = 'svg', download = false) { try { const { svg } = await this.modeler.saveSVG({ format: true }) if (download) { this.downloadFile(this.getProcessElement().name, svg, 'image/svg+xml') } return svg } catch (err) { console.log(err) } }, async save() { const process = this.getProcess() const xml = await this.saveXML() const svg = await this.saveImg() const result = { process, xml, svg } this.$emit('save', result) window.parent.postMessage(result, '*') }, openBpmn(file) { const reader = new FileReader() reader.readAsText(file, 'utf-8') reader.onload = () => { this.createNewDiagram(reader.result) } return false }, downloadFile(filename, data, type) { var a = document.createElement('a') var url = window.URL.createObjectURL(new Blob([data], { type: type })) a.href = url a.download = filename a.click() window.URL.revokeObjectURL(url) }, /** 获取数据类型 */ dataType(data){ this.$emit('dataType', data) } } } </script> <style lang="scss"> /*左边工具栏以及编辑节点的样式*/ @import "~bpmn-js/dist/assets/diagram-js.css"; @import "~bpmn-js/dist/assets/bpmn-font/css/bpmn.css"; @import "~bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css"; @import "~bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css"; .view-mode { .el-header, .el-aside, .djs-palette, .bjs-powered-by { display: none; } .el-loading-mask { background-color: initial; } .el-loading-spinner { display: none; } } .flow-containers { // background-color: #ffffff; width: 100%; height: 100%; .canvas { width: 100%; height: 100%; } .panel { position: absolute; right: 0; top: 50px; width: 300px; } .load { margin-right: 10px; } .el-form-item__label{ font-size: 13px; } .djs-palette{ left: 0px!important; top: 0px; border-top: none; } .djs-container svg { min-height: 650px; } .highlight.djs-shape .djs-visual > :nth-child(1) { fill: green !important; stroke: green !important; fill-opacity: 0.2 !important; } .highlight.djs-shape .djs-visual > :nth-child(2) { fill: green !important; } .highlight.djs-shape .djs-visual > path { fill: green !important; fill-opacity: 0.2 !important; stroke: green !important; } .highlight.djs-connection > .djs-visual > path { stroke: green !important; } // .djs-connection > .djs-visual > path { // stroke: orange !important; // stroke-dasharray: 4px !important; // fill-opacity: 0.2 !important; // } // .djs-shape .djs-visual > :nth-child(1) { // fill: orange !important; // stroke: orange !important; // stroke-dasharray: 4px !important; // fill-opacity: 0.2 !important; // } .highlight-todo.djs-connection > .djs-visual > path { stroke: orange !important; stroke-dasharray: 4px !important; fill-opacity: 0.2 !important; } .highlight-todo.djs-shape .djs-visual > :nth-child(1) { fill: orange !important; stroke: orange !important; stroke-dasharray: 4px !important; fill-opacity: 0.2 !important; } .overlays-div { font-size: 10px; color: red; width: 100px; top: -20px !important; } } </style>