BPMN-JS在Vue3中的自定义
文章目录
具体呈现效果
汉化包自定义
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
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) }) @import url('bpmn-js-properties-panel/dist/assets/properties-panel.css'); @import url('@/assets/bpmn.scss');
调整Task大小弹框
src\components\dialog\bpmn_edit_dialog.vue
import { ref } from 'vue' const props = defineProps({ bpmnObjectInformation: Object }) const bpmnObjectInformation = ref(props.bpmnObjectInformation) 取消 确认
总结
这次是我第一次使用CSDN所作的笔记,网上大多数的教程大多操作繁琐,并且希望一步能过够完整了解BPMN。而我作为一个初学者,站在我的角度上,我希望有一个入门的教程,使得我首先能够快速的入手BPMN,并能够从中获取其基本使用。故出此笔记,如果能给你帮助,那么我十分荣幸,若做得不到位,请斧正而非人身攻击。
本文章为BPMN-JS在VUE3中的入门应用,并非深入剖析,主要做到BPMN设置其元素改变元素颜色及其大小。并能够将BPMN给最终导出为SVG图标,进一步处理。
关于后端与前端交互问题,能够解决,由于前端将数据写入document中,可供后端进行操作并且存储,而也能有后端填入相关字段,使得前端渲染出不同的颜色及大小。