Skip to content

安装 bpmn-js

sh
npm i bpmn-js

事件监听

BpmnModeler 实例对象身上有个 on 方法,可以监听事件。所有事件可以通过 new BpmnModeler().get('eventBus')._listeners 查看。

selection.changed:选中/取消选中元素时触发。

工作流引擎

  1. Activiti
  2. Camunda
  3. Flowable

封装组件

Vue 文件

vue
<script setup>
import BPMN from "./components/BPMN.vue";
</script>

<template>
    <div class="app">
        <BPMN />
    </div>
</template>

<style scoped>
.app {
    width: 100vw;
    height: 100vh;
}
</style>
vue
<script setup>
    import {onMounted, ref, useTemplateRef, watch} from "vue";
    import BpmnModeler from 'bpmn-js/lib/Modeler';
    import 'bpmn-js/dist/assets/bpmn-js.css';
    import 'bpmn-js/dist/assets/diagram-js.css';
    import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css';
    import bpmnXML from '../assets/default.bpmn?raw';

    let modeler = null;
    let currentNode = null;
    let loading = ref(false);
    let tabIndex = ref('flow');     // Tab 索引
    let flowForm = ref(null);       // 流程配置
    let nodeForm = ref(null);       // 节点配置
    const container = useTemplateRef('container');

    // 导出 XML
    async function importXML() {
        loading.value = true;
        let res = await modeler.saveXML({format: true});
        console.log('res', res.xml);
        loading.value = false;
    }

    // 清空
    async function clear() {
        loading.value = true;
        await modeler.importXML(bpmnXML);
        let processBusinessObject = modeler.get('canvas').getRootElement().businessObject;
        flowForm.value = {
            id: processBusinessObject.get('id'),
            name: processBusinessObject.get('name'),
            zdy: processBusinessObject.get('zdy'),
        }
        loading.value = false;
    }

    // 监听流程表单
    watch(flowForm, newVal => {
        if (newVal) {
            modeler.get('modeling').updateProperties(modeler.get('canvas').getRootElement(), {
                name: newVal.name,
                zdy: newVal.zdy
            })
        }
    }, {deep: true})

    // 监听节点表单
    watch(nodeForm, (newVal, oldVal) => {
        if (newVal && oldVal) {
            modeler.get('modeling').updateProperties(currentNode, {
                name: newVal.name,
                zdy: newVal.zdy
            });
        }
    }, {deep: true})

    onMounted(async () => {
        loading.value = true;
        modeler = new BpmnModeler({
            container: container.value,
            additionalModules: [{
                translate: ['value', (template, replacements) => {
                    replacements = replacements || {};

                    // 中文字典
                    const translations = {
                        'Search in diagram': '在图表中搜索',
                        'Activate hand tool': '小手工具',
                        'Activate lasso tool': '套索工具',
                        'Activate create/remove space tool': '激活创建/移除空间工具',
                        'Activate global connect tool': '箭头',
                        'Create start event': '开始节点',
                        'Create intermediate/boundary event': '中间件',
                        'Create end event': '结束节点',
                        'Create gateway': '创建网关',
                        'Create task': '创建任务',
                        'Create data object reference': '创建数据对象引用',
                        'Create data store reference': '创建数据存储引用',
                        'Create expanded sub-process': '创建子流程',
                        'Create pool/participant': '创建池/参与者',
                        'Create group': '创建群组',
                    };

                    template = translations[template] || template;

                    return template.replace(/{([^}]+)}/g, function(_, key) {
                        return replacements[key] || '{' + key + '}';
                    });
                }]
            }]
        });
        await modeler.importXML(bpmnXML);

        // 在 processBusinessObject 中可获取流程属性
        let processBusinessObject = modeler.get('canvas').getRootElement().businessObject;
        flowForm.value = {
            id: processBusinessObject.get('id'),
            name: processBusinessObject.get('name'),
            zdy: processBusinessObject.get('zdy'),
        }

        // 选择任务节点时触发
        modeler.on('selection.changed', e => {
            if (e.newSelection.length === 1 && e.newSelection[0].type === 'bpmn:Task') {
                currentNode = e.newSelection[0];

                // 通过 currentNode.businessObject 读取 bpmn 节点属性,自定义属性在 currentNode.businessObject.$attrs 里。
                nodeForm.value = {
                    id: currentNode.businessObject.id,
                    name: currentNode.businessObject.name,
                    zdy: currentNode.businessObject.$attrs.zdy,
                }
                tabIndex.value = 'node';
            } else {
                currentNode = null;
                nodeForm.value = null;
                tabIndex.value = 'flow';
            }
        })

        // 画布默认居中
        modeler.get('canvas').zoom('fit-viewport', 'center');

        // 隐藏右下角 BPMN.IO 的 Logo
        document.querySelector('.bjs-powered-by').style.display = 'none';
        loading.value = false;
    })
</script>

<template>
    <div class="bpmn" v-loading="loading">
        <div ref="container" />
        <div class="form">
            <div class="btns">
                <el-button type="primary" icon="Download" plain @click="importXML">导出 XML</el-button>
                <el-button type="danger" icon="Delete" plain @click="clear">清空</el-button>
            </div>

            <el-tabs v-model="tabIndex">
                <el-tab-pane label="流程配置" name="flow">
                    <el-form ref="flowFormRef" :model="flowForm" label-width="auto" v-if="flowForm">
                        <el-form-item label="流程 ID">
                            <el-input v-model="flowForm.id" disabled />
                        </el-form-item>
                        <el-form-item label="流程名称">
                            <el-input v-model="flowForm.name" />
                        </el-form-item>
                        <el-form-item label="自定义配置">
                            <el-input v-model="flowForm.zdy" />
                        </el-form-item>
                    </el-form>
                </el-tab-pane>
                <el-tab-pane label="节点配置" name="node">
                    <el-form ref="nodeFormRef" :model="nodeForm" label-width="auto" v-if="nodeForm">
                        <el-form-item label="节点 ID">
                            <el-input v-model="nodeForm.id" disabled />
                        </el-form-item>
                        <el-form-item label="节点名称">
                            <el-input v-model="nodeForm.name" />
                        </el-form-item>
                        <el-form-item label="自定义配置">
                            <el-input v-model="nodeForm.zdy" />
                        </el-form-item>
                    </el-form>
                </el-tab-pane>
            </el-tabs>
        </div>
    </div>
</template>

<style scoped>
    .bpmn {
        height: 100%;
        display: grid;
        grid-template-columns: 1fr 400px;
        gap: 10px;

        > div {
            height: 100%;
        }

        .form {
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            padding: 10px;
            box-sizing: border-box;

            .btns {
                display: flex;
                justify-content: flex-end;
            }
        }
    }
</style>

其他文件

js
import {createApp} from 'vue';
import ElementPlus from 'element-plus';
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import 'element-plus/dist/index.css';
import './style.css';
import App from './App.vue';

const app = createApp(App);
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    app.component(key, component)
}

app.use(ElementPlus);
app.mount('#app');
css
* {
    margin: 0;
    padding: 0;
}
xml
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
    xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
    xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
    xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
    id="Definitions_0prl7x2"
    targetNamespace="http://bpmn.io/schema/bpmn"
    exporter="bpmn-js (https://demo.bpmn.io)"
    exporterVersion="18.9.0"
>
    <bpmn:process id="Process_03vftld" name="默认流程" zdy="流程自定义属性" isExecutable="false">
        <bpmn:startEvent id="StartEvent_19wzb6o" name="开始">
            <bpmn:outgoing>Flow_1kaoirg</bpmn:outgoing>
        </bpmn:startEvent>
        <bpmn:task id="Activity_1j6wqk8" name="操作节点1" zdy="节点自定义属性">
            <bpmn:incoming>Flow_1kaoirg</bpmn:incoming>
        </bpmn:task>
        <bpmn:sequenceFlow id="Flow_1kaoirg" sourceRef="StartEvent_19wzb6o" targetRef="Activity_1j6wqk8"/>
    </bpmn:process>
    <bpmndi:BPMNDiagram id="BPMNDiagram_1">
        <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_03vftld">
            <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_19wzb6o">
                <dc:Bounds x="152" y="102" width="36" height="36"/>
            </bpmndi:BPMNShape>
            <bpmndi:BPMNShape id="Activity_1j6wqk8_di" bpmnElement="Activity_1j6wqk8">
                <dc:Bounds x="280" y="80" width="100" height="80"/>
            </bpmndi:BPMNShape>
            <bpmndi:BPMNEdge id="Flow_1kaoirg_di" bpmnElement="Flow_1kaoirg">
                <di:waypoint x="188" y="120"/>
                <di:waypoint x="280" y="120"/>
            </bpmndi:BPMNEdge>
        </bpmndi:BPMNPlane>
    </bpmndi:BPMNDiagram>
</bpmn:definitions>
json
{
    "name": "vue3",
    "private": true,
    "version": "0.0.0",
    "type": "module",
    "scripts": {
        "dev": "vite",
        "build": "vite build",
        "preview": "vite preview"
    },
    "dependencies": {
        "@element-plus/icons-vue": "^2.3.2",
        "bpmn-js": "^18.9.1",
        "element-plus": "^2.11.9",
        "vue": "^3.5.13"
    },
    "devDependencies": {
        "@vitejs/plugin-vue": "^5.2.1",
        "vite": "^6.2.0"
    }
}

基于 MIT 许可发布