RuoYi-Vue 是一个 Java EE 企业级快速开发平台,基于经典技术组合(Spring Boot、Spring Security、MyBatis、Jwt、Vue),内置模块如:部门管理、角色用户、菜单及按钮授权、数据权限、系统参数、日志管理、通知公告、代码生成等。在线定时任务配置,支持集群,支持多数据源,支持分布式事务等。
本文将讲解ruoyi分离版的前端如何集成camunda在线设计器,实现流程的建模。
我们也有网站提供一站式解决方案, 请直接跳转若依工作流

本文主要聚焦在RuoYi-Vue2如何集成camunda 实现bpmn在线建模。
环境要求:
- JDK >= 1.8
- MySQL >= 5.7
- Maven >= 3.0
- Node >= 23( node版本低将导致bpmn-js相关依赖安装失败!!!)
- Redis >= 3
安装步骤
- 通过vscode打开前端代码,在控制台执行下面的命令
npm install \ bpmn-js@18.6.2 \ bpmn-js-properties-panel@2.0.0 \ camunda-bpmn-moddle@7.0.1 \ bpmn-moddle@7.0.2 \ @camunda/feel-builtins \ lezer-feel --save --registry=https://registry.npmmirror.comnpm install --save @bpmn-io/feel-lint @bpmn-io/lezer-feel feelers --registry=https://registry.npmmirror.comnpm install --save feelin --registry=https://registry.npmmirror.com// 建议不要直接使用 cnpm 安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题npm install --registry=https://registry.npmmirror.com
- 编辑
vue.config.js
将包加入 Babel 转译(解决 node_modules 内部现代语法如可选链):
transpileDependencies: [ 'quill', 'bpmn-js', 'diagram-js','bpmn-js-properties-panel','@bpmn-io/properties-panel','@bpmn-io/feel-editor','@bpmn-io/feel-lint', '@bpmn-io/lezer-feel', 'feelers', 'lezer-feel'],
增加 alias(解决 Webpack 4 对 package.json exports/ESM 解析不完善的问题)
resolve: { alias: {
'@': resolve('src'), // webpack4 不识别 package.json exports,手动指向入口
'lezer-feel$': resolve('node_modules/lezer-feel/dist/index.js'),
'@camunda/feel-builtins$':
resolve('node_modules/@camunda/feel-builtins/dist/index.js'), // 将 FEEL 相关库固定到
'feelers$': resolve('node_modules/feelers/dist/index.js'),
'feelin$': resolve('node_modules/feelin/dist/index.cjs'),
'@bpmn-io/feel-lint$': resolve('node_modules/@bpmn-io/feel-lint/dist/index.js'),
'@bpmn-io/lezer-feel$': resolve('node_modules/@bpmn-io/lezer-feel/dist/index.js')
}}
- 构建测试页面
需要您配置好菜单
<template>
<div class="bpmn-modeler-page">
<div class="toolbar">
<el-button size="middle" @click="handleOpen">导入</el-button>
<el-button size="middle" @click="downloadXML">导出XML</el-button>
<el-button size="middle" @click="downloadSVG">导出SVG</el-button>
<el-button type="primary" size="middle" @click="validateDiagram">流程检查</el-button>
<el-button type="success" size="middle" @click="deployDiagram">部署</el-button>
<input ref="fileInputRef" type="file" accept=".bpmn,.xml" style="display:none" @change="onFileChange" />
</div>
<div class="modeler-wrap">
<div class="canvas" ref="canvasRef"></div>
<div class="properties-panel" ref="propertiesRef"></div>
</div>
</div>
</template>
<script setup>
import { onMounted, onBeforeUnmount, ref } from 'vue'
import { ElMessage } from 'element-plus'
import BpmnModeler from 'bpmn-js/lib/Modeler'
import camundaBpmnModdle from 'camunda-bpmn-moddle/resources/camunda.json'
import { BpmnPropertiesPanelModule, BpmnPropertiesProviderModule, CamundaPlatformPropertiesProviderModule } from 'bpmn-js-properties-panel'
import 'bpmn-js-properties-panel/dist/assets/properties-panel.css'
import 'bpmn-js/dist/assets/diagram-js.css'
import 'bpmn-js/dist/assets/bpmn-js.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css'
const canvasRef = ref(null)
const propertiesRef = ref(null)
const fileInputRef = ref(null)
let modeler = null
function generateProcessId() {
const rand = Math.random().toString(36).slice(2, 8)
return `Process_${rand}`
}
const processId = ref(generateProcessId())
function buildDefaultXml(pid) {
return `<?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" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="Definitions_1" targetNamespace="http://bpmn.io/schema/bpmn">
<bpmn:process id="${pid}" isExecutable="true">
<bpmn:startEvent id="StartEvent_1" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="${pid}">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="180" y="180" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>`
}
function initModeler() {
if (modeler) return
modeler = new BpmnModeler({
container: canvasRef.value,
propertiesPanel: {
parent: propertiesRef.value
},
additionalModules: [
BpmnPropertiesPanelModule,
BpmnPropertiesProviderModule,
CamundaPlatformPropertiesProviderModule
],
moddleExtensions: {
camunda: camundaBpmnModdle
},
keyboard: {
bindTo: document
}
})
}
async function createNewDiagram() {
try {
await modeler.importXML(buildDefaultXml(processId.value))
const canvas = modeler.get('canvas')
canvas.zoom('fit-viewport')
// 自动选中开始事件以显示属性面板
const elementRegistry = modeler.get('elementRegistry')
const startEvent = elementRegistry.get('StartEvent_1')
if (startEvent) {
const selection = modeler.get('selection')
selection.select(startEvent)
}
} catch (error) {
console.error('创建新图表失败:', error)
}
}
function handleNew() {
createNewDiagram()
}
function handleOpen() {
fileInputRef.value && fileInputRef.value.click()
}
function onFileChange(e) {
const file = e.target.files && e.target.files[0]
if (!file) return
const reader = new FileReader()
reader.onload = async () => {
try {
await modeler.importXML(reader.result)
modeler.get('canvas').zoom('fit-viewport')
} catch (err) {
console.error('导入失败', err)
} finally {
e.target.value = ''
}
}
reader.readAsText(file)
}
async function downloadXML() {
try {
const { xml } = await modeler.saveXML({ format: true })
const blob = new Blob([xml], { type: 'application/xml' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'diagram.bpmn'
a.click()
URL.revokeObjectURL(url)
} catch (err) {
console.error('导出XML失败', err)
}
}
async function downloadSVG() {
try {
const { svg } = await modeler.saveSVG()
const blob = new Blob([svg], { type: 'image/svg+xml' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'diagram.svg'
a.click()
URL.revokeObjectURL(url)
} catch (err) {
console.error('导出SVG失败', err)
}
}
function validateDiagram() {
try {
const elementRegistry = modeler.get('elementRegistry')
const processes = elementRegistry.filter(e => e.type === 'bpmn:Process')
if (!processes.length) {
ElMessage.error('未找到流程定义 (bpmn:Process)')
return
}
const startEvents = elementRegistry.filter(e => e.type === 'bpmn:StartEvent')
if (!startEvents.length) {
ElMessage.error('流程缺少开始事件')
return
}
ElMessage.success('流程检查通过')
} catch (e) {
console.error(e)
ElMessage.error('流程检查失败')
}
}
async function deployDiagram() {
try {
const { xml } = await modeler.saveXML({ format: true })
const blob = new Blob([xml], { type: 'application/xml' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${processId.value}.bpmn`
a.click()
URL.revokeObjectURL(url)
ElMessage.success('已打包流程XML(请接入后端部署接口)')
} catch (e) {
console.error(e)
ElMessage.error('部署打包失败')
}
}
onMounted(async () => {
try {
initModeler()
await createNewDiagram()
} catch (error) {
console.error('初始化模型器失败:', error)
}
})
onBeforeUnmount(() => {
if (modeler) {
modeler.destroy()
modeler = null
}
})
</script>
<style scoped>
.bpmn-modeler-page {
display: flex;
flex-direction: column;
height: 100%;
}
.toolbar {
padding: 8px;
border-bottom: 1px solid var(--el-border-color);
}
.modeler-wrap {
display: flex;
flex: 1;
min-height: 0;
}
.canvas {
flex: 1;
height: calc(100vh - 120px);
}
.properties-panel {
width: 360px;
border-left: 1px solid var(--el-border-color);
height: calc(100vh - 120px);
overflow: auto;
}
/* bpmn-js core styles (containers) */
:deep(.djs-container) {
width: 100%;
height: 100%;
}
/* 属性面板样式调整 */
:deep(.bio-properties-panel) {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
:deep(.bio-properties-panel .bio-properties-panel-group-header) {
background: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
}
:deep(.bio-properties-panel .bio-properties-panel-entry) {
border-bottom: 1px solid #f0f0f0;
}
</style>
- 最终结果

如果您想在自己的ruoyi项目集成工作流, 我们提供一站式解决方案,ruoyiflow
2306

被折叠的 条评论
为什么被折叠?



