文章目录
具体呈现效果
汉化包自定义
Pinia存储所需数据
组件自定义
组件左侧栏框的自定义
组件操作栏框的自定义
渲染组件的自定义
封装组件
调整Task大小弹框
总结
项目环境:
"vue": "^3.4.15"
"sass": "^1.71.1"
"element-plus": "^2.5.6"
"pinia": "^2.1.7"
"bpmn-js": "^17.0.2"
"bpmn-js-properties-panel": "^5.13.0"
"min-dash": "^4.2.1"
"tiny-svg": "^4.0.0"
"diagram-js": "^14.1.0"
具体呈现效果
汉化包自定义
src/utils/bpmn/tanslatetranslations.js
const translations = {
Name: '名称',
Value: '值',
ID: '唯一标识(ID)',
General: '基础属性',
Documentation: '文档',
'Element documentation': '元素文档说明',
Executable: '可执行的',
'Activate hand tool': '拖动屏幕',
'Change element': '改变元素',
'Activate global connect tool': '连接',
'Activate lasso tool': '套索',
'Activate create/remove space tool': '创建/删除空间',
'Create start event': '开始事件',
'Create intermediate/boundary event': '中间/边界事件',
'Create end event': '结束事件',
'Create gateway': '网关',
'Create task': '任务',
'Create expanded sub-process': '扩展子流程',
'Create data store reference': '数据存储引用',
'Create data object reference': '数据对象引用',
'Create pool/participant': '池/参与者',
'Create group': '团队',
'Append end event': '结束事件',
'Append gateway': '网关',
'Append task': '任务',
'Append intermediate/boundary event': '中间/边界事件',
'Add text annotation': '文本注释',
Delete: '删除',
'Connect to other element': '连接另一个元素',
'Add lane above': '上方添加通道',
'Divide into two lanes': '分成两条通道',
'Divide into three lanes': '分成三条通道',
'Add lane below': '下方添加通道',
}
export const customTranslate = (template, replacements) => {
replacements = replacements || {}
template = translations[template] || template
return template.replace(/{([^}]+)}/g, (_, key) => {
return replacements[key] || '{' + key + '}'
})
}
注:可能部分翻译不到位请谅解,毕竟是需要符合本人使用时习惯所翻译。而其中并未导入所有的汉化内容,而是根据本人使用具体使用时,用到的部分汉化。
Pinia存储所需数据
src\stores\index.js
import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia
export * from './bpmn'
src\stores\bpmn.js
import { defineStore } from 'pinia'
import { ref, toRaw } from 'vue'
export const useBpmnStore = defineStore(
'bpmn',
() => {
//存储链接所需信息
const linkAppendServiceLinkEnd = ref('')
//存储bpmn的操作所需要的对象
const modeler = ref()
const elementRegistry = ref()
//控制调整Task大小弹窗的弹出与关闭
const bpmnObjectInformation = ref({
isOpen: false,
windowName: '编辑元素宽高',
item: {},
baseWidth: 0,
baseHeight: 0,
open: function (item) {
this.item = item
this.baseWidth = item.width
this.baseHeight = item.height
this.isOpen = true
},
close: function () {
this.isOpen = false
},
//关键函数,负责重设某个元素的大小
resetItem: function () {
let modeling = modeler.value.get('modeling')
modeling.resizeShape(toRaw(this.item), {
x: this.item.x,
y: this.item.y,
width: parseInt(this.baseWidth),
height: parseInt(this.baseHeight)
})
this.isOpen = false
}
})
const getLinkAppendServiceLinkEnd = () => {
return linkAppendServiceLinkEnd.value
}
const setLinkAppendServiceLinkEnd = (newLinkAppendServiceLinkEnd) => {
linkAppendServiceLinkEnd.value = newLinkAppendServiceLinkEnd
}
const removeLinkAppendServiceLinkEnd = () => {
linkAppendServiceLinkEnd.value = ''
}
//获取bpmn的操作所需要的对象
const setModeler = (newModeler, newElementRegistry) => {
modeler.value = newModeler
elementRegistry.value = newElementRegistry
}
return {
getLinkAppendServiceLinkEnd,
setLinkAppendServiceLinkEnd,
removeLinkAppendServiceLinkEnd,
setModeler,
bpmnObjectInformation
}
}
)
组件自定义
这部分主要是为了自定义组件,并能够显示出来,能够实现颜色、大小等自定义。
src\utils\bpmn\palette\index.js
import CustomContextPad from './CustomContextPad'
import CustomPalette from './CustomPalette'
import CustomRenderer from './CustomRenderer'
/**
* 在基础上添加CustomPalette/CustomContextPad的自定义(保留最开始的组件)
*/
// export default {
// __init__: ['customContextPad', 'customPalette', 'customRenderer'],
// customPalette: ['type', CustomPalette],
// customContextPad: ['type', CustomContextPad],
// customRenderer: ['type', CustomRenderer]
// }
/**
* CustomPalette/CustomContextPad的自定义(不保留最开始的组件)
*/
export default {
__init__: ['contextPadProvider', 'paletteProvider', 'customRenderer'],
paletteProvider: ['type', CustomPalette],
contextPadProvider: ['type', CustomContextPad],
customRenderer: ['type', CustomRenderer]
}
src/assets/bpmn.scss
//作为css文件,为后续渲染操作栏中各个元素颜色提供条件
.canvas {
width: 100%;
height: 100%;
}
.properties {
position: absolute;
top: 16px;
right: 24px;
width: 210px;
flex: 1;
z-index: 1;
}
.general-education-compulsory-course {
color: rgba(249, 197, 153, 1) !important;
}
.subject-based-course {
color: rgba(141, 177, 226, 1) !important;
}
.professional-basic-compulsory-course {
color: rgba(242, 220, 218, 1) !important;
}
.practice-section {
color: rgba(153, 255, 204, 1) !important;
}
.strong-link {
color: rgba(93, 93, 93, 1) !important;
}
.week-link {
color: rgba(163,187,223, 1) !important;
}
src\utils\bpmn\palette\item.js
//存储一些基本信息,确保自定义文件中的信息不会产生歧义
const itemColor = {
'general-education-compulsory-course': '#f9c599',
'subject-based-course': '#8db1e2',
'professional-basic-compulsory-course': '#f2dcda',
'practice-section': '#99ffcc',
'strong-link': '#5d5d5d',
'week-link': '#a3bbdf'
}
const itemText = {
'general-education-compulsory-course': '通识必修课',
'subject-based-course': '学科基础课',
'professional-basic-compulsory-course': '专业基础必修课',
'practice-section': '实践部分'
}
const itemClass = {
'strong-link': 'marker-strong-end',
'week-link': 'marker-week-end'
}
const General_Education_Compulsory_Course = 'general-education-compulsory-course'
const Subject_Based_Course = 'subject-based-course'
const Professional_Basic_Compulsory_Course = 'professional-basic-compulsory-course'
const Practice_Section = 'practice-section'
const Strong_Link = 'strong-link'
const Week_Link = 'week-link'
export {
itemColor,
itemText,
itemClass,
General_Education_Compulsory_Course,
Subject_Based_Course,
Professional_Basic_Compulsory_Course,
Practice_Section,
Week_Link,
Strong_Link
}
组件左侧栏框的自定义
src\utils\bpmn\palette\CustomPalette.js
import '@/assets/bpmn.scss'
import { General_Education_Compulsory_Course, Subject_Based_Course, Professional_Basic_Compulsory_Course, Practice_Section} from './item'
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() {
const { bpmnFactory, create, elementFactory, translate } = this
//构建Task,作为最基本元素,操作它可以类比出其他的组件
function createTask(suitabilityScore) {
return function (event) {
const businessObject = bpmnFactory.create('bpmn:Task')
const documentationObject = bpmnFactory.create('bpmn:Documentation')
documentationObject.text = suitabilityScore
//存储数据到document中,方便于后端获取,以及重新渲染
businessObject.documentation = [documentationObject]
businessObject.suitable = suitabilityScore
const shape = elementFactory.createShape({
type: 'bpmn:Task',
businessObject: businessObject,
height: 40
})
create.start(event, shape)
}
}
// 返回需要的组件
return {
'create.general-education-compulsory-course': {
group: 'activity',
//控制操作栏中元素的颜色
className: 'bpmn-icon-task general-education-compulsory-course',
//控制操作栏中元素的提示文字
title: translate('通识必修课'),
action: {
//控制操作栏中元素的各种函数
dragstart: createTask(General_Education_Compulsory_Course),
click: createTask(General_Education_Compulsory_Course)
}
},
'create.subject-based-course': {
group: 'activity',
className: 'bpmn-icon-task subject-based-course',
title: translate('学科基础课'),
action: {
dragstart: createTask(Subject_Based_Course),
click: createTask(Subject_Based_Course)
}
},
'create.professional-basic-compulsory-course': {
group: 'activity',
className: 'bpmn-icon-task professional-basic-compulsory-course',
title: translate('专业基础必修课'),
action: {
dragstart: createTask(Professional_Basic_Compulsory_Course),
click: createTask(Professional_Basic_Compulsory_Course)
}
},
'create.practice-section': {
group: 'activity',
className: 'bpmn-icon-task practice-section',
title: translate('事件部分'),
action: {
dragstart: createTask(Practice_Section),
click: createTask(Practice_Section)
}
}
}
}
}
CustomPalette.$inject = ['bpmnFactory', 'create', 'elementFactory', 'palette', 'translate']
组件操作栏框的自定义
src\utils\bpmn\palette\CustomContextPad.js
import '@/assets/bpmn.scss'
import { General_Education_Compulsory_Course, Subject_Based_Course, Professional_Basic_Compulsory_Course, Practice_Section, Strong_Link, Week_Link } from './item'
import { useBpmnStore } from '@/stores'
const bpmnStore = useBpmnStore()
export default class CustomContextPad {
constructor(
bpmnFactory,
contextPad,
create,
elementFactory,
translate,
modeling,
globalConnect,
connect
) {
this.bpmnFactory = bpmnFactory
this.create = create
this.elementFactory = elementFactory
this.translate = translate
this.modeling = modeling
this.globalConnect = globalConnect
this.connect = connect
this.suitabilityScore = undefined
contextPad.registerProvider(this)
}
getContextPadEntries(element) {
const { bpmnFactory, create, elementFactory, translate, modeling, connect } = this
//构建Task,作为最基本元素,操作它可以类比出其他的组件
function appendServiceTaskStart(suitabilityScore) {
return function (event) {
const businessObject = bpmnFactory.create('bpmn:Task')
const documentationObject = bpmnFactory.create('bpmn:Documentation')
documentationObject.text = suitabilityScore
//存储数据到document中,方便于后端获取,以及重新渲染
businessObject.documentation = [documentationObject]
businessObject.suitable = suitabilityScore
const shape = elementFactory.createShape({
type: 'bpmn:Task',
businessObject: businessObject,
height: 40
})
create.start(event, shape, element)
}
}
//连接开始时,pinia存储所需要渲染的数据,由于不知道如何在渲染结束时获取数据
function appendServiceLinkStart(suitabilityScore) {
return function (event) {
// this.suitabilityScore = suitabilityScore
bpmnStore.setLinkAppendServiceLinkEnd(suitabilityScore)
connect.start(event, element, undefined)
}
}
//连接结束时,从pinia存储的数据中,取出所需要存储在document中的数据,并存储
function appendServiceLinkEnd(item) {
if (item.type === 'bpmn:SequenceFlow' && item.suitable === undefined) {
item.suitable = bpmnStore.getLinkAppendServiceLinkEnd()
if (item.di?.bpmnElement) {
const documentationObject = bpmnFactory.create('bpmn:Documentation')
documentationObject.text = item.suitable
item.di.bpmnElement.documentation = [documentationObject]
}
}
}
// 返回需要的组件
return {
'create.general-education-compulsory-course': {
group: 'model',
//控制操作栏中元素的颜色
className: 'bpmn-icon-task general-education-compulsory-course',
//控制操作栏中元素的提示文字
title: translate('通识必修课'),
action: {
//控制操作栏中元素的各种函数
click: appendServiceTaskStart(General_Education_Compulsory_Course),
dragstart: appendServiceTaskStart(General_Education_Compulsory_Course)
}
},
'create.subject-based-course': {
group: 'model',
className: 'bpmn-icon-task subject-based-course',
title: translate('学科基础课'),
action: {
click: appendServiceTaskStart(Subject_Based_Course),
dragstart: appendServiceTaskStart(Subject_Based_Course)
}
},
'create.professional-basic-compulsory-course': {
group: 'model',
className: 'bpmn-icon-task professional-basic-compulsory-course',
title: translate('专业基础必修课'),
action: {
click: appendServiceTaskStart(Professional_Basic_Compulsory_Course),
dragstart: appendServiceTaskStart(Professional_Basic_Compulsory_Course)
}
},
'create.practice-section': {
group: 'model',
className: 'bpmn-icon-task practice-section',
title: translate('事件部分'),
action: {
click: appendServiceTaskStart(Practice_Section),
dragstart: appendServiceTaskStart(Practice_Section)
}
},
'create.strong-link': {
group: 'activity',
className: 'bpmn-icon-connection-multi strong-link',
title: translate('强连接'),
action: {
click: appendServiceLinkStart(Strong_Link),
dragstart: appendServiceLinkStart(Strong_Link),
drag: connect.move,
dragend: appendServiceLinkEnd(arguments[0])
}
},
'create.week-link': {
group: 'activity',
className: 'bpmn-icon-connection-multi week-link',
title: translate('弱连接'),
action: {
//控制线段开始时,去进行一些自定义操作
click: appendServiceLinkStart(Week_Link),
dragstart: appendServiceLinkStart(Week_Link),
//调用原生方法,做成原生一样的效果
drag: connect.move,
//控制线段结束,取出相关信息,并在渲染组件中,将相关信息渲染
dragend: appendServiceLinkEnd(arguments[0])
}
},
'create.delete': {
group: 'edit',
className: 'icon-custom bpmn-icon-trash',
title: translate('删除'),
action: {
click: () => {
//删除对应的元素,你可以在此处处理,如添加二次确认框
modeling.removeElements([element])
}
}
},
'create.edit': {
group: 'edit',
className: 'icon-custom bpmn-icon-screw-wrench',
title: translate('编辑'),
action: {
click: () => {
//打开编辑弹出,去重写图像大小
bpmnStore.bpmnObjectInformation.open(element)
}
}
}
}
}
}
CustomContextPad.$inject = [
'bpmnFactory',
'contextPad',
'create',
'elementFactory',
'translate',
'modeling',
'globalConnect',
'connect'
]
注:作者不知道如何将起点元素和终点元素的链接,换为自定义的链接,或者说我所写的项目也并没有提出这样的要求,而网上找到的解决方案为修改源文件,本人才疏学浅,如有大佬也望能够沟通斧正。
渲染组件的自定义
src\utils\bpmn\palette\CustomRenderer.js
import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer'
import { itemColor, itemText, itemClass } from './Item'
import {
attr as svgAttr
} from 'tiny-svg'
import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'
import { isNil } from 'min-dash'
const HIGH_PRIORITY = 1500
export default class CustomRenderer extends BaseRenderer {
constructor(eventBus, bpmnRenderer) {
super(eventBus, HIGH_PRIORITY)
this.bpmnRenderer = bpmnRenderer
this.isShow = false
}
canRender(element) {
return !element.labelTarget
}
//绘画普通图像
drawShape(parentNode, element) {
const shape = this.bpmnRenderer.drawShape(parentNode, element)
const suitabilityScoreBase = this.getSuitabilityScore(element)
const suitabilityScore = this.getText(suitabilityScoreBase)
//从document中取出信息,方便于渲染颜色
if (
element.di?.bpmnElement?.documentation &&
element.di?.bpmnElement?.documentation.length > 0
) {
svgAttr(shape, {
fill: this.getColor(element.di?.bpmnElement?.documentation[0].text),
rx: 0,
ry: 0,
height: element.height,
width: element.width
})
} else if (!isNil(suitabilityScore)) {
svgAttr(shape, {
fill: this.getColor(suitabilityScoreBase),
rx: 0,
ry: 0,
height: 40
})
}
return shape
}
getShapePath(shape) {
return this.bpmnRenderer.getShapePath(shape)
}
getSuitabilityScore(element) {
const businessObject = getBusinessObject(element)
const { suitable } = businessObject
return Number.isFinite(suitable) ? suitable : suitable
}
getColor(suitabilityScore) {
return itemColor[suitabilityScore]
}
getText(suitabilityScore) {
return itemText[suitabilityScore]
}
getClass(suitabilityScore) {
return itemClass[suitabilityScore]
}
//绘画连线图案
drawConnection(parentNode, element, number) {
const connection = this.bpmnRenderer.drawConnection(parentNode, element)
if (isNaN(number)) {
number = 0
}
//控制图像渲染此处,以防数字太大而导致一直渲染
//由于本函数渲染通过全局Store去存储并获取信息,所以使用定时器并结合这种方式,以防驻留在内存中,导致卡顿
//若有更好方式解决,请斧正,十分感谢
if (number >= 12) {
element.suitable = ''
return connection
}
if (element.suitable) {
svgAttr(connection, {
stroke: this.getColor(element.suitable)
})
this.isShow = true
} else if (element.di?.bpmnElement?.documentation) {
svgAttr(connection, {
stroke: this.getColor(element.di?.bpmnElement?.documentation[0]?.text)
})
this.isShow = true
} else {
if (this.isShow) {
this.isShow = false
return connection
}
setTimeout(() => {
number++
this.drawConnection(parentNode, element, number)
}, 100)
}
return connection
}
}
CustomRenderer.$inject = ['eventBus', 'bpmnRenderer']
封装组件
src\components\teacher\index\auxiliaryFunctions\children\course_topology_page.vue
<script setup>
import { ref, onMounted } from 'vue'
import Modeler from 'bpmn-js/lib/Modeler'
import customModule from '@/utils/bpmn/palette'
/** 本处导入自己封装的请求文件,并修改下列请求相关内容 */
import instance from '@/utils/request.js'
import 'bpmn-js/dist/assets/diagram-js.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css'
import 'bpmn-js/dist/assets/bpmn-js.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/element-templates.css'
import 'bpmn-js-properties-panel/dist/assets/properties-panel.css'
import { customTranslate } from '@/utils/bpmn/tanslate/translations'
import { useBpmnStore } from '@/stores'
import bpmn_edit_dialog from '@/components/dialog/bpmn_edit_dialog.vue'
const bpmnStore = useBpmnStore()
const canvas = ref(null)
const properties = ref(null)
const modeler = ref(null)
const elementRegistry = ref(null)
onMounted(() => {
initCurrentBpmn()
})
//初始化BPMN内容,此处从后端获取,若需要改为从前端获取,请自行修改
const initCurrentBpmn = async () => {
if (modeler.value) {
//清空相关内容,以防多次渲染bpmn,造成多个bpmn页面共存
modeler.value.clear()
} else {
//创建新对象,去渲染相关内容
modeler.value = new Modeler({
container: canvas.value,
propertiesPanel: {
parent: properties.value
},
additionalModules: [
customModule,
{
translate: ['value', customTranslate]
}
]
})
}
let bpmnXML = ''
try {
await instance
.get('/api/CourseTopology/get', {
grade: bpmnStore.currentGrade,
majorId: bpmnStore.currentMajorId
})
.then((res) => {
if (res.data?.xmlJsonString) {
//获取后端数据,去渲染图像
bpmnXML = res.data.xmlJsonString
}
})
.catch((err) => {
console.log(err)
})
// ...
} catch (err) {
// err...
}
await modeler.value.importXML(bpmnXML)
elementRegistry.value = modeler.value.get('elementRegistry')
//将数据存储到pinia中,为了自定义渲染所需要使用时能够获取
bpmnStore.setModeler(modeler.value, elementRegistry.value)
}
const updateCourseTopology = () => {
const xmlString = modeler.value.saveXML({ format: true })
xmlString.then((data) => {
const fileName = 'test.bpmn20.xml'
const blob = new Blob([data.xml], { type: 'application/xml' })
const file = new File([blob], fileName, { type: 'application/xml' })
updateCourseTopologyOp(file)
})
}
const setCurrentTopology = () => {
bpmnObjectSetCurrentInformation.value.open()
}
//将bpmn转变为svg图像,能够用于后续操作
const saveSVG = async () => {
const { svg } = await modeler.value.saveSVG({ format: true })
const dataTrack = 'bpmn'
const a = document.createElement('a')
const name = `.${dataTrack}20.svg`
a.setAttribute('href', `data:application/bpmn20-xml;charset=UTF-8,${encodeURIComponent(svg)}`)
a.setAttribute('target', '_blank')
a.setAttribute('dataTrack', `diagram:download-${dataTrack}`)
a.setAttribute('download', name)
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}
const emit = defineEmits([
'saveSVG',
])
onMounted(() => {
emit('saveSVG', saveSVG)
})
</script>
<template>
<div class="content-standrd-content editor">
<div ref="properties" class="properties"></div>
<div ref="canvas" class="canvas"></div>
</div>
<bpmn_edit_dialog :bpmnObjectInformation="bpmnStore.bpmnObjectInformation"> </bpmn_edit_dialog>
</template>
<style scoped>
@import url('bpmn-js-properties-panel/dist/assets/properties-panel.css');
@import url('@/assets/bpmn.scss');
</style>
调整Task大小弹框
src\components\dialog\bpmn_edit_dialog.vue
<script setup>
import { ref } from 'vue'
const props = defineProps({ bpmnObjectInformation: Object })
const bpmnObjectInformation = ref(props.bpmnObjectInformation)
</script>
<template>
<el-dialog
v-model="bpmnObjectInformation.isOpen"
:title="bpmnObjectInformation.windowName"
style="padding: 16px; width: 500px"
>
<el-form :model="bpmnObjectInformation" style="text-align: center; margin-top: 10px">
<el-form-item label="宽度" label-width="80px">
<el-input v-model="bpmnObjectInformation.baseWidth" autocomplete="off" />
</el-form-item>
<el-form-item label="高度" label-width="80px">
<el-input v-model="bpmnObjectInformation.baseHeight" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="bpmnObjectInformation.close()">取消</el-button>
<el-button type="primary" @click="bpmnObjectInformation.resetItem()"> 确认 </el-button>
</span>
</template>
</el-dialog>
</template>
总结
这次是我第一次使用CSDN所作的笔记,网上大多数的教程大多操作繁琐,并且希望一步能过够完整了解BPMN。而我作为一个初学者,站在我的角度上,我希望有一个入门的教程,使得我首先能够快速的入手BPMN,并能够从中获取其基本使用。故出此笔记,如果能给你帮助,那么我十分荣幸,若做得不到位,请斧正而非人身攻击。
本文章为BPMN-JS在VUE3中的入门应用,并非深入剖析,主要做到BPMN设置其元素改变元素颜色及其大小。并能够将BPMN给最终导出为SVG图标,进一步处理。
关于后端与前端交互问题,能够解决,由于前端将数据写入document中,可供后端进行操作并且存储,而也能有后端填入相关字段,使得前端渲染出不同的颜色及大小。