ruoyi集成dmn规则引擎

环境说明

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

版本号:3.9.0

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

集成步骤

  • 安装依赖
shell 复制代码
npm install dmn-js dmn-js-properties-panel --save
npm install --save dmn-moddle
  • vue.config.js增加dmn.js配置, 在transpileDependencies,alias 进行设置
shell 复制代码
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'],
  • 前端页面编码
vue 复制代码
<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>

最终页面展示

相关推荐
代码匠心16 小时前
AI 自动编程:一句话设计高颜值博客
前端·ai·ai编程·claude
_AaronWong17 小时前
Electron 实现仿豆包划词取词功能:从 AI 生成到落地踩坑记
前端·javascript·vue.js
cxxcode17 小时前
I/O 多路复用:从浏览器到 Linux 内核
前端
用户54330814419417 小时前
AI 时代,前端逆向的门槛已经低到离谱 — 以 Upwork 为例
前端
JarvanMo17 小时前
Flutter 版本的 material_ui 已经上架 pub.dev 啦!快来抢先体验吧。
前端
恋猫de小郭18 小时前
AI 可以让 WIFI 实现监控室内人体位置和姿态,无需摄像头?
前端·人工智能·ai编程
哀木18 小时前
给自己整一个 claude code,解锁编程新姿势
前端
程序员鱼皮18 小时前
GitHub 关注突破 2w,我总结了 10 个涨星涨粉技巧!
前端·后端·github
UrbanJazzerati18 小时前
Vue3 父子组件通信完全指南
前端·面试
是一碗螺丝粉18 小时前
5分钟上手LangChain.js:用DeepSeek给你的App加上AI能力
前端·人工智能·langchain