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>

最终页面展示

相关推荐
袋鱼不重2 小时前
AI入门知识点:什么是 AIGC、多模态、RAG、Function Call、Agent、MCP?
前端·aigc·ai编程
NuLL2 小时前
空值检测工具函数-统一规范且允许自定义配置的空值检测方案
前端
栀秋6663 小时前
“无重复字符的最长子串”:从O(n²)哈希优化到滑动窗口封神,再到DP降维打击!
前端·javascript·算法
鹿鹿鹿鹿isNotDefined3 小时前
Antd5.x 在 Next.js14.x 项目中,初次渲染样式丢失
前端·react.js·next.js
梨子同志3 小时前
Node.js 工具模块详解
前端
谷歌开发者3 小时前
Web 开发指向标|AI 辅助功能在性能面板中的使用与功能
前端·人工智能
OpenTiny社区3 小时前
TinyEngine2.9版本发布:更智能,更灵活,更开放!
前端·vue.js·低代码
被考核重击3 小时前
浏览器原理
前端·笔记·学习
网络研究院3 小时前
Firefox 146 为 Windows 用户引入了加密本地备份功能
前端·windows·firefox