文章目录
- 一、bpmn.js是什么?
- 二、使用步骤
- 1.引入bpmn
- 2.使用bpmn
- 3.引入bpmn-左侧工具栏
- 4.引入bpmn-左侧工具栏
- 5.引入bpmn数据导出
- 6.数据导出为svg格式
- 7.监听modeler并绑定事件
- 7.监听element点击……
- 8.自定义左侧工具栏图标
- 9.自定义左侧工具栏完整效果
- 10.右侧展示自定义节点内容
- 总结
一、bpmn.js是什么?
bpmn.js是一个基于JavaScript的库,用于在Web应用程序中创建、查看和编辑BPMN 2.0流程图。
二、使用步骤
1.引入bpmn
import BpmnModeler from "bpmn-js/lib/Modeler";
import { xmlStr } from "../mock/xmlStr";
2.使用bpmn
代码如下:
//html
<div class="containers">
<div class="canvas" ref="canvas"></div>
</div>
//数据
return {
// bpmn建模器
bpmnModeler: null,
container: null,
canvas: null
};
//methods
init() {
// 获取到属性ref为“canvas”的dom节点
const canvas = this.$refs.canvas;
// 建模
this.bpmnModeler = new BpmnModeler({
container: canvas
});
this.createNewDiagram();
},
createNewDiagram() {
// 将字符串转换成图显示出来
console.log(xmlStr);
this.bpmnModeler.importXML(xmlStr, err => {
if (err) {
// console.error(err)
} else {
// 这里是成功之后的回调, 可以在这里做一系列事情
this.success();
}
});
},
success() {
// console.log('创建成功!')
}
},
mounted() {
this.init();
},
};
</script>
<style lang="scss" scoped>
.containers {
position: absolute;
background-color: #ffffff;
width: 100%;
height: 100%;
}
.canvas {
width: 100%;
height: 100%;
}
.panel {
position: absolute;
right: 0;
top: 0;
width: 300px;
}
</style>
页面效果如图:
3.引入bpmn-左侧工具栏
这个很方便 直接在main.js中引入即可
// main.js中引入以下为bpmn工作流绘图工具的样式
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'
效果如图:
4.引入bpmn-左侧工具栏
1. 安装bpmn-js-properties-panel插件
2. import 'bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css' // main.css中引入右边工具栏样式
3. 在页面中引入propertiesProviderModule和propertiesPanelModule
...
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'
...
html结构
<div class="containers">
<div class="canvas" ref="canvas"></div>
<div id="js-properties-panel" class="panel"></div>
</div>
//在上边init基础上进行添加配置
init() {
// 获取到属性ref为“canvas”的dom节点
const canvas = this.$refs.canvas
// 建模
this.bpmnModeler = new BpmnModeler({
container: canvas,
//添加控制板
propertiesPanel: {
parent: '#js-properties-panel'
},
additionalModules: [
// 右边的属性栏
propertiesProviderModule,
propertiesPanelModule
],
moddleExtensions: {
camunda: camundaModdleDescriptor
}
})
this.createNewDiagram()
加载成功
5.引入bpmn数据导出
之前的createNewDiagram事件就是用将数据显示出来,他的第一个参数就是xml数据,动态渲染在拿到后端返回的数据之后重新调用这个方法即可
// 将字符串转换成图显示出来
this.bpmnModeler.importXML(this.xmlStr, err => {
if (err) {
// console.error(err)
} else {
// 这里是成功之后的回调, 可以在这里做一系列事情
this.success();//在success回调中绑定事件进行监听添加绑定事件
}
});
success(){
const that = this;
// 给图绑定事件,当图有发生改变就会触发这个事件
this.bpmnModeler.on("commandStack.changed", function() {
that.saveDiagram(function(err, xml) {
console.log(xml); // 这里获取到的就是最新的xml信息
});
});
}
// 下载为bpmn格式,done是个函数,调用的时候传入的
saveDiagram(done) {
// 把传入的done再传给bpmn原型的saveXML函数调用
this.bpmnModeler.saveXML({ format: true }, function(err, xml) {
done(err, xml);
});
}
6.数据导出为svg格式
有时候需要数据导出为svg格式的
首先在页面上定义好a标签用来下载数据
html
<a ref="xml" href="javascript:;">xml</a>
<a ref="svg" href="javascript:;">svg</a>
js
上边讲过从后端拿数据渲染之后有个success()回调
我们在这个回调里进行监听每次改变就会拿到xml数据,svg和它一样的,只需稍微改造一下
const that = this;
const downloadLink = this.$refs.xml;//首先获取页面上的a标签
const downloadSvgLink = this.$refs.svg;
// 给图绑定事件,当图有发生改变就会触发这个事件
this.bpmnModeler.on("commandStack.changed", function() {
//每次更改页面都会获取到xml和svg类型的数据保存到href中备用
that.saveDiagram(function(err, xml) {
console.log(xml); // 这里获取到的就是最新的xml信息 saveDiagram返回的数据
const data = encodeURIComponent(xml);
downloadLink.href =
"data:application/bpmn20-xml;charset=UTF-8," + data;
downloadLink.download = "1.bpmn";
});
that.saveSvg(function(err, svg) {
const data = encodeURIComponent(svg);
console.log(svg); // 这里获取到的就是最新的xml信息 saveDiagram返回的数据
downloadSvgLink.href ="data:application/bpmn20-xml;charset=UTF-8," + data;
downloadSvgLink.download = "1.svg";
});
});
// 下载为bpmn格式,done是个函数,调用的时候传入的
saveDiagram(done) {
// 把传入的done再传给bpmn原型的saveXML函数调用
this.bpmnModeler.saveXML({ format: true }, function(err, xml) {
done(err, xml);
});
},
saveSvg(done) {
this.bpmnModeler.saveSVG(done);
}
7.监听modeler并绑定事件
sussec中调用下边这个方法 用来监听
this.addModelerListener()
// 监听 modeler
addModelerListener() {
const bpmnjs = this.bpmnModeler;
const that = this;
// 用一个forEach给modeler上添加要绑定的事件
const events = [
"shape.added",
"shape.move.end",
"shape.removed",
"connect.end",
"connect.move"
];
events.forEach(function(event) {
that.bpmnModeler.on(event, e => {
console.log(event, e);
var elementRegistry = bpmnjs.get("elementRegistry");
var shape = e.element ? elementRegistry.get(e.element.id) : e.shape;
console.log(shape);
});
});
},
7.监听element点击……
success() {
console.log("创建成功!");
this.addBpmnListener(); // 页面改变触发
this.addModelerListener(); // 监听 modeler
this.addEventBusListener(); //监听元素
},
addEventBusListener() {
let that = this;
const eventBus = this.bpmnModeler.get("eventBus"); // 需要使用eventBus
const eventTypes = ["element.click", "element.changed"]; // 需要监听的事件集合
eventTypes.forEach(function(eventType) {
eventBus.on(eventType, function(e) {
console.log(e);
});
});
},
8.自定义左侧工具栏图标
再以上的基础上去components文件夹创建文件
custom/CustomPalette.js 核心
index.js
// CustomPalette.js
export default class CustomPalette {
constructor(bpmnFactory, create, elementFactory, palette, translate) {
this.bpmnFactory = bpmnFactory;
this.create = create;
this.elementFactory = elementFactory;
this.translate = translate;
palette.registerProvider(this);
}
// 这个函数就是绘制palette的核心
getPaletteEntries(element) {
const {
bpmnFactory,
create,
elementFactory,
translate
} = this;
function createTask() {
return function (event) {
const businessObject = bpmnFactory.create('bpmn:Task');
businessObject['custom'] = 1
const shape = elementFactory.createShape({
type: 'bpmn:Task',
businessObject
});
console.log(shape) // 只在拖动或者点击时触发
create.start(event, shape);
}
}
return {
'create.lindaidai-task': {
group: 'model', // 分组名
className: 'icon-custom lindaidai-task', // 样式类名
title: translate('创建一个类型为lindaidai-task的任务节点'),
action: { // 操作
dragstart: createTask(), // 开始拖拽时调用的事件
click: createTask() // 点击时调用的事件
}
}
}
}
}
CustomPalette.$inject = [
'bpmnFactory',
'create',
'elementFactory',
'palette',
'translate'
]
-------------------------------------
// custom/index.js
import CustomPalette from './CustomPalette'
export default {
__init__: ['customPalette'],
customPalette: ['type', CustomPalette]
}
自定义定义完成在页面中引入 配置样式
创建css文件在main.js中全局引入 css名对应上即可
xx.css
/* app.css */
.bpmn-icon-task.red {
color: #cc0000 !important;
}
.icon-custom {
/* 定义一个公共的类名 */
border-radius: 50%;
background-size: 65%;
background-repeat: no-repeat;
background-position: center;
}
.icon-custom.lindaidai-task {
/* 加上背景图 */
background-image: url('https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/rules.png');
}
--------------------------
main.js
import '@/assets/a.css'
--------------------------
//xxx.vue 需要使用的页面
import customModule from "../components/custom";
propertiesPanel对象中 //添加控制板
......
propertiesPanel: {
parent: "#js-properties-panel"
},
additionalModules: [
// 左边工具栏以及节点
propertiesProviderModule,
// 自定义的节点!!!!在这里
customModule,
// 右边的工具栏
propertiesPanelModule
],
看看效果
9.自定义左侧工具栏完整效果
完整版为了方便阅读 避免混乱创建的文件和第八条完全独立 此处建议删除第八条数据重新开始
此处custom为第八步创建的文件 注意区分
新建customModeler文件如下图4个js文件
//1.CustomPalette.js
export default class CustomPalette {
constructor(bpmnFactory, create, elementFactory, palette, translate) {
this.bpmnFactory = bpmnFactory;
this.create = create;
this.elementFactory = elementFactory;
this.translate = translate;
palette.registerProvider(this);
}
getPaletteEntries(element) {
const {
bpmnFactory,
create,
elementFactory,
translate
} = this;
function createTask() {
return function (event) {
const businessObject = bpmnFactory.create('bpmn:Task');//这里固定
businessObject['custom'] = 1
const shape = elementFactory.createShape({
type: 'bpmn:Task',//这里是自定义的节点名称昂!!!!
businessObject
});
console.log(shape) // 只在拖动或者点击时触发
create.start(event, shape);
}
}
return {
'create.lindaidai-task': {
group: 'model',
className: 'icon-custom lindaidai-task',
// className: 'bpmn-icon-user-task',
title: translate('创建一个类型为lindaidai-task的任务节点'),
action: {
dragstart: createTask(),
click: createTask()
}
}
}
}
}
CustomPalette.$inject = [
'bpmnFactory',
'create',
'elementFactory',
'palette',
'translate'
]
//2.CustomRenderer.js
import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer';
import {
append as svgAppend,
attr as svgAttr,
create as svgCreate
} from 'tiny-svg';
import { customElements, customConfig, hasLabelElements } from './util'
import { is } from 'bpmn-js/lib/util/ModelUtil';
const HIGH_PRIORITY = 1500
export default class CustomRenderer extends BaseRenderer {
constructor(eventBus, bpmnRenderer, modeling) {
super(eventBus, HIGH_PRIORITY);
this.bpmnRenderer = bpmnRenderer;
this.modeling = modeling;
}
canRender(element) {
// ignore labels
return !element.labelTarget;
}
drawShape(parentNode, element) {
console.log(element)
const type = element.type // 获取到类型
if (customElements.includes(type)) { // or customConfig[type]
const { url, attr } = customConfig[type]
const customIcon = svgCreate('image', {
...attr,
href: url
})
element['width'] = attr.width // 这里我是取了巧, 直接修改了元素的宽高
element['height'] = attr.height
svgAppend(parentNode, customIcon)
// 判断是否有name属性来决定是否要渲染出label
if (!hasLabelElements.includes(type) && element.businessObject.name) {
const text = svgCreate('text', {
x: attr.x,
y: attr.y + attr.height + 20,
"font-size": "14",
"fill": "#000"
})
text.innerHTML = element.businessObject.name
svgAppend(parentNode, text)
console.log(text)
}
// this.modeling.resizeShape(element, {
// x: element.x,
// y: element.y,
// width: element['width'] / 2,
// height: element['height'] / 2
// })
return customIcon
}
// else if (type === 'bpmn:TextAnnotation' && element.businessObject.color) {
// console.log('我是绿色的')
// let color = element.businessObject.color
// element.businessObject.di.set('bioc:stroke', color)
// const shape = this.bpmnRenderer.drawShape(parentNode, element)
// return shape
// }
const shape = this.bpmnRenderer.drawShape(parentNode, element)
return shape
}
getShapePath(shape) {
return this.bpmnRenderer.getShapePath(shape);
}
}
CustomRenderer.$inject = ['eventBus', 'bpmnRenderer', 'modeling'];
//3. index.js
import CustomPalette from './CustomPalette'
import CustomRenderer from './CustomRenderer'
export default {
__init__: ['customPalette', 'customRenderer'],
customPalette: ['type', CustomPalette],
customRenderer: ['type', CustomRenderer]
}
//4.util.js
const customElements = ['bpmn:Task', 'bpmn:StartEvent'] // 自定义元素的类型
const customConfig = { // 自定义元素的配置
'bpmn:Task': {
'url': require('../../assets/www.png'),
// 'url': require('../../assets/rules.png'),
// 'url': 'https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/rules.png',
'attr': { x: 0, y: 0, width: 48, height: 48 }
},
'bpmn:StartEvent': {
'url': 'https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/start.png',
'attr': { x: 0, y: 0, width: 40, height: 40 }
}
}
const hasLabelElements = ['bpmn:StartEvent', 'bpmn:EndEvent'] // 一开始就有label标签的元素类型
export { customElements, customConfig, hasLabelElements }
接下来康康效果
ok结束啦 图标不一样是因为util.js和全局的样式不一样,替换下即可
本文参考霖呆呆LinDaiDai_的文章
链接地址:https://juejin.cn/post/6844904017567416328
10.右侧展示自定义节点内容
创建组件 注入信息
<style scoped lang="scss">
.custom-properties-panel {
position: absolute;
width: 300px;
right: 20px;
top: 20px;
background-color: #fff9f9;
border-color: rgba(0, 0, 0, 0.09);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
padding: 20px;
border-radius: 8px;
background: skyblue;
fieldset {
border: 0;
}
input{
border: none;
outline: none;
padding: 10px;
border-radius: 5px;
}
}
</style>
<template>
<div class="custom-properties-panel">
<div class="empty" v-if="selectedElements.length <= 0">请选择一个元素</div>
<div class="empty" v-else-if="selectedElements.length > 1">只能选择一个元素</div>
<div v-else>
<fieldset class="element-item">
<label>节点id:</label>
<span>{{ element.id }}</span>
</fieldset>
<fieldset class="element-item">
<label>节点名称:</label>
<input :value="element.name" @change="(event) => changeField(event, 'name')" />
</fieldset>
<!-- <fieldset class="element-item">
<label>customProps:</label>
<input :value="element.name" @change="(event) => changeField(event, 'customProps')" />
</fieldset> -->
</div>
</div>
</template>
<script>
export default {
name: 'PropertiesView',
props: {
modeler: {
type: Object,
default: () => ({})
}
},
data() {
return {
selectedElements: [],
element: null,
}
},
created() {
this.init()
},
methods: {
init() {
const { modeler } = this
modeler.on('selection.changed', e => {
this.selectedElements = e.newSelection
this.element = e.newSelection[0]
})
modeler.on('element.changed', e => {
const { element } = e
const { element: currentElement } = this
if (!currentElement) {
return
}
// update panel, if currently selected element changed
if (element.id === currentElement.id) {
this.element = element
}
})
},
/**
* 改变控件触发的事件
* @param { Object } input的Event
* @param { String } 要修改的属性的名称
*/
changeField(event, type) {
console.log(event, type);
const value = event.target.value
let properties = {}
properties[type] = value
this.element[type] = value
this.updateProperties(properties)
},
updateName(name) {
const { modeler, element } = this
const modeling = modeler.get('modeling')
// modeling.updateLabel(element, name)
modeling.updateProperties(element, {
name
})
},
/**
* 更新元素属性
* @param { Object } 要更新的属性, 例如 { name: '' }
*/
updateProperties(properties) {
const { modeler, element } = this
const modeling = modeler.get('modeling')
modeling.updateProperties(element, properties)
}
}
}
</script>
根据自己需求 更改自定义样式
本demo源码
总结
还有版本问题 以下是本demo的版本 版本太高会报错哦
大家有什么问题也可以留言
希望可以帮到你
"bpmn-js": "^6.0.4",
"bpmn-js-properties-panel": "^0.33.0",
"camunda-bpmn-moddle": "^4.3.0",
未完待续~✿✿ヽ(°▽°)ノ✿