1.简介
后端的实现可以参考文章:https://blog.csdn.net/fly_cheng_zi/article/details/141359478,这一篇主要是前端页面的实现,技术架构就是在node下跑vue3,页面的部分ui是Element UI。主要是两个页面,一个是流程设计页面,一个是流程编辑页面。
2.使用
1.依赖引入
# bpmn组件
npm install --save vue-bpmn
# bpmn依赖
npm install --save bpmn-js
# 属性面板
npm install --save bpmn-js-properties-panel
# 扩展属性
npm install --save camunda-bpmn-moddle #
导入bpmn组件所需
npm install --save raw-loader
2.流程设计页面
定义流程的canvas
<div class="containers" ref="containers">
<div id="js-canvas" class="canvas" ref="canvas"></div>
<div id="js-properties-panel"></div>
</div>
引入流程引擎的js配置等
import BpmnModeler from 'bpmn-js/lib/Modeler' // 引入 bpmn-js
import customTranslate from './customTranslate/customTranslate' //汉化
import xmlStr from './xml' //引入默认显示的xml字符串数据
import propertiesPanelModule from 'bpmn-js-properties-panel' //节点编辑
import propertiesProviderModule from 'bpmn-js-properties-panel/lib/provider/camunda'
import camundaModdleDescriptor from 'camunda-bpmn-moddle/resources/camunda'
创建流程设计器model,将属性挂载上去就完成了
this.containers = this.$refs.containers // 获取到属性ref为“containers”的dom节点
const canvas = this.$refs.canvas // 获取到属性ref为“canvas”的dom节点
this.bpmnModeler = new BpmnModeler({
container: canvas,
//添加控制板
propertiesPanel: {
parent: '#js-properties-panel'
},
//左侧
additionalModules: [
this.customTranslateModule,
// 右边的属性栏
propertiesProviderModule,
propertiesPanelModule
],
moddleExtensions: {
camunda: camundaModdleDescriptor
}
})
this.createNewDiagram()
},
3.完整代码
<template>
<ElButton class="btn" type="primary" @click="saveModelBpmnXml()">保存</ElButton>
<ElButton class="btn" type="primary" @click="chooseAgignee()">选择审核人</ElButton>
<div class="add_btn" id="btn_center"></div><div class="mask"></div
><div class="window" id="center"
><div class="border_add_nav"
><div> </div
><div class="border_add_nav_box flex-container"
><span class="add_title">选择审核人</span><span class="close_btn">X</span></div
></div
><div class="border_add_main_content"
><div class="border_add_main_content_box">
<div class="popover">
<input
class="search"
id="serchName"
@keyup.enter="getUserList()"
type="text"
placeholder="搜索..."
/>
<ul class="unstyled list">
<li v-for="item in userList" :key="item.id"
><a @click="setAgignee(item.username)">{{ item.name }}</a></li
>
</ul>
</div>
</div></div
><div class="border_add_btn_box"><div class="pull-right" style="float: right"></div></div
></div>
<div class="containers" ref="containers">
<div id="js-canvas" class="canvas" ref="canvas"></div>
<div id="js-properties-panel"></div>
</div>
</template>
<script>
import { ElButton, ElTag, ElInput } from 'element-plus'
import BpmnModeler from 'bpmn-js/lib/Modeler' // 引入 bpmn-js
import customTranslate from './customTranslate/customTranslate' //汉化
import xmlStr from './xml' //引入默认显示的xml字符串数据
import { useCache } from '@/hooks/web/useCache'
const { wsCache } = useCache()
//右侧属性栏功能
import propertiesPanelModule from 'bpmn-js-properties-panel'
import propertiesProviderModule from 'bpmn-js-properties-panel/lib/provider/camunda'
import camundaModdleDescriptor from 'camunda-bpmn-moddle/resources/camunda'
import request from '@/config/axios'
import axios from 'axios'
import { config } from '@/config/axios/config'
import $ from 'jquery'
const baseUrl = config.base_url.base
export default {
data() {
return {
bpmnModeler: null,
containers: null,
canvas: null,
customTranslateModule: {
translate: ['value', customTranslate]
},
userList: []
}
},
mounted() {
this.initDiagram()
},
methods: {
//初始化方法
initDiagram() {
axios.defaults.headers.common['Authorization'] = 'Bearer ' + wsCache.get('access_token')
this.containers = this.$refs.containers // 获取到属性ref为“containers”的dom节点
const canvas = this.$refs.canvas // 获取到属性ref为“canvas”的dom节点
this.bpmnModeler = new BpmnModeler({
container: canvas,
//添加控制板
propertiesPanel: {
parent: '#js-properties-panel'
},
//左侧
additionalModules: [
this.customTranslateModule,
// 右边的属性栏
propertiesProviderModule,
propertiesPanelModule
],
moddleExtensions: {
camunda: camundaModdleDescriptor
}
})
this.createNewDiagram()
},
// 注意:必须先加载一个bpmn文件,新建就是加载一个空的bpmn文件,否则不能拖拽节点
createNewDiagram() {
/**
* 获取后台,获取默认的xml
* */
let mr_xml = xmlStr //默认值-xml
let processKey = ''
let processName = ''
var diagramUrl = baseUrl + '/obpm/orange-work-flow/queryById?id=' + this.$route.query.id
axios
.get(diagramUrl)
.then((res) => {
console.log(res.data.data)
if (
res.data.data &&
res.data.data.workFlowData &&
res.data.data.workFlowData.replace(/^\s+|\s+$/g, '') != ''
) {
this.openDiagram(res.data.data.workFlowData)
return
}
if (res.data.data) {
processKey = res.data.data.orangeFlowKey
processName = res.data.data.workFlowName
mr_xml = mr_xml.replaceAll('orange_system_t', processKey)
mr_xml = mr_xml.replaceAll('t_name', processName)
this.openDiagram(mr_xml)
return
}
})
.catch((err) => {
console.log(err)
})
// let mr_xml = '' //默认值-xml
this.openDiagram(mr_xml)
},
openDiagram(xml) {
/**
* 导入xml(字符串形式),返回导入结果
* 后续会取消传入回调函数的方式
* 推荐使用async/await或者链式调用
* @param { string } xml 流程图xml字符串
* @param { Promise } callback 回调函数,出错时返回{ warnings,err }
*/
this.bpmnModeler.importXML(xml, function (err) {
if (err) {
// container
// .removeClass('with-diagram')
// .addClass('with-error');
console.error(err)
} else {
// container
// .removeClass('with-error')
// .addClass('with-diagram');
}
})
},
saveModelBpmnXml() {
const id = this.$route.query.id
const toot = this.$router
this.bpmnModeler.saveXML({ format: true }, function (err, xml) {
console.log(xml)
const orangeWorkFlow = {
id: id,
workFlowData: xml
}
var diagramUrl = baseUrl + '/obpm/orange-work-flow/edit'
axios
.post(diagramUrl, orangeWorkFlow)
.then((res) => {
console.log(res.data)
toot.push('/orangeWorkFlow/list')
})
.catch((err) => {
console.log(err)
})
})
},
getUserList() {
var diagramUrl = baseUrl + '/sys/user/list?name=' + $('#serchName').val()
axios
.get(diagramUrl)
.then((res) => {
this.userList = res.data.data.records
})
.catch((err) => {
console.log(err)
})
},
chooseAgignee() {
if ($('#camunda-assignee') && $('#camunda-assignee')[0]) {
//获取系统用户
var diagramUrl = baseUrl + '/sys/user/list'
axios
.get(diagramUrl)
.then((res) => {
this.userList = res.data.data.records
console.log(this.userList)
$('#btn_center').css('height', $(document).height())
$('.mask').css('display', 'block')
$('.mask').css('width', $(window).width())
$('.mask').css('height', $(document).height())
popCenterWindow()
})
.catch((err) => {
console.log(err)
})
// $('#camunda-assignee')[0].onclick = function () {
// alert('asad')
// }
} else {
alert('请选择一个用户任务节点再选择审核人')
}
},
setAgignee(username) {
// $('#camunda-assignee').val(username)
let element = document.getElementById('camunda-assignee') // input输入框
console.log(element)
element.value = username // 输入的内容
var event = new Event('input', {
bubbles: true,
cancelable: true
})
element.dispatchEvent(event)
$('.window').hide('slow')
$('.mask').css('display', 'none')
$('#btn_center').css('height', 0)
}
}
}
// $(window).ready(function () {
// $('#btn_center').click(function () {
// $('.mask').css('display', 'block')
// $('.mask').css('width', $(window).width())
// $('.mask').css('height', $(document).height())
// popCenterWindow()
// })
// })
//获取窗口的高度
var windowHeight
//获取窗口的宽度
var windowWidth
//获取弹窗的宽度
var popWidth
//获取弹窗高度
var popHeight
function init() {
windowHeight = $(window).height()
windowWidth = $(window).width()
popHeight = $('.window').height()
popWidth = $('.window').width()
}
//关闭窗口的方法
function closeWindow() {
$('.close_btn').click(function () {
$('.window').hide('slow')
$('.mask').css('display', 'none')
$('#btn_center').css('height', 0)
})
}
//定义弹出居中窗口的方法
function popCenterWindow() {
init()
//计算弹出窗口的左上角X的偏移量
var popX = (windowWidth - popWidth) / 2
// 计算弹出窗口的左上角Y的偏移量为窗口的高度 - 弹窗高度 / 2 + 被卷去的页面的top
var popY = (windowHeight - popHeight) / 2 + $(document).scrollTop()
//设定窗口的位置
$('#center').css('top', popY).css('left', popX).slideToggle('fast')
closeWindow()
}
</script>
<style lang="css">
/*左边工具栏以及编辑节点的样式*/
@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';
/*右侧详情*/
@import 'bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css';
.containers {
position: absolute;
background-color: #ffffff;
width: 100%;
height: 100%;
display: flex;
}
.canvas {
width: 100%;
height: 100%;
}
.bjs-powered-by {
display: none;
}
.btn {
display: inline-flex;
justify-content: center;
align-items: center;
line-height: 0.6;
height: 30px;
white-space: nowrap;
cursor: pointer;
color: var(--el-button-text-color);
text-align: center;
box-sizing: border-box;
outline: 0;
transition: 0.1s;
font-weight: var(--el-button-font-weight);
-webkit-user-select: none;
user-select: none;
vertical-align: middle;
background-color: #409eff;
border: var(--el-border);
border-color: var(--el-button-border-color);
padding: 8px 15px;
font-size: var(--el-font-size-base);
border-radius: var(--el-border-radius-base);
margin-bottom: 10px;
margin-left: 10px;
}
.btn:hover {
background-color: #333;
color: #fff;
}
.flex-container {
display: -webkit-flex;
display: flex;
-webkit-justify-content: space-between;
justify-content: space-between;
}
.window {
width: 20%;
padding-bottom: 20px;
background-color: #fff;
position: fixed;
display: none;
margin-bottom: 100px;
border: 1px solid #e0dfdf;
}
.add_btn {
height: 0px;
cursor: pointer;
}
.btn_text {
width: 80px;
height: 40px;
line-height: 40px;
text-align: center;
color: #fff;
position: absolute;
top: 50%;
left: 50%;
margin-top: -40px;
margin-left: -20px;
background-color: #fddb54;
}
.border_add_nav {
width: 100%;
border-bottom: 1px solid #e0dfdf;
}
.border_add_nav_box {
width: 90%;
margin: 0 auto;
font-size: 16px;
}
.border_add_main_content {
width: 100%;
margin-left: 5%;
margin-bottom: 3%;
overflow: hidden;
overflow-y: auto;
}
.border_add_btn_box {
width: 90%;
height: 100%;
margin: 0 auto;
overflow: hidden;
}
.add_title {
color: #20aae4;
}
.name,
.input {
margin-top: 30px;
float: left;
}
.input {
width: 160px;
height: 40px;
text-align: center;
outline: none;
appearance: none;
-moz-appearance: none;
border-radius: 4px;
border: 1px solid #c8cccf;
color: #000;
}
.cancel,
.save {
width: 80px;
height: 40px;
line-height: 40px;
float: left;
color: #fff;
text-align: center;
border-radius: 5%;
cursor: pointer;
}
.cancel {
margin-right: 10px;
background: #e0dfdf;
}
.save {
background: #20aae4;
}
.mask {
position: absolute;
top: 0;
display: none;
background-color: rgba(0, 0, 0, 0.5);
}
button {
background-color: #eee;
font-weight: 300;
font-size: 16px;
font-family: 'Helvetica Neue Light', 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande',
sans-serif;
text-decoration: none;
text-align: center;
line-height: 28px;
height: 28px;
padding: 0 16px;
margin: 0;
display: inline-block;
appearance: none;
cursor: pointer;
border: none;
box-sizing: border-box;
transition: all 0.3s;
}
button:focus,
button:hover {
background-color: #f6f6f6;
text-decoration: none;
outline: 0;
}
button:active {
text-shadow: 0 1px 0 rgb(255 255 255 / 30%);
text-decoration: none;
background-color: #eee;
border-color: #cfcfcf;
color: #999;
transition-duration: 0s;
box-shadow: inset 0 1px 3px rgb(0 0 0 / 20%);
}
ul.unstyled {
padding: 0;
margin: 0;
list-style: none;
}
ul.unstyled > li {
list-style-type: none;
}
.popover-wrapper {
position: relative;
}
.popover-wrapper .popover {
padding: 8px;
border: 1px solid #ebeef5;
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
width: 150px;
position: absolute;
right: 0;
top: 28px;
margin-top: 4px;
display: none;
}
.popover-wrapper .popover .search {
-webkit-appearance: none;
background-color: #fff;
background-image: none;
border-radius: 4px;
border: 1px solid #dcdfe6;
box-sizing: border-box;
color: #606266;
display: inline-block;
font-size: inherit;
height: 28px;
line-height: 28px;
outline: none;
padding: 0 15px;
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
width: 100%;
}
.popover-wrapper .popover .list {
margin-top: 4px;
}
.popover-wrapper .popover .list li a {
display: block;
padding: 4px 8px;
text-decoration: none;
color: #000;
transition: all 0.3s;
}
.popover-wrapper .popover .list li a:hover,
.popover-wrapper .popover .list li a:focus {
background: rgba(39, 174, 96, 0.2);
}
.popover-wrapper .popover .list li a:active {
background: rgba(39, 174, 96, 0.8);
}
.logs {
display: inline-block;
vertical-align: text-top;
width: 400px;
padding: 8px;
border: 1px solid;
height: 400px;
overflow: auto;
margin-left: 16px;
}
.logs > p {
margin: 0 0 8px;
}
.container {
height: 600px;
width: 600px;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
}
</style>
4.总结
前端部分比较简单,引入对应的依赖之后,就可以设计流程,一般来说,我们设计好之后,需要将流程图保存到业务库,然后部署发布流程之后,才可以审批等。如大家需要前后端架构源码可联系博主。