ruoyi集成dmn规则引擎
环境说明
基于RuoYi-Vue2q前端如何集成DMN组件
版本号:3.9.0
更多关于ruoyi集成工作流,请访问若依工作流
集成步骤
- 安装依赖
npm install dmn-js dmn-js-properties-panel --save
npm install --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>