ruoyi集成camunda-前端篇

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打开前端代码,在控制台执行下面的命令
js 复制代码
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 内部现代语法如可选链):
js 复制代码
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 解析不完善的问题)

js 复制代码
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')   
}}
  • 构建测试页面 需要您配置好菜单
vue 复制代码
<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

相关推荐
Apifox42 分钟前
如何通过抓包工具快速生成 Apifox 接口文档?
前端·后端·测试
没事多睡觉66643 分钟前
JavaScript 中 this 指向教程
开发语言·前端·javascript
苏打水com44 分钟前
浏览器与HTTP核心考点全解析(字节高频)
前端·http
Aerelin1 小时前
scrapy的介绍与使用
前端·爬虫·python·scrapy·js
BD_Marathon1 小时前
【JavaWeb】前端三大件——HTML简介
前端·html
asdfg12589631 小时前
replace(/,/g, ‘‘);/\B(?=(\d{3})+(?!\d))/;千分位分隔
开发语言·前端·javascript
irises1 小时前
从零实现2D绘图引擎:6.动画系统的实现
前端·数据可视化
_Jyann_1 小时前
uniapp两种方式实现自定义tabbar
前端·javascript·uni-app
一 乐1 小时前
数码商城系统|电子|基于SprinBoot+vue的数码商城系统(源码+数据库+文档)
java·前端·javascript·数据库·vue.js·springboot