news 2026/2/12 14:58:24

ruoyi-vue2前端集成DMN规则引擎

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ruoyi-vue2前端集成DMN规则引擎

环境说明

基于RuoYi-Vue2q前端如何集成DMN组件

版本号:3.9.0

更多关于ruoyi集成工作流,请访问若依工作流

集成步骤

  • 安装依赖
npminstalldmn-js dmn-js-properties-panel --savenpminstall--save dmn-moddle
  • vue.config.js增加dmn.js配置, 在transpileDependencies,alias 进行设置
lias:{'@':resolve('src'),'lezer-feel$':resolve('node_modules/lezer-feel/dist/index.js'),'@camunda/feel-builtins$':resolve('node_modules/@camunda/feel-builtins/dist/index.js'),'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'), // dmn-moddle 使用 ES 模块,webpack4 需要指向 CJS 版本'dmn-moddle$':resolve('node_modules/dmn-moddle/dist/index.cjs')}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', //以下是dmn-js需要的配置,主要是因为dmn-js 使用了 ES6+ 语法,但 webpack 未转译 node_modules 中的这些文件'lezer-feel','dmn-js','dmn-js-properties-panel','dmn-js-boxed-expression','dmn-js-decision-table','dmn-js-literal-expression','dmn-js-shared','dmn-moddle'],
  • 前端页面编码
<template> <el-container class="dmn-modeler-container"> <!-- 头部操作区域 --> <el-header class="dmn-header"> <div class="header-content"> <div class="header-title"> <h3>DMN 决策表建模器</h3> </div> <div class="header-actions"> <el-button-group> <el-button icon="el-icon-folder-opened" @click="openFile">导入</el-button> <el-button icon="el-icon-download" @click="downloadDiagram">导出</el-button> <el-button icon="el-icon-document" type="primary" @click="saveDiagram">部署</el-button> </el-button-group> </div> </div> </el-header> <!-- 主要内容区域 --> <el-main class="dmn-main"> <div class="dmn-content"> <!-- DMN 画布区域 --> <div class="canvas-container"> <div id="canvas" class="dmn-canvas" v-loading="initializing"></div> </div> </div> </el-main> <!-- 文件输入 --> <input ref="fileInput" type="file" accept=".dmn,.xml" style="display: none" @change="handleFileImport" /> </el-container> </template> <script> import DmnModeler from 'dmn-js/lib/Modeler' import FileSaver from 'file-saver' import { deployDmnTable } from '@/api/camunda/dmn' // 样式引入 // 基础样式 import 'dmn-js/dist/assets/diagram-js.css' // DMN 字体样式 import 'dmn-js/dist/assets/dmn-font/css/dmn.css' // 决策表相关样式(确保决策表正确显示) import 'dmn-js/dist/assets/dmn-js-shared.css' import 'dmn-js/dist/assets/dmn-js-decision-table.css' import 'dmn-js/dist/assets/dmn-js-decision-table-controls.css' // DRD (Decision Requirements Diagram) 视图样式 import 'dmn-js/dist/assets/dmn-js-drd.css' export default { name: 'CamundaDmnModeler', data() { return { dmnModeler: null, canUndo: false, canRedo: false, isInitialized: false, // 标记是否初始化成功 initializing: false, // 初始化或导入中的 loading 状态 initPromise: null // 记录初始化 Promise,便于后续等待 } }, mounted() { this.$nextTick(() => { this.initModeler() }) }, beforeDestroy() { if (this.dmnModeler) { this.dmnModeler.destroy() this.dmnModeler = null } this.initPromise = null }, methods: { // 生成随机决策表ID generateDecisionId() { const randomNum = Math.floor(Math.random() * 10000) return `Decision_${randomNum}` }, initModeler() { if (this.initializing && this.initPromise) { return this.initPromise } try { // 如果已有实例,先销毁重新创建,避免残留状态 if (this.dmnModeler) { try { this.dmnModeler.destroy() } catch (destroyErr) { console.warn('销毁旧的 DMN Modeler 失败:', destroyErr) } } this.dmnModeler = new DmnModeler({ container: '#canvas' }) this.initializing = true this.isInitialized = false // 加载空白决策表 - 使用标准的 DMN 1.3 格式 // 根据 dmn-moddle 11.0.0,使用正确的命名空间 const decisionId = this.generateDecisionId() const decisionTableId = 'DecisionTable_' + Date.now() const diagramXML = `<?xml version="1.0" encoding="UTF-8"?> <definitions xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/" xmlns:dmndi="http://www.omg.org/spec/DMN/20180521/DMNDI" xmlns:di="http://www.omg.org/spec/DMN/20180521/DI/" xmlns:dc="http://www.omg.org/spec/DMN/20180521/DC/" id="Definitions_1" name="决策表" namespace="http://camunda.org/schema/1.0/dmn"> <decision id="${decisionId}" name="决策表"> <decisionTable id="${decisionTableId}" hitPolicy="UNIQUE"> <input id="Input_1" label="输入"> <inputExpression id="InputExpression_1" typeRef="string"> <text></text> </inputExpression> </input> <output id="Output_1" label="输出" typeRef="string" /> </decisionTable> </decision> <dmndi:DMNDI> <dmndi:DMNDiagram id="DMNDiagram_1"> <dmndi:DMNShape id="DMNShape_${decisionId}" dmnElementRef="${decisionId}"> <dc:Bounds x="100" y="100" width="300" height="200" /> </dmndi:DMNShape> </dmndi:DMNDiagram> </dmndi:DMNDI> </definitions>` // 使用箭头函数确保 this 上下文正确 const initTask = this.dmnModeler.importXML(diagramXML) this.initPromise = initTask initTask.then(() => { // 只有在 importXML 成功后才标记为已初始化 this.isInitialized = true this.initializing = false this.$message.success('决策表初始化成功') // 确保 dmnModeler 已完全初始化后再访问服务 if (this.dmnModeler && typeof this.dmnModeler.get === 'function') { // 等待 DOM 更新 this.$nextTick(() => { // 监听撤销重做状态 const eventBus = this.dmnModeler.get('eventBus') if (eventBus) { eventBus.on('commandStack.changed', () => { if (this.dmnModeler && typeof this.dmnModeler.get === 'function') { const commandStack = this.dmnModeler.get('commandStack') if (commandStack) { this.canUndo = commandStack.canUndo() this.canRedo = commandStack.canRedo() } } }) } }) } }).catch(err => { console.error('初始化失败:', err) console.error('XML 内容:', diagramXML) this.isInitialized = false this.initializing = false this.$message.error('决策表初始化失败: ' + (err.message || '未知错误')) // 如果初始化失败,清空 dmnModeler,避免使用不完整的状态 if (this.dmnModeler) { try { this.dmnModeler.destroy() } catch (e) { console.warn('销毁失败的 modeler:', e) } this.dmnModeler = null } throw err }).finally(() => { // 保持 initPromise 只代表最近一次初始化 if (this.initPromise === initTask) { this.initPromise = null } }) return initTask } catch (err) { console.error('创建 DMN Modeler 失败:', err) this.$message.error('创建决策表建模器失败: ' + (err.message || '未知错误')) this.initializing = false this.isInitialized = false this.initPromise = null throw err } }, async ensureModelerReady() { debugger if (this.isInitialized && this.dmnModeler && typeof this.dmnModeler.get === 'function') { return true } if (!this.initializing || !this.initPromise) { try { await this.initModeler() } catch (err) { console.error('重新初始化决策表建模器失败:', err) return false } } if (this.initPromise) { try { await this.initPromise } catch (err) { console.error('等待决策表建模器初始化失败:', err) return false } } return this.isInitialized && this.dmnModeler && typeof this.dmnModeler.get === 'function' }, // 确保XML包含必要的命名空间 ensureDmnNamespace(xml) { // 检查是否包含正确的 DMN 1.3 命名空间 // MODEL 命名空间应该是 https://www.omg.org/spec/DMN/20191111/MODEL/ if (xml.indexOf('xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/"') === -1 && xml.indexOf('xmlns:dmn="https://www.omg.org/spec/DMN/20191111/MODEL/"') === -1) { // 如果缺少默认命名空间,尝试添加 if (xml.indexOf('<definitions') !== -1) { // 替换 definitions 标签,添加默认命名空间 xml = xml.replace( /<definitions([^>]*)>/, '<definitions$1 xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/" xmlns:dmndi="http://www.omg.org/spec/DMN/20180521/DMNDI" xmlns:di="http://www.omg.org/spec/DMN/20180521/DI/" xmlns:dc="http://www.omg.org/spec/DMN/20180521/DC/">' ) } else if (xml.indexOf('<dmn:definitions') !== -1) { // 如果使用 dmn: 前缀,也添加命名空间 xml = xml.replace( /<dmn:definitions([^>]*)>/, '<dmn:definitions$1 xmlns:dmn="https://www.omg.org/spec/DMN/20191111/MODEL/" xmlns:dmndi="http://www.omg.org/spec/DMN/20180521/DMNDI" xmlns:di="http://www.omg.org/spec/DMN/20180521/DI/" xmlns:dc="http://www.omg.org/spec/DMN/20180521/DC/">' ) } } return xml }, // 从 XML 中提取第一个 decision 的 name 属性 extractDecisionName(xml) { if (!xml || typeof xml !== 'string') { return null } try { if (typeof window !== 'undefined' && window.DOMParser) { const parser = new DOMParser() const doc = parser.parseFromString(xml, 'text/xml') const parserError = doc.getElementsByTagName('parsererror') if (parserError && parserError.length) { console.warn('DOMParser 解析 DMN XML 出错,退回正则解析') } else { // 先尝试不带命名空间的 decision let decisionEl = doc.getElementsByTagName('decision')[0] if (!decisionEl) { // 再尝试带命名空间的 decision decisionEl = doc.getElementsByTagNameNS('https://www.omg.org/spec/DMN/20191111/MODEL/', 'decision')[0] } if (decisionEl) { const name = decisionEl.getAttribute('name') if (name) { return name } } } } } catch (err) { console.warn('DOMParser 提取决策名称失败:', err) } // 正则后备方案,兼容单引号或双引号 const match = xml.match(/<\s*(?:dmn:)?decision\b[^>]*\bname=['"]([^'"]+)['"]/i) if (match && match[1]) { return match[1] } return null }, async saveDiagram() { try { // const ready = await this.ensureModelerReady() // if (!ready) { // this.$message.error('决策表建模器未初始化,请稍后再试') // return // } const modeler = this.dmnModeler // if (!modeler || typeof modeler.get !== 'function') { // this.$message.error('决策表建模器不可用,请刷新页面后重试') // return // } const { xml } = await modeler.saveXML({ format: true, preamble: true }) // 确保XML包含必要的命名空间 const processedXml = this.ensureDmnNamespace(xml) // 获取决策表名称:优先读取 XML 中 decision 的 name let decisionName = this.extractDecisionName(processedXml) if (!decisionName) { try { const elementRegistry = modeler.get('elementRegistry') if (elementRegistry) { // 尝试从决策表中获取名称 const decisions = elementRegistry.filter(el => el.type === 'dmn:Decision') if (decisions.length > 0) { const decision = decisions[0] const bo = decision.businessObject || decision decisionName = bo.name || bo.id || decisionName } } } catch (e) { console.warn('从 elementRegistry 获取决策表名称失败:', e) } } if (!decisionName) { decisionName = 'decision_' + Date.now() } // 准备部署参数 const deployData = { decisionName: decisionName, dmnXml: processedXml, tenantId: '', description: '决策表部署' } // 调用部署API this.$message.info('正在部署决策表...') const response = await deployDmnTable(deployData) this.$message.success(`决策表部署成功!决策名称: ${decisionName}`) console.log('Deployment response:', response) // 跳转到决策表列表页面 this.$router.push('/dmn/list') } catch (err) { console.error('Deployment error:', err) const errorMessage = err.response?.data?.message || err.message || '部署失败' this.$message.error('部署失败: ' + errorMessage) } }, async downloadDiagram() { try { // const ready = await this.ensureModelerReady() // if (!ready) { // this.$message.error('决策表建模器未初始化,请稍后再试') // return // } const modeler = this.dmnModeler // if (!modeler || typeof modeler.get !== 'function') { // this.$message.error('决策表建模器不可用,请刷新页面后重试') // return // } const { xml } = await modeler.saveXML({ format: true, preamble: true }) // 确保XML包含必要的命名空间 const processedXml = this.ensureDmnNamespace(xml) const blob = new Blob([processedXml], { type: 'application/xml' }) FileSaver.saveAs(blob, 'decision-table.dmn') } catch (err) { this.$message.error('导出失败: ' + (err.message || '未知错误')) } }, openFile() { this.$refs.fileInput.click() }, async handleFileImport(event) { const file = event.target.files[0] if (!file) return // const ready = await this.ensureModelerReady() // if (!ready) { // this.$message.error('决策表建模器初始化失败,请刷新页面后重试') // return // } const reader = new FileReader() reader.onload = (e) => { try { const xml = e.target.result this.initializing = true const modeler = this.dmnModeler // if (!modeler || typeof modeler.get !== 'function') { // this.initializing = false // this.$message.error('决策表建模器不可用,请刷新页面后重试') // return // } modeler.importXML(xml).then(() => { this.isInitialized = true this.initializing = false this.$message.success('文件导入成功') }).catch(error => { console.error('文件导入失败:', error) this.isInitialized = false this.initializing = false this.$message.error('文件导入失败: ' + (error.message || '未知错误')) }) } catch (error) { console.error('文件读取失败:', error) this.initializing = false this.$message.error('文件读取失败: ' + (error.message || '未知错误')) } } reader.readAsText(file) // 清空文件输入 event.target.value = '' } } } </script> <style scoped> .dmn-modeler-container { width: 100%; height: 100vh; min-width: 900px; display: flex; flex-direction: column; } /* 头部样式 */ .dmn-header { background-color: #f5f7fa; border-bottom: 1px solid #e4e7ed; padding: 0 20px; height: 60px !important; display: flex; align-items: center; } .header-content { width: 100%; display: flex; justify-content: space-between; align-items: center; } .header-title h3 { margin: 0; color: #303133; font-size: 18px; font-weight: 500; } .header-actions { display: flex; gap: 15px; flex-wrap: wrap; } .header-actions .el-button-group { margin-right: 0; } .header-actions .el-button-group .el-button { margin-right: 0; } /* 主内容区域样式 */ .dmn-main { padding: 0; height: calc(100vh - 60px); overflow: hidden; } .dmn-content { display: flex; height: 100%; width: 100%; } /* 画布容器样式 */ .canvas-container { flex: 1; position: relative; display: flex; flex-direction: column; min-width: 0; /* 允许 flex 子元素缩小 */ } .dmn-canvas { width: 100%; height: 100%; border: 1px solid #dcdfe6; background-color: #fff; } </style>

最终页面展示

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/11 2:39:33

刚出生就被围剿,豆包手机动了谁的蛋糕?

豆包手机作为一款创新的AI手机&#xff0c;刚刚进入市场就受到了来自各方的“围剿”与强烈反应。这款手机不仅将人工智能助手深度集成到操作系统底层&#xff0c;还在用户体验上提出了个性化、情感智能化的新概念&#xff0c;吸引了大量关注。然而&#xff0c;豆包手机的突如其…

作者头像 李华
网站建设 2026/2/6 5:45:14

QtWebEngine 自动重启方案

公众号&#xff1a;cpp手艺人 QtWebEngine 自动重启方案 在实际项目中不可避免的会遇到QWebengine崩溃和假死的问题。 在无法避免的情况下&#xff0c;我们一种可靠的机制能够重启。 由于QtWebEngine 使用多进程架构&#xff0c;渲染进程由 QWebEngineProcess.exe 负责。当渲染…

作者头像 李华
网站建设 2026/2/11 22:47:08

【dz-966】基于STM32的小区车库防涝系统设计

摘要 随着极端天气频发&#xff0c;小区车库涝灾风险显著增加&#xff0c;严重威胁车辆安全与居民财产。传统车库防涝多依赖人工巡查和手动操作挡杆、水泵&#xff0c;存在响应滞后、预警不及时等问题&#xff0c;难以应对突发暴雨引发的积水险情。​ 基于 STM32F103C8T6 单片…

作者头像 李华
网站建设 2026/2/3 15:41:43

回归单体架构到底是不是技术倒退

前言 软件技术发展这么多年,我们经历了单体,再到SOA,再到微服务的架构转变,这些变化的实践发起者都是用户规模庞大的大型企业,引来行业无数中小公司的效仿。可近些年出现了这样一个现象。那些已经迁移到微服务的公司,逐渐在试着回归单体架构。这不仅是中小公司的选择,像…

作者头像 李华