瀏覽代碼

bpmn版本更新及样式优化

lph 1 年之前
父節點
當前提交
e0ad867d03
共有 100 個文件被更改,包括 6891 次插入816 次删除
  1. 18 7
      ruoyi-ui/package.json
  2. 19 0
      ruoyi-ui/src/config/preset-configuration/editor.config.js
  3. 18 0
      ruoyi-ui/src/config/preset-configuration/enumsOption.js
  4. 17 1
      ruoyi-ui/src/main.js
  5. 20 15
      ruoyi-ui/src/router/index.js
  6. 23 1
      ruoyi-ui/src/store/getters.js
  7. 3 1
      ruoyi-ui/src/store/index.js
  8. 70 0
      ruoyi-ui/src/store/modules/bpmn.js
  9. 150 0
      ruoyi-ui/src/utils/bpmn/EventEmitter.js
  10. 100 0
      ruoyi-ui/src/utils/bpmn/Logger.js
  11. 20 0
      ruoyi-ui/src/utils/bpmn/files.js
  12. 9 0
      ruoyi-ui/src/utils/bpmn/printCatch.js
  13. 23 0
      ruoyi-ui/src/utils/bpmn/resetPopover.js
  14. 42 0
      ruoyi-ui/src/utils/bpmn/tool.js
  15. 14 0
      ruoyi-ui/src/utils/bpmn/uuid.js
  16. 35 0
      ruoyi-ui/src/utils/bpmn/xml.js
  17. 0 22
      ruoyi-ui/src/views/system/bpmn/components/properties-panel-extension/descriptors/authority.json
  18. 0 92
      ruoyi-ui/src/views/system/bpmn/components/properties-panel-extension/provider/authority/AuthorityPropertiesProvider.js
  19. 0 6
      ruoyi-ui/src/views/system/bpmn/components/properties-panel-extension/provider/authority/index.js
  20. 0 16
      ruoyi-ui/src/views/system/bpmn/components/properties-panel-extension/provider/authority/parts/TitleProps.js
  21. 0 240
      ruoyi-ui/src/views/system/bpmn/index.vue
  22. 0 20
      ruoyi-ui/src/views/system/bpmn/mock/customTranslate.js
  23. 0 90
      ruoyi-ui/src/views/system/bpmn/mock/minimap.css
  24. 0 243
      ruoyi-ui/src/views/system/bpmn/mock/translationsGerman.js
  25. 0 62
      ruoyi-ui/src/views/system/bpmn/mock/xmlStr.js
  26. 104 0
      ruoyi-ui/src/views/system/bpmnPro/components/ContextMenu/ContextMenu.vue
  27. 33 0
      ruoyi-ui/src/views/system/bpmnPro/components/ContextMenu/contextMenuActions.js
  28. 61 0
      ruoyi-ui/src/views/system/bpmnPro/components/Designer/index.vue
  29. 75 0
      ruoyi-ui/src/views/system/bpmnPro/components/Designer/initModeler.js
  30. 152 0
      ruoyi-ui/src/views/system/bpmnPro/components/Designer/moduleAndExtensions.js
  31. 70 0
      ruoyi-ui/src/views/system/bpmnPro/components/Panel/components/ElementAsyncContinuations.vue
  32. 137 0
      ruoyi-ui/src/views/system/bpmnPro/components/Panel/components/ElementConditional.vue
  33. 47 0
      ruoyi-ui/src/views/system/bpmnPro/components/Panel/components/ElementDocumentations.vue
  34. 193 0
      ruoyi-ui/src/views/system/bpmnPro/components/Panel/components/ElementExecutionListeners.vue
  35. 84 0
      ruoyi-ui/src/views/system/bpmnPro/components/Panel/components/ElementExtensionField.vue
  36. 93 0
      ruoyi-ui/src/views/system/bpmnPro/components/Panel/components/ElementExtensionProperties.vue
  37. 87 0
      ruoyi-ui/src/views/system/bpmnPro/components/Panel/components/ElementGenerations.vue
  38. 68 0
      ruoyi-ui/src/views/system/bpmnPro/components/Panel/components/ElementJobExecution.vue
  39. 43 0
      ruoyi-ui/src/views/system/bpmnPro/components/Panel/components/ElementStartInitiator.vue
  40. 14 0
      ruoyi-ui/src/views/system/bpmnPro/components/Panel/components/SubChild/BpmnScript.vue
  41. 148 0
      ruoyi-ui/src/views/system/bpmnPro/components/Panel/index.vue
  42. 205 0
      ruoyi-ui/src/views/system/bpmnPro/components/Settings/index.vue
  43. 27 0
      ruoyi-ui/src/views/system/bpmnPro/components/Toolbar/index.vue
  44. 66 0
      ruoyi-ui/src/views/system/bpmnPro/components/Toolbar/tools/Aligns.vue
  45. 46 0
      ruoyi-ui/src/views/system/bpmnPro/components/Toolbar/tools/Commands.vue
  46. 61 0
      ruoyi-ui/src/views/system/bpmnPro/components/Toolbar/tools/Exports.vue
  47. 162 0
      ruoyi-ui/src/views/system/bpmnPro/components/Toolbar/tools/Externals.vue
  48. 37 0
      ruoyi-ui/src/views/system/bpmnPro/components/Toolbar/tools/Import.vue
  49. 64 0
      ruoyi-ui/src/views/system/bpmnPro/components/Toolbar/tools/Previews.vue
  50. 98 0
      ruoyi-ui/src/views/system/bpmnPro/components/Toolbar/tools/Scales.vue
  51. 65 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-components/EnhancementContextmenu.js
  52. 73 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/AutoPlace/CustomAutoPlace.js
  53. 8 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/AutoPlace/index.js
  54. 104 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/ContextPad/EnhancementContextPad/enhancementContextPadProvider.js
  55. 8 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/ContextPad/EnhancementContextPad/index.js
  56. 8 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/ContextPad/RewriteContextPad/index.js
  57. 78 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/ContextPad/RewriteContextPad/rewriteContextPadProvider.js
  58. 24 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/ElementFactory/CustomElementFactory.js
  59. 5 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/ElementFactory/index.js
  60. 95 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Lint/bpmnlint.js
  61. 23 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Lint/customLintRules/taskRequired.js
  62. 106 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Palette/EnhancementPalette/enhancementPaletteProvider.js
  63. 8 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Palette/EnhancementPalette/index.js
  64. 9 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Palette/RewritePalette/index.js
  65. 191 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Palette/RewritePalette/rewritePaletteProvider.js
  66. 25 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Palette/utils.js
  67. 3 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/PopupMenu/EnhancementPopupMenu/enhancementPopupMenuProvider.js
  68. 8 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/PopupMenu/EnhancementPopupMenu/index.js
  69. 8 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/PopupMenu/RewritePopupMenu/index.js
  70. 3 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/PopupMenu/RewritePopupMenu/rewritePopupMenuProvider.js
  71. 54 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Renderer/EnhancementRenderer/EnhancementRenderer.js
  72. 8 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Renderer/EnhancementRenderer/index.js
  73. 45 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Renderer/EnhancementRenderer/renderEventContent.js
  74. 2015 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Renderer/RewriteRenderer/RewriteRenderer.js
  75. 8 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Renderer/RewriteRenderer/index.js
  76. 2 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Renderer/RewriteRenderer/rewritePaths.js
  77. 59 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Renderer/utils.js
  78. 21 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Rules/CustomRules.js
  79. 6 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Rules/index.js
  80. 17 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Translate/index.js
  81. 48 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Translate/zh-cn/events.js
  82. 13 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Translate/zh-cn/gateway.js
  83. 13 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Translate/zh-cn/index.js
  84. 4 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Translate/zh-cn/lint.js
  85. 186 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Translate/zh-cn/other.js
  86. 27 0
      ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Translate/zh-cn/tasks.js
  87. 63 0
      ruoyi-ui/src/views/system/bpmnPro/components/bo-utils/asynchronousContinuationsUtil.js
  88. 188 0
      ruoyi-ui/src/views/system/bpmnPro/components/bo-utils/conditionUtil.js
  89. 45 0
      ruoyi-ui/src/views/system/bpmnPro/components/bo-utils/documentationUtil.js
  90. 114 0
      ruoyi-ui/src/views/system/bpmnPro/components/bo-utils/executionListenersUtil.js
  91. 77 0
      ruoyi-ui/src/views/system/bpmnPro/components/bo-utils/extensionPropertiesUtil.js
  92. 21 0
      ruoyi-ui/src/views/system/bpmnPro/components/bo-utils/idUtil.js
  93. 23 0
      ruoyi-ui/src/views/system/bpmnPro/components/bo-utils/initiatorUtil.js
  94. 94 0
      ruoyi-ui/src/views/system/bpmnPro/components/bo-utils/jobExecutionUtil.js
  95. 63 0
      ruoyi-ui/src/views/system/bpmnPro/components/bo-utils/nameUtil.js
  96. 27 0
      ruoyi-ui/src/views/system/bpmnPro/components/bo-utils/processUtil.js
  97. 19 0
      ruoyi-ui/src/views/system/bpmnPro/components/bo-utils/scriptUtil.js
  98. 14 0
      ruoyi-ui/src/views/system/bpmnPro/components/bpmn-icons/bpmn-empty-state.svg
  99. 3 0
      ruoyi-ui/src/views/system/bpmnPro/components/bpmn-icons/bpmn-icon-association.svg
  100. 6 0
      ruoyi-ui/src/views/system/bpmnPro/components/bpmn-icons/bpmn-icon-business-rule-task.svg

+ 18 - 7
ruoyi-ui/package.json

@@ -38,24 +38,33 @@
   "dependencies": {
     "@antv/x6": "^1.34.2",
     "@babel/polyfill": "^7.12.1",
+    "@bpmn-io/add-exporter": "^0.2.0",
+    "@bpmn-io/element-template-chooser": "^0.0.5",
+    "@bpmn-io/properties-panel": "^0.20.3",
     "@riophae/vue-treeselect": "0.4.0",
     "ant-design-vue": "^1.7.8",
     "axios": "0.24.0",
-    "bpmn-js": "^7.3.1",
-    "bpmn-js-properties-panel": "^0.37.2",
+    "bpmn-js": "^9.4.1",
+    "bpmn-js-bpmnlint": "^0.19.0",
+    "bpmn-js-connectors-extension": "^0.4.1",
+    "bpmn-js-external-label-modeling": "^1.0.3",
+    "bpmn-js-properties-panel": "^1.6.1",
+    "bpmn-js-token-simulation": "^0.27.0",
     "bpmn-moddle": "^6.0.0",
-    "camunda-bpmn-moddle": "^4.5.0",
+    "bpmnlint": "^7.8.0",
+    "camunda-bpmn-moddle": "^7.0.1",
     "circular-json": "^0.5.9",
     "clipboard": "2.0.8",
     "core-js": "3.25.3",
     "diagram-js-context-pad": "^1.0.2",
-    "diagram-js-grid-bg": "^1.0.4",
-    "diagram-js-minimap": "^2.0.4",
+    "diagram-js-grid-bg": "^1.0.3",
+    "diagram-js-minimap": "^2.1.1",
     "echarts": "5.4.0",
     "element-ui": "2.15.12",
+    "fast-xml-parser": "^4.3.2",
     "file-saver": "2.0.5",
     "fuse.js": "6.4.3",
-    "highlight.js": "9.18.5",
+    "highlight.js": "^10.5.0",
     "insert-css": "^2.0.0",
     "js-beautify": "1.13.0",
     "js-cookie": "3.0.1",
@@ -63,6 +72,7 @@
     "k-form-design": "^3.8.18",
     "less": "^2.7.3",
     "less-loader": "^4.1.0",
+    "lucide-vue": "^0.89.0",
     "nprogress": "0.2.0",
     "quill": "1.3.7",
     "screenfull": "5.0.2",
@@ -80,7 +90,8 @@
     "vuedraggable": "2.24.3",
     "vuex": "3.6.0",
     "vuex-persistedstate": "^4.1.0",
-    "webpack": "^4.46.0"
+    "webpack": "^4.46.0",
+    "xml2js": "^0.6.2"
   },
   "devDependencies": {
     "@vue/cli-plugin-babel": "4.4.6",

+ 19 - 0
ruoyi-ui/src/config/preset-configuration/editor.config.js

@@ -0,0 +1,19 @@
+export const defaultSettings = {
+  processId: `Process_${new Date().getTime()}`,
+  processName: `业务流程`,
+  processEngine: "camunda",
+  paletteMode: "enhancement",
+  penalMode: "custom",
+  contextPadMode: "enhancement",
+  rendererMode: "rewrite",
+  bg: "grid-image",
+  toolbar: true,
+  useMinimap: true,
+  useLint: true,
+  useMock: true,
+  contextmenu: true,
+  customContextmenu: true,
+  otherModule: true,
+  templateChooser: true,
+  customTheme: {}
+};

+ 18 - 0
ruoyi-ui/src/config/preset-configuration/enumsOption.js

@@ -0,0 +1,18 @@
+export const scriptTypeOptions = [
+  { label: "外链脚本( External Resource )", value: "external" },
+  { label: "内联脚本( Inline Script )", value: "inline" },
+  { label: "无( None )", value: "none" }
+];
+
+export const listenerTypeOptions = [
+  { label: "Java Class", value: "class" },
+  { label: "Expression", value: "expression" },
+  { label: "DelegateExpression", value: "delegateExpression" },
+  { label: "Script", value: "script" }
+];
+
+export const listenerEventTypeOptions = [
+  { label: "Start", value: "start" },
+  { label: "End", value: "end" },
+  { label: "Take", value: "take" }
+];

+ 17 - 1
ruoyi-ui/src/main.js

@@ -12,11 +12,27 @@ import 'bpmn-js/dist/assets/diagram-js.css' // 左边工具栏以及编辑节点
 import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css'
 import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css'
 import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css'
-import 'bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css'
+// import 'bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css'
 // 左边工具栏以及编辑节点的样式
 // import 'bpmn-js-properties-panel/dist/assets/properties-panel.css'
 // bpmn样式结束
 
+// bpmnPro start
+import { vuePlugin } from "@packages/highlight";
+import "highlight.js/styles/atom-one-dark-reasonable.css";
+Vue.use(vuePlugin);
+
+import Common from "@packages/common";
+Vue.use(Common);
+
+import ResetPopover from "@utils/resetPopover";
+Vue.directive("r-popover", ResetPopover);
+
+import "@packages/theme/index.scss";
+
+// bpmnPro end
+
+
 import '@/assets/styles/index.scss' // global css
 import '@/assets/styles/ruoyi.scss' // ruoyi css
 import App from './App'

+ 20 - 15
ruoyi-ui/src/router/index.js

@@ -72,23 +72,28 @@ export const constantRoutes = [
       }
     ]
   },
+  // {
+  //   path: '/bpmn',
+  //   component: Layout,
+  //   hidden: true,
+  //   name: 'bpmn',
+  //   meta: {
+  //     title: "流程图",
+  //     icon: "form",
+  //     noCache: false,
+  //     link: null
+  //   },
+  //   children: [
+  //     {
+  //       path: 'index',
+  //       component: () => import('@/views/system/bpmn/index.vue'),
+  //     }
+  //   ]
+  // },
   {
-    path: '/bpmn',
-    component: Layout,
+    path: '/bpmnPro',
+    component: () => import('@/views/system/bpmnPro'),
     hidden: true,
-    name: 'bpmn',
-    meta: {
-      title: "流程图",
-      icon: "form",
-      noCache: false,
-      link: null
-    },
-    children: [
-      {
-        path: 'index',
-        component: () => import('@/views/system/bpmn/index.vue'),
-      }
-    ]
   },
   {
     path: '/login',

+ 23 - 1
ruoyi-ui/src/store/getters.js

@@ -15,6 +15,28 @@ const getters = {
   topbarRouters: state => state.permission.topbarRouters,
   defaultRoutes: state => state.permission.defaultRoutes,
   sidebarRouters: state => state.permission.sidebarRouters,
-  addRoutes: state => state.permission.addRoutes
+  addRoutes: state => state.permission.addRoutes,
+  // bpmn getters
+  //  editor
+  getEditor: (state) => state.bpmn.editor,
+  getProcessDef: (state) => ({
+    processName: state.bpmn.editor.processName,
+    processId: state.bpmn.editor.processId
+  }),
+  getProcessEngine: (state) => state.bpmn.editor.processEngine,
+  getEditorConfig: (state) => {
+    return Object.keys(state.bpmn.editor).reduce((config, key) => {
+      if (!["processName", "processId", "processEngine"].includes(key)) {
+        config[key] = state.bpmn.editor[key];
+      }
+      return config;
+    }, {});
+  },
+
+  // modeler
+  getModeler: (state) => state.bpmn.bpmn._modeler,
+  getModeling: (state) => (state.bpmn.bpmn._modeler ? state.bpmn.bpmn._modeler.get("modeling") : undefined),
+  getActive: (state) => state.bpmn.bpmn.activeElement
+
 }
 export default getters

+ 3 - 1
ruoyi-ui/src/store/index.js

@@ -8,6 +8,7 @@ import permission from './modules/permission'
 import settings from './modules/settings'
 import getters from './getters'
 import flow from './modules/flow'
+import bpmn from './modules/bpmn'
 import createPersistedstate from 'vuex-persistedstate'
 Vue.use(Vuex)
 const PERSIST_PATHS = ['tagsView.visitedViews'];
@@ -19,7 +20,8 @@ const store = new Vuex.Store({
     tagsView,
     permission,
     settings,
-    flow
+    flow,
+    bpmn
   },
   getters,
   plugins: [

+ 70 - 0
ruoyi-ui/src/store/modules/bpmn.js

@@ -0,0 +1,70 @@
+
+import { defaultSettings } from "@/config/preset-configuration/editor.config";
+import { unObserver } from "@/utils/bpmn/tool";
+
+const state = {
+  editor: { ...defaultSettings },
+  bpmn: {}
+}
+
+// const getters = {
+//   //  editor
+//   getEditor: (state) => state.editor,
+//   getProcessDef: (state) => ({
+//     processName: state.editor.processName,
+//     processId: state.editor.processId
+//   }),
+//   getProcessEngine: (state) => state.editor.processEngine,
+//   getEditorConfig: (state) => {
+//     return Object.keys(state.editor).reduce((config, key) => {
+//       if (!["processName", "processId", "processEngine"].includes(key)) {
+//         config[key] = state.editor[key];
+//       }
+//       return config;
+//     }, {});
+//   },
+
+//   // modeler
+//   getModeler: (state) => state.bpmn._modeler,
+//   getModeling: (state) => (state.bpmn._modeler ? state.bpmn._modeler.get("modeling") : undefined),
+//   getActive: (state) => state.bpmn.activeElement
+// }
+const mutations = {
+  // editor
+  setConfiguration(state, conf) {
+    state.editor = { ...state.editor, ...conf };
+  },
+
+  clearBpmnState(state) {
+    state.bpmn = {};
+  },
+  /**
+   * @param state
+   * @param modeler { object }
+   */
+  setModeler(state, modeler) {
+    state.bpmn._modeler = unObserver(modeler);
+  },
+  /**
+   * @param state
+   * @param key { string }
+   * @param module { object }
+   */
+  // setModules(state, { key, module }) {
+  //   // state.bpmn[`_${key}`] = Object.freeze(module);
+  // },
+  /**
+   * @param state
+   * @param id { string }
+   * @param element { object }
+   */
+  setElement(state, { element, id }) {
+    state.bpmn.activeElement = { element: unObserver(element), id };
+  }
+}
+
+export default {
+  namespaced: false,
+  state,
+  mutations
+}

+ 150 - 0
ruoyi-ui/src/utils/bpmn/EventEmitter.js

@@ -0,0 +1,150 @@
+import { getRawType, notNull } from "./tool";
+
+const isArray = (obj) => getRawType(obj) === "array";
+const isNullOrUndefined = (obj) => !notNull(obj);
+
+class EventEmitter {
+  static _events = {};
+
+  constructor() {}
+
+  static _addListener(type, fn, context, once) {
+    if (typeof fn !== "function") {
+      throw new TypeError("fn must be a function");
+    }
+
+    fn.context = context;
+    fn.once = !!once;
+
+    const event = EventEmitter._events[type];
+    // only one, let `this._events[type]` to be a function
+    if (isNullOrUndefined(event)) {
+      EventEmitter._events[type] = fn;
+    } else if (typeof event === "function") {
+      // already has one function, `this._events[type]` must be a function before
+      EventEmitter._events[type] = [event, fn];
+    } else if (isArray(event)) {
+      // already has more than one function, just push
+      EventEmitter._events[type].push(fn);
+    }
+
+    return EventEmitter;
+  }
+
+  static addListener(type, fn, context) {
+    return EventEmitter._addListener(type, fn, context);
+  }
+
+  static on(type, fn, context) {
+    return EventEmitter.addListener(type, fn, context);
+  }
+
+  static once(type, fn, context) {
+    return EventEmitter._addListener(type, fn, context, true);
+  }
+
+  static emit(type, ...rest) {
+    if (isNullOrUndefined(type)) {
+      throw new Error("emit must receive at lease one argument");
+    }
+
+    const event = EventEmitter._events[type];
+
+    if (isNullOrUndefined(event)) return false;
+
+    if (typeof event === "function") {
+      event.call(event.context || null, ...rest);
+      if (event.once) {
+        EventEmitter.removeListener(type, event);
+      }
+    } else if (isArray(event)) {
+      event.map((e) => {
+        e.call(e.context || null, ...rest);
+        if (e.once) {
+          EventEmitter.removeListener(type, e);
+        }
+      });
+    }
+
+    return true;
+  }
+
+  static removeListener(type, fn) {
+    if (isNullOrUndefined(EventEmitter._events)) return EventEmitter;
+
+    // if type is undefined or null, nothing to do, just return this
+    if (isNullOrUndefined(type)) return EventEmitter;
+
+    if (typeof fn !== "function") {
+      throw new Error("fn must be a function");
+    }
+
+    const events = EventEmitter._events[type];
+
+    if (typeof events === "function") {
+      events === fn && delete EventEmitter._events[type];
+    } else {
+      const findIndex = events.findIndex((e) => e === fn);
+
+      if (findIndex === -1) return EventEmitter;
+
+      // match the first one, shift faster than splice
+      if (findIndex === 0) {
+        events.shift();
+      } else {
+        events.splice(findIndex, 1);
+      }
+
+      // just left one listener, change Array to Function
+      if (events.length === 1) {
+        // @ts-ignore
+        EventEmitter._events[type] = events[0];
+      }
+    }
+
+    return EventEmitter;
+  }
+
+  static removeAllListeners(type) {
+    if (isNullOrUndefined(EventEmitter._events)) return EventEmitter;
+
+    // if not provide type, remove all
+    if (isNullOrUndefined(type)) EventEmitter._events = Object.create(null);
+
+    const events = EventEmitter._events[type];
+    if (!isNullOrUndefined(events)) {
+      // check if `type` is the last one
+      if (Object.keys(EventEmitter._events).length === 1) {
+        EventEmitter._events = Object.create(null);
+      } else {
+        delete EventEmitter._events[type];
+      }
+    }
+
+    return EventEmitter;
+  }
+
+  static listeners(type) {
+    if (isNullOrUndefined(EventEmitter._events)) return [];
+
+    const events = EventEmitter._events[type];
+    // use `map` because we need to return a new array
+    return isNullOrUndefined(events) ? [] : typeof events === "function" ? [events] : events.map((o) => o);
+  }
+
+  static listenerCount(type) {
+    if (isNullOrUndefined(EventEmitter._events)) return 0;
+
+    const events = EventEmitter._events[type];
+
+    return isNullOrUndefined(events) ? 0 : typeof events === "function" ? 1 : events.length;
+  }
+
+  static eventNames() {
+    if (isNullOrUndefined(EventEmitter._events)) return [];
+
+    return Object.keys(EventEmitter._events);
+  }
+}
+
+export default EventEmitter;

+ 100 - 0
ruoyi-ui/src/utils/bpmn/Logger.js

@@ -0,0 +1,100 @@
+import { getRawType } from "./tool";
+
+export class Logger {
+  constructor() {}
+
+  static types = ["primary", "success", "warn", "error", "info"];
+  static typeColor(type) {
+    switch (type) {
+      case "primary":
+        return "#2d8cf0";
+      case "success":
+        return "#19be6b";
+      case "info":
+        return "#909399";
+      case "warn":
+        return "#ff9900";
+      case "error":
+        return "#f03f14";
+      default:
+        return "#35495E";
+    }
+  }
+
+  static rawType(val) {
+    return Logger.printBack(getRawType(val));
+  }
+
+  static isArray(val) {
+    return getRawType(val) === "array";
+  }
+
+  static print(text, type = "default", back = false) {
+    if (typeof text === "object") {
+      // 如果是對象則調用打印對象方式
+      Logger.isArray(text) ? console.table(text) : console.dir(text);
+      return;
+    }
+    if (back) {
+      // 如果是打印帶背景圖的
+      console.log(
+        `%c ${text} `,
+        `background:${Logger.typeColor(type)}; padding: 2px; border-radius: 4px; color: #fff;`
+      );
+    } else {
+      console.log(
+        `%c ${text} `,
+        `border: 1px solid ${Logger.typeColor(type)};
+                 padding: 2px; border-radius: 4px;
+                 color: ${Logger.typeColor(type)};`
+      );
+    }
+  }
+
+  static printBack(text, type = "primary") {
+    return Logger.print(text, type, true);
+  }
+
+  static pretty(type = "primary", title, text) {
+    if (typeof text === "object") {
+      console.group("Console Group", title);
+      console.log(
+        `%c ${title}`,
+        `background:${Logger.typeColor(type)};border:1px solid ${Logger.typeColor(type)};
+                 padding: 1px; border-radius: 4px; color: #fff;`
+      );
+      Logger.isArray(text) ? console.table(text) : console.dir(text);
+      console.groupEnd();
+      return;
+    }
+    console.log(
+      `%c ${title} %c ${text} %c`,
+      `background:${Logger.typeColor(type)};border:1px solid ${Logger.typeColor(type)};
+             padding: 1px; border-radius: 4px 0 0 4px; color: #fff;`,
+      `border:1px solid ${Logger.typeColor(type)};
+             padding: 1px; border-radius: 0 4px 4px 0; color: ${Logger.typeColor(type)};`,
+      "background:transparent"
+    );
+  }
+
+  static prettyPrimary(title, ...text) {
+    text.forEach((t) => Logger.pretty("primary", title, t));
+  }
+
+  static prettySuccess(title, ...text) {
+    text.forEach((t) => Logger.pretty("success", title, t));
+  }
+
+  static prettyWarn(title, ...text) {
+    text.forEach((t) => Logger.pretty("warn", title, t));
+  }
+
+  static prettyError(title, ...text) {
+    text.forEach((t) => Logger.pretty("error", title, t));
+  }
+
+  static prettyInfo(title, ...text) {
+    text.forEach((t) => Logger.pretty("info", title, t));
+  }
+}
+export default Logger;

+ 20 - 0
ruoyi-ui/src/utils/bpmn/files.js

@@ -0,0 +1,20 @@
+// 根据所需类型进行转码并返回下载地址
+export function setEncoded(type, filename, data) {
+  const encodedData = encodeURIComponent(data);
+  return {
+    filename: `${filename}.${type.toLowerCase()}`,
+    href: `data:application/${type === "svg" ? "text/xml" : "bpmn20-xml"};charset=UTF-8,${encodedData}`,
+    data: data
+  };
+}
+
+// 文件下载方法
+export function downloadFile(href, filename) {
+  if (href && filename) {
+    const a = document.createElement("a");
+    a.download = filename; //指定下载的文件名
+    a.href = href; //  URL对象
+    a.click(); // 模拟点击
+    URL.revokeObjectURL(a.href); // 释放URL 对象
+  }
+}

+ 9 - 0
ruoyi-ui/src/utils/bpmn/printCatch.js

@@ -0,0 +1,9 @@
+import Logger from "@utils/Logger";
+
+export function catchWarning(warn) {
+  Logger.prettyWarn("[Process Designer Warning]", warn);
+}
+
+export function catchError(error) {
+  Logger.prettyError("[Process Designer Error]", typeof error === "string" ? error : error.message);
+}

+ 23 - 0
ruoyi-ui/src/utils/bpmn/resetPopover.js

@@ -0,0 +1,23 @@
+const getReference = (el, binding, vnode) => {
+  const { arg, value } = binding;
+  let elRefs;
+  if (arg) {
+    elRefs = vnode.context.$refs[arg];
+    if (Array.isArray(elRefs)) {
+      elRefs[value].$refs.reference = el;
+    } else {
+      elRefs.$refs.reference = el;
+    }
+  } else {
+    vnode.context.$refs[value].$refs.reference = arg;
+  }
+};
+
+export default {
+  bind(el, binding, vnode) {
+    getReference(el, binding, vnode);
+  },
+  inserted(el, binding, vnode) {
+    getReference(el, binding, vnode);
+  }
+};

+ 42 - 0
ruoyi-ui/src/utils/bpmn/tool.js

@@ -0,0 +1,42 @@
+/* 空函数 */
+export function noop() {}
+
+/**
+ * 校验非空
+ * @param {*} val
+ * @return boolean
+ */
+export function notEmpty(val) {
+  if (!notNull(val)) {
+    return false;
+  }
+  if (getRawType(val) === "array") {
+    return val.length;
+  }
+  if (getRawType(val) === "object") {
+    return !!Object.keys(val).length;
+  }
+  return true;
+}
+
+export function notNull(val) {
+  return val !== undefined && val !== null;
+}
+
+/**
+ * 返回数据原始类型
+ * @param value
+ * @return { string } type
+ */
+export function getRawType(value) {
+  return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
+}
+
+// 取消响应式
+export function unObserver(val) {
+  if (getRawType(val) === "object" || getRawType(val) === "array") {
+    val.__v_skip = true;
+    return val;
+  }
+  return val;
+}

+ 14 - 0
ruoyi-ui/src/utils/bpmn/uuid.js

@@ -0,0 +1,14 @@
+/**
+ * 随机生成一个指定长度的 id, 默认长度为 8
+ * @param length {number}
+ * @param [chars] {string}
+ * @returns {string}
+ */
+export default function uuid(length = 8, chars) {
+  let result = "";
+  const charsString = chars || "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+  for (let i = length; i > 0; --i) {
+    result += charsString[Math.floor(Math.random() * charsString.length)];
+  }
+  return result;
+}

+ 35 - 0
ruoyi-ui/src/utils/bpmn/xml.js

@@ -0,0 +1,35 @@
+import { catchError, catchWarning } from "./printCatch";
+
+export function emptyXML(key, name) {
+  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"
+  targetNamespace="http://bpmn.io/schema/bpmn"
+  id="Definitions_0001">
+  <bpmn:process id="${key}" name="${name}" isExecutable="true" />
+  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
+    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="${key}" />
+  </bpmndi:BPMNDiagram>
+</bpmn:definitions>`;
+}
+
+export async function createNewDiagram(modeler, newXml, settings) {
+  try {
+    const timestamp = Date.now();
+    const { processId, processName } = settings || {};
+    const newId = processId ? processId : `Process_${timestamp}`;
+    const newName = processName || `业务流程_${timestamp}`;
+    const xmlString = newXml || emptyXML(newId, newName);
+    console.log(xmlString);
+    const { warnings } = await modeler.importXML(xmlString);
+    if (warnings && warnings.length) {
+      warnings.forEach(catchWarning);
+    }
+  } catch (e) {
+    catchError(e);
+  }
+}

+ 0 - 22
ruoyi-ui/src/views/system/bpmn/components/properties-panel-extension/descriptors/authority.json

@@ -1,22 +0,0 @@
-{
-  "name": "Authority",
-  "prefix": "authority",
-  "uri": "http://authority",
-  "xml": {
-    "tagAlias": "lowerCase"
-  },
-  "associations": [],
-  "types": [
-    {
-      "name": "LinDaiDaiStartEvent",
-      "extends": ["bpmn:BaseElement"],
-      "properties": [
-        {
-          "name": "title",
-          "isAttr": true,
-          "type": "String"
-        }
-      ]
-    }
-  ]
-}

+ 0 - 92
ruoyi-ui/src/views/system/bpmn/components/properties-panel-extension/provider/authority/AuthorityPropertiesProvider.js

@@ -1,92 +0,0 @@
-import inherits from 'inherits';
-
-import PropertiesActivator from 'bpmn-js-properties-panel/lib/PropertiesActivator';
-
-
-import idProps from 'bpmn-js-properties-panel/lib/provider/bpmn/parts/IdProps';
-import nameProps from 'bpmn-js-properties-panel/lib/provider/bpmn/parts/NameProps';
-import processProps from 'bpmn-js-properties-panel/lib/provider/bpmn/parts/ProcessProps';
-import linkProps from 'bpmn-js-properties-panel/lib/provider/bpmn/parts/LinkProps';
-import eventProps from 'bpmn-js-properties-panel/lib/provider/bpmn/parts/EventProps';
-import documentationProps from 'bpmn-js-properties-panel/lib/provider/bpmn/parts/DocumentationProps';
-
-import TitleProps from './parts/TitleProps';
-
-function createGeneralTabGroups(element, bpmnFactory, canvas, elementRegistry, translate) {
-
-  var generalGroup = {
-    id: 'general',
-    label: 'General',
-    entries: []
-  };
-  idProps(generalGroup, element, translate);
-  nameProps(generalGroup, element, bpmnFactory, canvas, translate);
-  processProps(generalGroup, element, translate);
-
-  var detailsGroup = {
-    id: 'details',
-    label: 'Details',
-    entries: []
-  };
-  linkProps(detailsGroup, element, translate);
-  eventProps(detailsGroup, element, bpmnFactory, elementRegistry, translate);
-
-  var documentationGroup = {
-    id: 'documentation',
-    label: 'Documentation',
-    entries: []
-  };
-
-  documentationProps(documentationGroup, element, bpmnFactory, translate);
-
-  return [
-    generalGroup,
-    detailsGroup,
-    documentationGroup
-  ];
-}
-
-function createAuthorityTabGroups(element) {
-  var editAuthorityGroup = {
-    id: 'edit-authority',
-    label: '编辑权限',
-    entries: []
-  }
-
-  // 每个属性都有自己的props方法
-  TitleProps(editAuthorityGroup, element);
-  // OtherProps1(editAuthorityGroup, element);
-  // OtherProps2(editAuthorityGroup, element);
-
-  return [
-    editAuthorityGroup
-  ];
-}
-
-export default function AuthorityPropertiesProvider(
-  eventBus, bpmnFactory, canvas,
-  elementRegistry, translate
-) {
-  PropertiesActivator.call(this, eventBus);
-
-  this.getTabs = function (element) {
-    var generalTab = {
-      id: 'general',
-      label: 'General',
-      groups: createGeneralTabGroups(element, bpmnFactory, canvas, elementRegistry, translate)
-    };
-
-    var authorityTab = {
-      id: 'authority',
-      label: '权限',
-      groups: createAuthorityTabGroups(element)
-    };
-    return [
-      generalTab,
-      authorityTab
-    ];
-  }
-}
-
-inherits(AuthorityPropertiesProvider, PropertiesActivator);
-

+ 0 - 6
ruoyi-ui/src/views/system/bpmn/components/properties-panel-extension/provider/authority/index.js

@@ -1,6 +0,0 @@
-import AuthorityPropertiesProvider from './AuthorityPropertiesProvider';
-
-export default {
-  __init__: ['propertiesProvider'],
-  propertiesProvider: ['type', AuthorityPropertiesProvider]
-};

+ 0 - 16
ruoyi-ui/src/views/system/bpmn/components/properties-panel-extension/provider/authority/parts/TitleProps.js

@@ -1,16 +0,0 @@
-// /parts/TitleProps.js
-import entryFactory from 'bpmn-js-properties-panel/lib/factory/EntryFactory';
-
-import { is } from 'bpmn-js/lib/util/ModelUtil';
-
-export default function (group, element) {
-  if (is(element, 'bpmn:StartEvent')) { // 可以在这里做类型判断
-    group.entries.push(entryFactory.textField(element, {
-      id: 'title',
-      description: '权限的标题',
-      label: '标题',
-      modelProperty: 'title'
-    }));
-  }
-
-}

+ 0 - 240
ruoyi-ui/src/views/system/bpmn/index.vue

@@ -1,240 +0,0 @@
-<template>
-  <div class="wrapper">
-    <div class="container">
-      <!-- 创建一个canvas画布 npmn-js是通过canvas实现绘图的,并设置ref让vue获取到element -->
-      <div class="bpmn-container">
-        <div class="bpmn-canvas" ref="canvas"></div>
-        <!-- 工具栏显示的地方 -->
-        <div class="bpmn-js-properties-panel" id="js-properties-panel"></div>
-      </div>
-
-      <!-- 把操作按钮写在这里面 -->
-      <div class="action">
-        <!-- 打开本地xml文件 -->
-        <el-upload action class="upload-demo" :before-upload="openBpmn">
-          <el-button icon="el-icon-folder-opened"></el-button>
-        </el-upload>
-        <!-- 重置操作 -->
-        <el-button
-          class="new"
-          icon="el-icon-circle-plus"
-          @click="newDiagram"
-        ></el-button>
-        <!-- 导出文件 -->
-        <el-button icon="el-icon-download" @click="downloadBpmn"></el-button>
-        <!-- 导出图片 -->
-        <el-button icon="el-icon-picture" @click="downloadSvg"></el-button>
-        <a hidden ref="downloadLink"></a>
-      </div>
-    </div>
-  </div>
-</template>
-<script>
-// 引入相关的依赖
-import BpmnModeler from "bpmn-js/lib/Modeler";
-import xmlStr from "./mock/xmlStr"; // 这里是直接引用了xml字符串
-// 工具栏相关
-import propertiesPanelModule from "bpmn-js-properties-panel";
-import propertiesProviderModule from "bpmn-js-properties-panel/lib/provider/camunda";
-import camundaModdleDescriptor from "camunda-bpmn-moddle/resources/camunda";
-
-// 自定义panel
-// import propertiesProviderModule from "@/views/system/bpmn/components/properties-panel-extension/provider/authority/index";
-// import authorityModdleDescriptor from "@/views/system/bpmn/components/properties-panel-extension/descriptors/authority.json";
-// 网格插件
-import GridLineModule from "diagram-js-grid-bg";
-import ContextPadModule from "diagram-js-context-pad"; //上下文优化
-// 汉化
-import customTranslate from "./mock/customTranslate";
-
-import minimapModule from "diagram-js-minimap"; //导入小地图
-import "./mock/minimap.css"; //自定义小地图样式
-
-export default {
-  name: "",
-  components: {},
-  created() {},
-  mounted() {
-    this.init();
-  },
-  data() {
-    return {
-      // bpmn建模器
-      bpmnModeler: null,
-      container: null,
-      canvas: null,
-    };
-  },
-  methods: {
-    // 初始化相关方法
-    init() {
-      // 获取到属性ref为“canvas”的dom节点
-      const canvas = this.$refs.canvas;
-      // 将汉化包装成一个模块
-      const customTranslateModule = {
-        translate: ["value", customTranslate],
-      };
-      // 建模
-      this.bpmnModeler = new BpmnModeler({
-        container: canvas,
-        // 加入工具栏支持
-        propertiesPanel: {
-          parent: "#js-properties-panel",
-        },
-        additionalModules: [
-          // 工具栏模块
-          propertiesPanelModule,
-          propertiesProviderModule,
-          // 汉化模块
-          customTranslateModule,
-          GridLineModule, //网格
-          minimapModule, //小地图
-        ],
-        moddleExtensions: {
-          camunda: camundaModdleDescriptor,
-          // authority: authorityModdleDescriptor,
-        },
-        contextPad: {
-          beauty: true,
-        },
-      });
-      this.createNewDiagram(xmlStr);
-    },
-    async createNewDiagram(xmlStr) {
-      try {
-        await this.bpmnModeler.importXML(xmlStr);
-        this.success();
-      } catch (err) {
-        this.$Message.error("打开模型出错,请确认该模型符合Bpmn2.0规范");
-      }
-    },
-    success() {
-      console.log(this.bpmnModeler);
-      console.log("创建成功!");
-    },
-    newDiagram() {
-      this.createNewDiagram(xmlStr);
-    },
-
-    // 文件操作相关方法
-    downloadBpmn() {
-      this.bpmnModeler.saveXML({ format: true }, (err, xml) => {
-        if (!err) {
-          // 获取文件名
-          const name = `${this.getFilename(xml)}.bpmn`;
-          // 将文件名以及数据交给下载方法
-          this.download({ name: name, data: xml });
-        }
-      });
-    },
-    downloadSvg() {
-      this.bpmnModeler.saveXML({ format: true }, (err, xml) => {
-        if (!err) {
-          // 获取文件名
-          const name = `${this.getFilename(xml)}.svg`;
-
-          // 从建模器画布中提取svg图形标签
-          let context = "";
-          const djsGroupAll = this.$refs.canvas.querySelectorAll(".djs-group");
-          for (let item of djsGroupAll) {
-            context += item.innerHTML;
-          }
-          // 获取svg的基本数据,长宽高
-          const viewport = this.$refs.canvas
-            .querySelector(".viewport")
-            .getBBox();
-
-          // 将标签和数据拼接成一个完整正常的svg图形
-          const svg = `
-            <svg
-              xmlns="http://www.w3.org/2000/svg"
-              xmlns:xlink="http://www.w3.org/1999/xlink"
-              width="${viewport.width}"
-              height="${viewport.height}"
-              viewBox="${viewport.x} ${viewport.y} ${viewport.width} ${viewport.height}"
-              version="1.1"
-              >
-              ${context}
-            </svg>
-          `;
-          // 将文件名以及数据交给下载方法
-          this.download({ name: name, data: svg });
-        }
-      });
-    },
-
-    openBpmn(file) {
-      const reader = new FileReader();
-      // 读取File对象中的文本信息,编码格式为UTF-8
-      reader.readAsText(file, "utf-8");
-      reader.onload = () => {
-        //读取完毕后将文本信息导入到Bpmn建模器
-        this.createNewDiagram(reader.result);
-      };
-      return false;
-    },
-
-    getFilename(xml) {
-      let start = xml.indexOf("process");
-      let filename = xml.substr(start, xml.indexOf(">"));
-      filename = filename.substr(filename.indexOf("id") + 4);
-      filename = filename.substr(0, filename.indexOf('"'));
-      return filename;
-    },
-
-    download({ name = "diagram.bpmn", data }) {
-      // 这里就获取到了之前设置的隐藏链接
-      const downloadLink = this.$refs.downloadLink;
-      // 把数据转换为URI,下载要用到的
-      const encodedData = encodeURIComponent(data);
-
-      if (data) {
-        // 将数据给到链接
-        downloadLink.href =
-          "data:application/bpmn20-xml;charset=UTF-8," + encodedData;
-        // 设置文件名
-        downloadLink.download = name;
-        // 触发点击事件开始下载
-        downloadLink.click();
-      }
-    },
-  },
-};
-</script>
-
-<style scoped lang="scss">
-.wrapper {
-  height: calc(100vh - 173px);
-}
-.container {
-  position: relative;
-  height: 100%;
-}
-.bpmn-container {
-  width: 100%;
-  height: 100%;
-  display: flex;
-}
-
-.bpmn-canvas {
-  width: calc(100% - 300px);
-  height: 100%;
-}
-
-.bpmn-js-properties-panel {
-  width: 320px;
-  height: inherit;
-  overflow-y: auto;
-}
-
-.action {
-  position: fixed;
-  top: 135px;
-  left: 50%;
-  transform: translate(-50%);
-  display: flex;
-}
-.upload-demo {
-  margin-right: 10px;
-}
-</style>

+ 0 - 20
ruoyi-ui/src/views/system/bpmn/mock/customTranslate.js

@@ -1,20 +0,0 @@
-import translations from "./translationsGerman";
-
-export default function customTranslate(template, replacements) {
-  replacements = replacements || {};
-
-  // Translate
-  template = translations[template] || template;
-
-  // Replace
-  return template.replace(/{([^}]+)}/g, function (_, key) {
-    var str = replacements[key];
-    if (
-      translations[replacements[key]] != null &&
-      translations[replacements[key]] != "undefined"
-    ) {
-      str = translations[replacements[key]];
-    }
-    return str || "{" + key + "}";
-  });
-}

+ 0 - 90
ruoyi-ui/src/views/system/bpmn/mock/minimap.css

@@ -1,90 +0,0 @@
-.djs-minimap {
-  position: absolute;
-  top: 20px;
-  /*自定义样式*/
-  right: 10px;
-  overflow: hidden;
-  background-color: rgba(255, 255, 255, 0.9);
-  border: solid 1px #ccc;
-  border-radius: 2px;
-  box-sizing: border-box;
-  user-select: none;
-  -moz-user-select: none;
-  -ms-user-select: none;
-  -webkit-user-select: none;
-}
-
-.djs-minimap:not(.open) {
-  overflow: hidden;
-}
-
-.djs-minimap .map {
-  display: none;
-}
-
-.djs-minimap.open .map {
-  display: block;
-}
-
-.djs-minimap .map {
-  width: 320px;
-  height: 180px;
-}
-
-.djs-minimap:not(.open) .toggle {
-  padding: 10px;
-  text-align: center;
-  cursor: pointer;
-}
-
-.djs-minimap .toggle:before {
-  content: attr(title);
-}
-
-.djs-minimap.open .toggle {
-  position: absolute;
-  right: 0;
-  padding: 6px;
-  z-index: 1;
-  cursor: pointer;
-}
-
-.djs-minimap .map {
-  cursor: crosshair;
-}
-
-.djs-minimap .viewport {
-  /* fill: rgba(255, 116, 0, 0.25); */
-  fill: none;
-  stroke: none;
-}
-
-.djs-minimap .viewport-dom {
-  position: absolute;
-  border: solid 2px orange;
-  border-radius: 2px;
-  box-sizing: border-box;
-  cursor: move;
-}
-
-.djs-minimap:not(.open) .viewport-dom {
-  display: none;
-}
-
-.djs-minimap.open .overlay {
-  position: absolute;
-  top: 0;
-  right: 0;
-  bottom: 0;
-  left: 0;
-  background: rgba(255, 255, 255, 0.2);
-  pointer-events: none;
-}
-
-.djs-minimap .cursor-crosshair {
-  cursor: crosshair;
-}
-
-.djs-minimap .cursor-move {
-  cursor: move;
-}

+ 0 - 243
ruoyi-ui/src/views/system/bpmn/mock/translationsGerman.js

@@ -1,243 +0,0 @@
-export default {
-  // Labels
-  'Activate the global connect tool': '激活全局连接工具',
-  'Append {type}': '追加 {type}',
-  'Append EndEvent': '追加 结束事件 ',
-  'Append Task': '追加 任务',
-  'Append Gateway': '追加 网关',
-  'Append Intermediate/Boundary Event': '追加 中间/边界 事件',
-  'Add Lane above': '在上面添加道',
-  'Divide into two Lanes': '分割成两个道',
-  'Divide into three Lanes': '分割成三个道',
-  'Add Lane below': '在下面添加道',
-  'Append compensation activity': '追加补偿活动',
-  'Change type': '修改类型',
-  'Connect using Association': '使用关联连接',
-  'Connect using Sequence/MessageFlow or Association': '使用顺序/消息流或者关联连接',
-  'Connect using DataInputAssociation': '使用数据输入关联连接',
-  'Remove': '移除',
-  'Activate the hand tool': '激活抓手工具',
-  'Activate the lasso tool': '激活套索工具',
-  'Activate the create/remove space tool': '激活创建/删除空间工具',
-  'Create expanded SubProcess': '创建扩展子过程',
-  'Create IntermediateThrowEvent/BoundaryEvent': '创建中间抛出事件/边界事件',
-  'Create Pool/Participant': '创建池/参与者',
-  'Parallel Multi Instance': '并行多重事件',
-  'Sequential Multi Instance': '时序多重事件',
-  'DataObjectReference': '数据对象参考',
-  'DataStoreReference': '数据存储参考',
-  'Loop': '循环',
-  'Ad-hoc': '即席',
-  'Create {type}': '创建 {type}',
-  'Create Task': '创建任务',
-  'Create StartEvent': '创建开始事件',
-  'Create EndEvent': '创建结束事件',
-  'Create Group': '创建组',
-  'Task': '任务',
-  'Send Task': '发送任务',
-  'Receive Task': '接收任务',
-  'User Task': '用户任务',
-  'Manual Task': '手工任务',
-  'Business Rule Task': '业务规则任务',
-  'Service Task': '服务任务',
-  'Script Task': '脚本任务',
-  'Call Activity': '调用活动',
-  'Sub Process (collapsed)': '子流程(折叠的)',
-  'Sub Process (expanded)': '子流程(展开的)',
-  'Start Event': '开始事件',
-  'StartEvent': '开始事件',
-  'Intermediate Throw Event': '中间事件',
-  'End Event': '结束事件',
-  'EndEvent': '结束事件',
-  'Create Gateway': '创建网关',
-  'GateWay': '网关',
-  'Create Intermediate/Boundary Event': '创建中间/边界事件',
-  'Message Start Event': '消息开始事件',
-  'Timer Start Event': '定时开始事件',
-  'Conditional Start Event': '条件开始事件',
-  'Signal Start Event': '信号开始事件',
-  'Error Start Event': '错误开始事件',
-  'Escalation Start Event': '升级开始事件',
-  'Compensation Start Event': '补偿开始事件',
-  'Message Start Event (non-interrupting)': '消息开始事件(非中断)',
-  'Timer Start Event (non-interrupting)': '定时开始事件(非中断)',
-  'Conditional Start Event (non-interrupting)': '条件开始事件(非中断)',
-  'Signal Start Event (non-interrupting)': '信号开始事件(非中断)',
-  'Escalation Start Event (non-interrupting)': '升级开始事件(非中断)',
-  'Message Intermediate Catch Event': '消息中间捕获事件',
-  'Message Intermediate Throw Event': '消息中间抛出事件',
-  'Timer Intermediate Catch Event': '定时中间捕获事件',
-  'Escalation Intermediate Throw Event': '升级中间抛出事件',
-  'Conditional Intermediate Catch Event': '条件中间捕获事件',
-  'Link Intermediate Catch Event': '链接中间捕获事件',
-  'Link Intermediate Throw Event': '链接中间抛出事件',
-  'Compensation Intermediate Throw Event': '补偿中间抛出事件',
-  'Signal Intermediate Catch Event': '信号中间捕获事件',
-  'Signal Intermediate Throw Event': '信号中间抛出事件',
-  'Message End Event': '消息结束事件',
-  'Escalation End Event': '定时结束事件',
-  'Error End Event': '错误结束事件',
-  'Cancel End Event': '取消结束事件',
-  'Compensation End Event': '补偿结束事件',
-  'Signal End Event': '信号结束事件',
-  'Terminate End Event': '终止结束事件',
-  'Message Boundary Event': '消息边界事件',
-  'Message Boundary Event (non-interrupting)': '消息边界事件(非中断)',
-  'Timer Boundary Event': '定时边界事件',
-  'Timer Boundary Event (non-interrupting)': '定时边界事件(非中断)',
-  'Escalation Boundary Event': '升级边界事件',
-  'Escalation Boundary Event (non-interrupting)': '升级边界事件(非中断)',
-  'Conditional Boundary Event': '条件边界事件',
-  'Conditional Boundary Event (non-interrupting)': '条件边界事件(非中断)',
-  'Error Boundary Event': '错误边界事件',
-  'Cancel Boundary Event': '取消边界事件',
-  'Signal Boundary Event': '信号边界事件',
-  'Signal Boundary Event (non-interrupting)': '信号边界事件(非中断)',
-  'Compensation Boundary Event': '补偿边界事件',
-  'Exclusive Gateway': '互斥网关',
-  'Parallel Gateway': '并行网关',
-  'Inclusive Gateway': '相容网关',
-  'Complex Gateway': '复杂网关',
-  'Event based Gateway': '事件网关',
-  'Transaction': '转运',
-  'Sub Process': '子流程',
-  'Event Sub Process': '事件子流程',
-  'Collapsed Pool': '折叠池',
-  'Expanded Pool': '展开池',
-  // Errors
-  'no parent for {element} in {parent}': '在{parent}里,{element}没有父类',
-  'no shape type specified': '没有指定的形状类型',
-  'flow elements must be children of pools/participants': '流元素必须是池/参与者的子类',
-  'out of bounds release': 'out of bounds release',
-  'more than {count} child lanes': '子道大于{count} ',
-  'element required': '元素不能为空',
-  'diagram not part of bpmn:Definitions': '流程图不符合bpmn规范',
-  'no diagram to display': '没有可展示的流程图',
-  'no process or collaboration to display': '没有可展示的流程/协作',
-  'element {element} referenced by {referenced}#{property} not yet drawn': '由{referenced}#{property}引用的{element}元素仍未绘制',
-  'already rendered {element}': '{element} 已被渲染',
-  'failed to import {element}': '导入{element}失败',
-  //属性面板的参数
-  'Id': '编号',
-  'Name': '名称',
-  'General': '常规',
-  'Details': '详情',
-  'Message Name': '消息名称',
-  'Message': '消息',
-  'Initiator': '创建者',
-  'Asynchronous Continuations': '持续异步',
-  'Asynchronous Before': '异步前',
-  'Asynchronous After': '异步后',
-  'Job Configuration': '工作配置',
-  'Exclusive': '排除',
-  'Job Priority': '工作优先级',
-  'Retry Time Cycle': '重试时间周期',
-  'Documentation': '文档',
-  'Element Documentation': '元素文档',
-  'History Configuration': '历史配置',
-  'History Time To Live': '历史的生存时间',
-  'Forms': '表单',
-  'Form Key': '表单key',
-  'Form Fields': '表单字段',
-  'Business Key': '业务key',
-  'Form Field': '表单字段',
-  'ID': '编号',
-  'Type': '类型',
-  'Label': '名称',
-  'Default Value': '默认值',
-  'Validation': '校验',
-  'Add Constraint': '添加约束',
-  'Config': '配置',
-  'Properties': '属性',
-  'Add Property': '添加属性',
-  'Value': '值',
-  'Add': '添加',
-  'Values': '值',
-  'Add Value': '添加值',
-  'Listeners': '监听器',
-  'Execution Listener': '执行监听',
-  'Event Type': '事件类型',
-  'Listener Type': '监听器类型',
-  'Java Class': 'Java类',
-  'Expression': '表达式',
-  'Must provide a value': '必须提供一个值',
-  'Delegate Expression': '代理表达式',
-  'Script': '脚本',
-  'Script Format': '脚本格式',
-  'Script Type': '脚本类型',
-  'Inline Script': '内联脚本',
-  'External Script': '外部脚本',
-  'Resource': '资源',
-  'Field Injection': '字段注入',
-  'Extensions': '扩展',
-  'Input/Output': '输入/输出',
-  'Input Parameters': '输入参数',
-  'Output Parameters': '输出参数',
-  'Parameters': '参数',
-  'Output Parameter': '输出参数',
-  'Timer Definition Type': '定时器定义类型',
-  'Timer Definition': '定时器定义',
-  'Date': '日期',
-  'Duration': '持续',
-  'Cycle': '循环',
-  'Signal': '信号',
-  'Signal Name': '信号名称',
-  'Escalation': '升级',
-  'Error': '错误',
-  'Link Name': '链接名称',
-  'Condition': '条件名称',
-  'Variable Name': '变量名称',
-  'Variable Event': '变量事件',
-  'Specify more than one variable change event as a comma separated list.': '多个变量事件以逗号隔开',
-  'Wait for Completion': '等待完成',
-  'Activity Ref': '活动参考',
-  'Version Tag': '版本标签',
-  'Executable': '可执行文件',
-  'External Task Configuration': '扩展任务配置',
-  'Task Priority': '任务优先级',
-  'External': '外部',
-  'Connector': '连接器',
-  'Must configure Connector': '必须配置连接器',
-  'Connector Id': '连接器编号',
-  'Implementation': '实现方式',
-  'Field Injections': '字段注入',
-  'Fields': '字段',
-  'Result Variable': '结果变量',
-  'Topic': '主题',
-  'Configure Connector': '配置连接器',
-  'Input Parameter': '输入参数',
-  'Assignee': '代理人',
-  'Candidate Users': '候选用户',
-  'Candidate Groups': '候选组',
-  'Due Date': '到期时间',
-  'Follow Up Date': '跟踪日期',
-  'Priority': '优先级',
-  'The follow up date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)': '跟踪日期必须符合EL表达式,如: ${someDate} ,或者一个ISO标准日期,如:2015-06-26T09:54:00',
-  'The due date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)': '跟踪日期必须符合EL表达式,如: ${someDate} ,或者一个ISO标准日期,如:2015-06-26T09:54:00',
-  'Variables': '变量',
-  'Candidate Starter Configuration': '候选开始配置',
-  'Task Listener': '任务监听器',
-  'Candidate Starter Groups': '候选开始组',
-  'Candidate Starter Users': '候选开始用户',
-  'Tasklist Configuration': '任务列表配置',
-  'Startable': '启动',
-  'Specify more than one group as a comma separated list.': '指定多个组,用逗号分隔',
-  'Specify more than one user as a comma separated list.': '指定多个用户,用逗号分隔',
-  'This maps to the process definition key.': '这会映射为流程定义的键',
-  'CallActivity Type': '调用活动类型',
-  'Condition Type': '条件类型',
-  'Create UserTask': '创建用户任务',
-  'Create CallActivity': '创建调用活动',
-  'Called Element': '调用元素',
-  'Create DataObjectReference': '创建数据对象引用',
-  'Create DataStoreReference': '创建数据存储引用',
-  'Multi Instance': '多实例',
-  'Loop Cardinality': '实例数量',
-  'Collection': '任务参与人列表',
-  'Element Variable': '元素变量',
-  'Completion Condition': '完成条件',
-  //小地图
-  'Open minimap': '打开小地图',
-  'Close minimap': '关闭小地图'
-
-};

+ 0 - 62
ruoyi-ui/src/views/system/bpmn/mock/xmlStr.js

@@ -1,62 +0,0 @@
-export default '<?xml version="1.0" encoding="UTF-8"?>\n' +
-  '<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" id="Definitions_0fppxr8" targetNamespace="http://bpmn.io/schema/bpmn">\n' +
-  '  <bpmn:process id="Process_1" isExecutable="false">\n' +
-  '    <bpmn:startEvent id="StartEvent_1" name="begin&#10;">\n' +
-  '      <bpmn:outgoing>SequenceFlow_0nrfbee</bpmn:outgoing>\n' +
-  '    </bpmn:startEvent>\n' +
-  '    <bpmn:task id="Task_0ho18x0" name="hello&#10;">\n' +
-  '      <bpmn:incoming>SequenceFlow_0nrfbee</bpmn:incoming>\n' +
-  '      <bpmn:outgoing>SequenceFlow_00ho26x</bpmn:outgoing>\n' +
-  '    </bpmn:task>\n' +
-  '    <bpmn:task id="Task_1ymuvem" name="world">\n' +
-  '      <bpmn:incoming>SequenceFlow_00ho26x</bpmn:incoming>\n' +
-  '      <bpmn:outgoing>SequenceFlow_18df8vb</bpmn:outgoing>\n' +
-  '    </bpmn:task>\n' +
-  '    <bpmn:endEvent id="EndEvent_1c0ed2n" name="end">\n' +
-  '      <bpmn:incoming>SequenceFlow_18df8vb</bpmn:incoming>\n' +
-  '    </bpmn:endEvent>\n' +
-  '    <bpmn:sequenceFlow id="SequenceFlow_0nrfbee" sourceRef="StartEvent_1" targetRef="Task_0ho18x0" />\n' +
-  '    <bpmn:sequenceFlow id="SequenceFlow_00ho26x" sourceRef="Task_0ho18x0" targetRef="Task_1ymuvem" />\n' +
-  '    <bpmn:sequenceFlow id="SequenceFlow_18df8vb" sourceRef="Task_1ymuvem" targetRef="EndEvent_1c0ed2n" />\n' +
-  '  </bpmn:process>\n' +
-  '  <bpmndi:BPMNDiagram id="BPMNDiagram_1">\n' +
-  '    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">\n' +
-  '      <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">\n' +
-  '        <dc:Bounds x="173" y="102" width="36" height="36" />\n' +
-  '        <bpmndi:BPMNLabel>\n' +
-  '          <dc:Bounds x="178" y="145" width="27" height="27" />\n' +
-  '        </bpmndi:BPMNLabel>\n' +
-  '      </bpmndi:BPMNShape>\n' +
-  '      <bpmndi:BPMNShape id="Task_0ho18x0_di" bpmnElement="Task_0ho18x0">\n' +
-  '        <dc:Bounds x="485" y="244" width="100" height="80" />\n' +
-  '      </bpmndi:BPMNShape>\n' +
-  '      <bpmndi:BPMNShape id="Task_1ymuvem_di" bpmnElement="Task_1ymuvem">\n' +
-  '        <dc:Bounds x="712" y="391" width="100" height="80" />\n' +
-  '      </bpmndi:BPMNShape>\n' +
-  '      <bpmndi:BPMNShape id="EndEvent_1c0ed2n_di" bpmnElement="EndEvent_1c0ed2n">\n' +
-  '        <dc:Bounds x="1056" y="568" width="36" height="36" />\n' +
-  '        <bpmndi:BPMNLabel>\n' +
-  '          <dc:Bounds x="1065" y="611" width="19" height="14" />\n' +
-  '        </bpmndi:BPMNLabel>\n' +
-  '      </bpmndi:BPMNShape>\n' +
-  '      <bpmndi:BPMNEdge id="SequenceFlow_0nrfbee_di" bpmnElement="SequenceFlow_0nrfbee">\n' +
-  '        <di:waypoint x="209" y="120" />\n' +
-  '        <di:waypoint x="347" y="120" />\n' +
-  '        <di:waypoint x="347" y="284" />\n' +
-  '        <di:waypoint x="485" y="284" />\n' +
-  '      </bpmndi:BPMNEdge>\n' +
-  '      <bpmndi:BPMNEdge id="SequenceFlow_00ho26x_di" bpmnElement="SequenceFlow_00ho26x">\n' +
-  '        <di:waypoint x="585" y="284" />\n' +
-  '        <di:waypoint x="649" y="284" />\n' +
-  '        <di:waypoint x="649" y="431" />\n' +
-  '        <di:waypoint x="712" y="431" />\n' +
-  '      </bpmndi:BPMNEdge>\n' +
-  '      <bpmndi:BPMNEdge id="SequenceFlow_18df8vb_di" bpmnElement="SequenceFlow_18df8vb">\n' +
-  '        <di:waypoint x="812" y="431" />\n' +
-  '        <di:waypoint x="934" y="431" />\n' +
-  '        <di:waypoint x="934" y="586" />\n' +
-  '        <di:waypoint x="1056" y="586" />\n' +
-  '      </bpmndi:BPMNEdge>\n' +
-  '    </bpmndi:BPMNPlane>\n' +
-  '  </bpmndi:BPMNDiagram>\n' +
-  '</bpmn:definitions>\n'

+ 104 - 0
ruoyi-ui/src/views/system/bpmnPro/components/ContextMenu/ContextMenu.vue

@@ -0,0 +1,104 @@
+<template>
+  <div class="bpmn-context-menu" v-if="showPopover" :style="currentPosition">
+    <div class="context-menu_header">{{ contextMenuTitle }}</div>
+    <div class="context-menu_body">
+      <div
+        v-for="item in currentReplaceOptions"
+        :key="item.actionName"
+        class="context-menu_item"
+      >
+        <i :class="`context-menu_item_icon ${item.className}`"></i>
+        <span @click="triggerAction(item, $event)">{{
+          translateCh(item.label)
+        }}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { customTranslate } from "@packages/additional-modules/Translate";
+import contextMenuActions from "@packages/ContextMenu/contextMenuActions";
+import EventEmitter from "@utils/EventEmitter";
+import { isAppendAction } from "@packages/bpmn-utils/BpmnDesignerUtils";
+import BpmnReplaceOptions from "@packages/bpmn-utils/BpmnReplaceOptions";
+import { catchError } from "@utils/printCatch";
+
+export default {
+  name: "BpmnContextMenu",
+  data() {
+    return {
+      currentReplaceOptions: [],
+      contextMenuTitle: "",
+      showPopover: false,
+      isAppend: false,
+      position: { x: 0, y: 0 },
+      currentPosition: {},
+    };
+  },
+  created() {
+    EventEmitter.on("show-contextmenu", this.initEventCallback);
+    document.body.addEventListener("click", this.closePopover);
+  },
+  beforeDestroy() {
+    EventEmitter.removeListener("show-contextmenu", this.initEventCallback);
+    document.body.removeEventListener("click", this.closePopover);
+  },
+  methods: {
+    translateCh(text) {
+      return customTranslate(text);
+    },
+    triggerAction(entry, event) {
+      try {
+        const { appendAction, replaceAction } = contextMenuActions();
+        this.isAppend
+          ? appendAction(entry.target, event)
+          : replaceAction(entry.target, this._currentElement);
+        this.showPopover = false;
+      } catch (e) {
+        catchError(e);
+      }
+    },
+    closePopover() {
+      this.showPopover = false;
+    },
+
+    async initEventCallback(event, element) {
+      console.log(event, this.$el.clientWidth);
+      this._currentElement = element || null;
+      this.isAppend = isAppendAction(element);
+      this.currentReplaceOptions = BpmnReplaceOptions(element);
+      this.contextMenuTitle = this.isAppend ? "创建元素" : "更改元素";
+      this.showPopover = true;
+      this.currentPosition = await this.resetPosition(event);
+    },
+
+    async resetPosition(event) {
+      if (!this.$el) {
+        await this.$nextTick();
+        await this.resetPosition();
+      }
+      // 页面内容区尺寸
+      const { clientWidth: pageWidth, clientHeight: pageHeight } =
+        document.body;
+      // 组件大小
+      const { clientWidth: modelWidth, clientHeight: modelHeight } = this.$el;
+      // 默认不靠边,边距 20 px
+      const padding = 20;
+      // 鼠标点击位置
+      const { clientX, clientY } = event;
+
+      // 组件位置计算
+      let left = clientX,
+        top = clientY;
+      if (modelWidth + padding + clientX >= pageWidth) {
+        left = clientX - modelWidth;
+      }
+      if (modelHeight + padding + clientY >= pageHeight) {
+        top = clientY - modelHeight;
+      }
+      return { left: (left += "px"), top: (top += "px") };
+    },
+  },
+};
+</script>

+ 33 - 0
ruoyi-ui/src/views/system/bpmnPro/components/ContextMenu/contextMenuActions.js

@@ -0,0 +1,33 @@
+import { getModeler } from "@packages/bpmn-utils/BpmnDesignerUtils";
+
+export default function () {
+  let replaceElement;
+  let elementFactory;
+  let create;
+
+  function replaceAction(target, currentElement) {
+    if (!replaceElement) {
+      replaceElement = getModeler.get("bpmnReplace").replaceElement;
+    }
+    replaceElement(currentElement, target);
+  }
+
+  function appendAction(target, event) {
+    if (!elementFactory) {
+      elementFactory = getModeler.get("elementFactory");
+    }
+    if (!create) {
+      create = getModeler.get("create");
+    }
+    const shape = elementFactory.createShape(target);
+    if (target.isExpanded != null) {
+      shape.businessObject.di.isExpanded = target.isExpanded;
+    }
+    setTimeout(() => create.start(event, shape), 30);
+  }
+
+  return {
+    replaceAction,
+    appendAction
+  };
+}

+ 61 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Designer/index.vue

@@ -0,0 +1,61 @@
+<template>
+  <div :class="['bpmn-designer', bgClass]" ref="designerRef"></div>
+</template>
+
+<script>
+import { debounce } from "min-dash";
+import { mapGetters } from "vuex";
+import { createNewDiagram } from "@utils/xml";
+import { catchError } from "@utils/printCatch";
+import moduleAndExtensions from "./moduleAndExtensions";
+import initModeler from "./initModeler";
+
+export default {
+  name: "BpmnDesigner",
+  props: {
+    xml: {
+      type: String,
+      default: undefined
+    },
+    events: {
+      type: Array,
+      default: () => []
+    }
+  },
+  computed: {
+    ...mapGetters(["getEditor", "getModeler", "getModeling"]),
+    bgClass() {
+      const bg = this.getEditor.bg;
+      if (bg === "grid-image") return "designer-with-bg";
+      if (bg === "image") return "designer-with-image";
+      return "";
+    }
+  },
+  methods: {
+    reloadProcess: debounce(async function (setting, oldSetting) {
+      const modelerModules = moduleAndExtensions(setting);
+
+      await this.$nextTick();
+      const modeler = initModeler(this.$refs.designerRef, modelerModules, this);
+      if (!oldSetting || setting.processEngine !== oldSetting.processEngine) {
+        await createNewDiagram(modeler);
+      } else {
+        await createNewDiagram(modeler, this.xml, setting);
+      }
+    }, 100)
+  },
+  watch: {
+    getEditor: {
+      immediate: true,
+      deep: true,
+      handler: async function (value, oldValue) {
+        try {
+          this.reloadProcess(value, oldValue);
+        } catch (e) {
+          catchError(e);
+        }
+      }
+    }
+  }
+};
+</script>

+ 75 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Designer/initModeler.js

@@ -0,0 +1,75 @@
+import Modeler from "bpmn-js/lib/Modeler";
+import EventEmitter from "@/utils/bpmn/EventEmitter";
+import { catchError } from "@/utils/bpmn/printCatch";
+import EnhancementContextmenu from "../additional-components/EnhancementContextmenu";
+import { addExtensionProperty } from '@packages/bo-utils/extensionPropertiesUtil'
+import { unObserver } from "@/utils/bpmn/tool";
+
+export default function (designerDom, moduleAndExtensions, context) {
+  const options = {
+    container: designerDom,
+    additionalModules: moduleAndExtensions[0] || [],
+    moddleExtensions: moduleAndExtensions[1] || {},
+    ...moduleAndExtensions[2]
+  };
+
+  // 清除旧 modeler
+  context.getModeler && context.getModeler.destroy();
+  context.$store.commit("clearBpmnState");
+
+  const modeler = new Modeler(options);
+
+  context.$store.commit("setModeler", modeler);
+  console.log(context.$store);
+
+  EventEmitter.emit("modeler-init", modeler);
+
+  context.events?.forEach((event) => {
+    modeler.on(event, function (eventObj) {
+      let eventName = event.replace(/\./g, "-");
+      let element = eventObj ? eventObj.element : null;
+      context.$emit(eventName, unObserver({ element, eventObj }));
+    });
+  });
+
+  modeler.on("commandStack.changed", async (event) => {
+    try {
+      const { xml } = await modeler.saveXML({ format: true });
+
+      context.$emit("update:xml", xml);
+      context.$emit("command-stack-changed", event);
+    } catch (error) {
+      catchError(error);
+    }
+  });
+
+  // 右键菜单
+  EnhancementContextmenu(modeler, context.getEditor);
+
+  // 功能测试部分,可删除
+  modeler.on("commandStack.elements.create.preExecute", (event) => {
+    console.log("create", event);
+    const {
+      context: { elements }
+    } = event;
+    if (elements[0] && elements[0].type === "bpmn:UserTask") {
+      addExtensionProperty(elements[0], { name: "111", value: "1231" });
+    }
+    return event;
+  });
+  modeler.on("commandStack.shape.replace.preExecute", (event) => {
+    console.log("replace", event);
+    debugger;
+    const {
+      context: { newShape, newData }
+    } = event;
+    if (newData && newData.type === "bpmn:UserTask") {
+      addExtensionProperty(newData.businessObject, { name: "111", value: "1231" });
+    }
+    return event;
+  });
+
+  console.log(modeler);
+
+  return modeler;
+}

+ 152 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Designer/moduleAndExtensions.js

@@ -0,0 +1,152 @@
+// ** 官方流程模拟 module
+import TokenSimulationModule from "bpmn-js-token-simulation";
+// camunda 官方侧边栏扩展
+import {
+  BpmnPropertiesPanelModule,
+  BpmnPropertiesProviderModule,
+  CloudElementTemplatesPropertiesProviderModule,
+  CamundaPlatformPropertiesProviderModule
+} from "bpmn-js-properties-panel";
+// 官方扩展工具 元素模板选择
+import ElementTemplateChooserModule from "@bpmn-io/element-template-chooser";
+import ConnectorsExtensionModule from "bpmn-js-connectors-extension";
+// 官方 默认点状背景
+// import Grid from "diagram-js/lib/features/grid-snapping/visuals";
+// 流程图校验部分
+import lintModule from "bpmn-js-bpmnlint";
+import { resolver, rules } from "@/views/system/bpmnPro/components/additional-modules/Lint/bpmnlint";
+// 小地图
+import minimapModule from "diagram-js-minimap";
+import gridBGModule from "diagram-js-grid-bg";
+
+// moddle 定义文件
+import activitiModdleDescriptors from "@/views/system/bpmnPro/components/moddle-extensions/activiti.json";
+import camundaModdleDescriptors from "@/views/system/bpmnPro/components/moddle-extensions/camunda.json";
+import flowableModdleDescriptors from "@/views/system/bpmnPro/components/moddle-extensions/flowable.json";
+import miyueModdleDescriptors from "@/views/system/bpmnPro/components/moddle-extensions/miyue.json";
+import zeebeModdleDescriptors from "@/views/system/bpmnPro/components/moddle-extensions/zeebe.json";
+
+// 自定义扩展部分
+import EnhancementPalette from "@/views/system/bpmnPro/components/additional-modules/Palette/EnhancementPalette";
+import RewritePalette from "@/views/system/bpmnPro/components/additional-modules/Palette/RewritePalette";
+import EnhancementContextPad from "@/views/system/bpmnPro/components/additional-modules/ContextPad/EnhancementContextPad";
+import RewriteContextPad from "@/views/system/bpmnPro/components/additional-modules/ContextPad/RewriteContextPad";
+import EnhancementRenderer from "@/views/system/bpmnPro/components/additional-modules/Renderer/EnhancementRenderer";
+import RewriteRenderer from "@/views/system/bpmnPro/components/additional-modules/Renderer/RewriteRenderer";
+import Rules from "@/views/system/bpmnPro/components/additional-modules/Rules";
+import AutoPlace from "@/views/system/bpmnPro/components/additional-modules/AutoPlace";
+import ElementFactory from "@/views/system/bpmnPro/components/additional-modules/ElementFactory";
+import translate from "@/views/system/bpmnPro/components/additional-modules/Translate";
+
+export default function (settings) {
+  const modules = []; // modules 扩展模块数组
+  let moddle = {}; // moddle 声明文件对象
+  const options = {}; // modeler 其他配置
+
+  // 配置 palette (可覆盖 paletteProvider 取消原生侧边栏)
+  settings.paletteMode === "enhancement" && modules.push(EnhancementPalette);
+  settings.paletteMode === "rewrite" && modules.push(RewritePalette);
+  settings.paletteMode === "custom" && modules.push({ paletteProvider: ["type", function () { }] });
+
+  // 配置 contextPad (可覆盖 contextPadProvider 取消原生上下文菜单)
+  settings.contextPadMode === "enhancement" && modules.push(EnhancementContextPad);
+  settings.contextPadMode === "rewrite" && modules.push(RewriteContextPad);
+
+  // 配置 自定义渲染
+  settings.rendererMode === "enhancement" && modules.push(EnhancementRenderer);
+  if (settings.rendererMode === "rewrite") {
+    modules.push(RewriteRenderer);
+    options["bpmnRenderer"] = { ...(settings.customTheme || {}), useCurve: settings.useCurve };
+  }
+
+  // 配置模板选择弹窗(会影响默认 popupmenu)
+  // if (settings.templateChooser || settings.penalMode !== "custom") {
+  //   modules.push(BpmnPropertiesPanelModule, BpmnPropertiesProviderModule, CamundaPlatformPropertiesProviderModule);
+  //   moddle = {};
+  //   if (settings.penalMode !== "custom") {
+  //     options["propertiesPanel"] = { parent: "#camunda-panel" };
+  //     moddle["camunda"] = camundaModdleDescriptors;
+  //   }
+  //   if (settings.templateChooser) {
+  //     modules.push(
+  //       CloudElementTemplatesPropertiesProviderModule,
+  //       ElementTemplateChooserModule,
+  //       ConnectorsExtensionModule
+  //     );
+  //     options["exporter"] = {
+  //       name: "element-template-chooser",
+  //       version: "0.0.1"
+  //     };
+  //     options["connectorsExtension"] = {
+  //       appendAnything: true
+  //     };
+  //   }
+  // }
+
+  // 设置 lint 校验
+  if (settings.useLint) {
+    modules.push(lintModule);
+    options["linting"] = {
+      active: true,
+      bpmnlint: {
+        config: {
+          rules: { ...rules, "task-required": "error" }
+        },
+        resolver
+      }
+    };
+  }
+
+  // 设置 lint 校验
+  if (settings.useMinimap) {
+    modules.push(minimapModule);
+    options["minimap"] = {
+      open: true
+    };
+  }
+
+  // 设置 lint 校验
+  if (settings.useMock) {
+    modules.push(TokenSimulationModule);
+  }
+
+  // 官方网点背景
+  if (settings.bg === "grid") {
+    modules.push(gridBGModule);
+  }
+
+  // 设置其他模块的启用
+  if (settings.otherModule) {
+    // 设置 自定义规则
+    modules.push(Rules);
+
+    modules.push(AutoPlace);
+
+    // 设置键盘事件绑定
+    options["keyboard"] = {
+      bindTo: document
+    };
+
+    modules.push(ElementFactory);
+    options["elementFactory"] = {
+      "bpmn:Task": { width: 120, height: 120 },
+      "bpmn:SequenceFlow": { width: 100, height: 80 }
+    };
+  }
+
+  // 配置 翻译 与 流程模拟
+  modules.push(translate);
+
+  // 设置对应的 moddle 解析配置文件 ( 避免上面已经配置了 camunda )
+  if (!Object.keys(moddle).length) {
+    if (settings.processEngine === "activiti") moddle["activiti"] = activitiModdleDescriptors;
+    if (settings.processEngine === "camunda") moddle["camunda"] = camundaModdleDescriptors;
+    if (settings.processEngine === "flowable") moddle["flowable"] = flowableModdleDescriptors;
+  }
+  // moddle["zeebe"] = zeebeModdleDescriptors;
+
+  // 设置自定义属性
+  moddle["miyue"] = miyueModdleDescriptors;
+
+  return [modules, moddle, options];
+}

+ 70 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Panel/components/ElementAsyncContinuations.vue

@@ -0,0 +1,70 @@
+<template>
+  <el-collapse-item name="element-async-continuations">
+    <template #title>
+      <collapse-title title="异步属性">
+        <lucide-icon name="Shuffle" />
+      </collapse-title>
+    </template>
+    <edit-item label="Before" :label-width="120">
+      <el-switch v-model="acBefore" @change="updateElementACBefore" />
+    </edit-item>
+    <edit-item label="After" :label-width="120">
+      <el-switch v-model="acAfter" @change="updateElementACAfter" />
+    </edit-item>
+    <edit-item v-if="showExclusive" label="Exclusive" :label-width="120">
+      <el-switch v-model="acExclusive" @change="updateElementACExclusive" />
+    </edit-item>
+  </el-collapse-item>
+</template>
+
+<script>
+import {
+  getACAfter,
+  getACBefore,
+  getACExclusive,
+  setACAfter,
+  setACBefore,
+  setACExclusive
+} from "@packages/bo-utils/asynchronousContinuationsUtil";
+import EventEmitter from "@utils/EventEmitter";
+import { getActive } from "@packages/bpmn-utils/BpmnDesignerUtils";
+
+export default {
+  name: "ElementAsyncContinuations",
+  data() {
+    return {
+      acBefore: false,
+      acAfter: false,
+      acExclusive: false
+    };
+  },
+  computed: {
+    showExclusive() {
+      return this.acBefore || this.acAfter;
+    }
+  },
+  mounted() {
+    this.reloadACStatus();
+    EventEmitter.on("element-update", this.reloadACStatus);
+  },
+  methods: {
+    reloadACStatus() {
+      this.acBefore = getACBefore(getActive());
+      this.acAfter = getACAfter(getActive());
+      this.acExclusive = getACExclusive(getActive());
+    },
+    updateElementACBefore(value) {
+      setACBefore(getActive(), value);
+      this.reloadACStatus();
+    },
+    updateElementACAfter(value) {
+      setACAfter(getActive(), value);
+      this.reloadACStatus();
+    },
+    updateElementACExclusive(value) {
+      setACExclusive(getActive(), value);
+      this.reloadACStatus();
+    }
+  }
+};
+</script>

+ 137 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Panel/components/ElementConditional.vue

@@ -0,0 +1,137 @@
+<template>
+  <el-collapse-item name="element-conditional">
+    <template #title>
+      <collapse-title title="条件设置">
+        <lucide-icon name="ArrowLeftRight" />
+      </collapse-title>
+    </template>
+    <div class="element-conditional">
+      <template v-if="varVisible">
+        <edit-item key="variableName" label="变量名称" :label-width="120">
+          <el-input v-model="variableName" maxlength="32" @change="setElementVariableName" />
+        </edit-item>
+        <edit-item v-if="varEventVisible" key="variableEvent" label="变量事件" :label-width="120">
+          <el-input v-model="variableEvents" @change="setElementVariableEvents" />
+        </edit-item>
+      </template>
+      <edit-item key="condition" label="条件类型" :label-width="120">
+        <el-select
+          v-model="conditionData.conditionType"
+          :options="conditionTypeOptions"
+          @change="setElementConditionType"
+        />
+      </edit-item>
+      <edit-item
+        v-if="conditionData.conditionType && conditionData.conditionType === 'expression'"
+        key="expression"
+        label="条件内容"
+        :label-width="120"
+      >
+        <el-input v-model="conditionData.expression" @change="setConditionExpression" />
+      </edit-item>
+      <template v-if="conditionData.conditionType && conditionData.conditionType === 'script'">
+        <edit-item key="scriptType" label="脚本类型" :label-width="120">
+          <el-select v-model="conditionData.scriptType" @change="setElementConditionScriptType">
+            <el-option v-for="{ label, value } in scriptTypeOptions" :label="label" :value="value" :key="value" />
+          </el-select>
+        </edit-item>
+        <edit-item key="scriptLanguage" label="脚本语言" :label-width="120">
+          <el-input v-model="conditionData.language" @change="setConditionScriptLanguage" />
+        </edit-item>
+        <edit-item v-show="conditionData.scriptType === 'inline'" key="scriptBody" label="脚本内容" :label-width="120">
+          <el-input v-model="conditionData.body" type="textarea" @change="setConditionScriptBody" />
+        </edit-item>
+        <edit-item
+          v-show="conditionData.scriptType === 'external'"
+          key="scriptResource"
+          label="资源地址"
+          :label-width="120"
+        >
+          <el-input v-model="conditionData.resource" @change="setConditionScriptResource" />
+        </edit-item>
+      </template>
+    </div>
+  </el-collapse-item>
+</template>
+
+<script>
+import * as CU from "@packages/bo-utils/conditionUtil";
+import EventEmitter from "@utils/EventEmitter";
+import { scriptTypeOptions } from "@packages/preset-configuration/enumsOption";
+import { getActive } from "@packages/bpmn-utils/BpmnDesignerUtils";
+
+export default {
+  name: "ElementConditional",
+  data() {
+    return {
+      varVisible: false,
+      varEventVisible: false,
+      variableName: "",
+      variableEvents: {},
+      conditionTypeOptions: [],
+      conditionData: {},
+      scriptTypeOptions: scriptTypeOptions
+    };
+  },
+
+  mounted() {
+    this.getElementVariables();
+    this.getElementConditionType();
+    this.conditionTypeOptions = CU.getConditionTypeOptions(getActive());
+    EventEmitter.on("element-update", () => {
+      this.conditionTypeOptions = CU.getConditionTypeOptions(getActive());
+      this.getElementVariables();
+      this.getElementConditionType();
+    });
+  },
+  methods: {
+    getElementVariables() {
+      this.varVisible = CU.isConditionEventDefinition(getActive());
+      this.variableName = CU.getVariableNameValue(getActive());
+      if (this.varVisible) {
+        this.varEventVisible = !CU.isExtendStartEvent(getActive());
+        this.variableEvents = CU.getVariableEventsValue(getActive());
+      }
+    },
+    getElementConditionType() {
+      this.conditionData.conditionType = CU.getConditionTypeValue(getActive());
+      this.conditionData.conditionType === "expression" && this.getConditionExpression();
+      this.conditionData.conditionType === "script" && this.getConditionScript();
+    },
+    getConditionExpression() {
+      this.conditionData.expression = CU.getConditionExpressionValue(getActive());
+    },
+    getConditionScript() {
+      this.conditionData.language = CU.getConditionScriptLanguageValue(getActive());
+      this.conditionData.scriptType = CU.getConditionScriptTypeValue(getActive());
+      this.conditionData.body = CU.getConditionScriptBodyValue(getActive());
+      this.conditionData.resource = CU.getConditionScriptResourceValue(getActive());
+    },
+
+    setElementVariableName(value) {
+      CU.setVariableNameValue(getActive(), value);
+    },
+    setElementVariableEvents(value) {
+      CU.setVariableEventsValue(getActive(), value);
+    },
+    setElementConditionType(value) {
+      CU.setConditionTypeValue(getActive(), value);
+    },
+    setConditionExpression(value) {
+      CU.setConditionExpressionValue(getActive(), value);
+    },
+    setConditionScriptLanguage(value) {
+      CU.setConditionScriptLanguageValue(getActive(), value);
+    },
+    setElementConditionScriptType(value) {
+      CU.setConditionScriptTypeValue(getActive(), value);
+    },
+    setConditionScriptBody(value) {
+      CU.setConditionScriptBodyValue(getActive(), value);
+    },
+    setConditionScriptResource(value) {
+      CU.setConditionScriptResourceValue(getActive(), value);
+    }
+  }
+};
+</script>

+ 47 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Panel/components/ElementDocumentations.vue

@@ -0,0 +1,47 @@
+<template>
+  <el-collapse-item name="element-documentations">
+    <template #title>
+      <collapse-title title="附加文档">
+        <lucide-icon name="FileText" />
+      </collapse-title>
+    </template>
+    <edit-item label="Documentation" :label-width="120">
+      <el-input v-model="elementDoc" type="textarea" @change="updateElementDoc" />
+    </edit-item>
+  </el-collapse-item>
+</template>
+
+<script>
+import { getDocumentValue, setDocumentValue } from "@packages/bo-utils/documentationUtil";
+import EventEmitter from "@utils/EventEmitter";
+import { getActive } from "@packages/bpmn-utils/BpmnDesignerUtils";
+
+export default {
+  name: "ElementDocumentations",
+  data() {
+    return {
+      elementDoc: ""
+    };
+  },
+
+  watch: {
+    getActive: {
+      immediate: true,
+      handler() {
+        this.elementDoc = getDocumentValue(getActive()) || "";
+      }
+    }
+  },
+  mounted() {
+    this.elementDoc = getDocumentValue(getActive()) || "";
+    EventEmitter.on("element-update", () => {
+      this.elementDoc = getDocumentValue(getActive()) || "";
+    });
+  },
+  methods: {
+    updateElementDoc(value) {
+      setDocumentValue(getActive(), value);
+    }
+  }
+};
+</script>

+ 193 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Panel/components/ElementExecutionListeners.vue

@@ -0,0 +1,193 @@
+<template>
+  <el-collapse-item name="element-execution-listeners">
+    <template #title>
+      <collapse-title title="执行监听">
+        <lucide-icon name="Radio" />
+      </collapse-title>
+      <number-tag :value="listeners.length" margin-left="12px" />
+    </template>
+    <div class="element-extension-listeners">
+      <el-table border :data="listeners" style="width: 100%" height="200px">
+        <el-table-column label="No" type="index" width="50" />
+        <el-table-column label="EventType" prop="event" show-overflow-tooltip />
+        <el-table-column label="ListenerType" prop="type" show-overflow-tooltip />
+        <el-table-column label="操作" width="140">
+          <template slot-scope="{ row, $index }">
+            <el-button type="text" @click="openListenerModel($index, row)">编辑</el-button>
+            <el-button type="text" @click="removeListener($index)">移除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <el-button type="primary" class="inline-large-button" icon="el-icon-plus" @click="openListenerModel(-1)">
+        添加执行监听
+      </el-button>
+    </div>
+
+    <el-dialog :visible.sync="modelVisible" title="添加执行监听器" width="640px" append-to-body destroy-on-close>
+      <el-form ref="formRef" :model="newListener" :rules="formRules" class="need-filled" aria-modal="true">
+        <el-form-item path="event" label="事件类型( Event Type )">
+          <el-select v-model="newListener.event">
+            <el-option
+              v-for="{ label, value } in listenerEventTypeOptions"
+              :label="label"
+              :value="value"
+              :key="value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item path="type" label="监听器类型( Listener Type )">
+          <el-select v-model="newListener.type" @change="updateListenerType">
+            <el-option v-for="{ label, value } in listenerTypeOptions" :label="label" :value="value" :key="value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item v-if="formItemVisible.listenerType === 'class'" path="class" label="Java类( Java Class )">
+          <el-input v-model="newListener.class" @keydown.enter.prevent />
+        </el-form-item>
+        <el-form-item
+          v-if="formItemVisible.listenerType === 'expression'"
+          path="expression"
+          label="条件表达式( Expression )"
+        >
+          <el-input v-model="newListener.expression" @keydown.enter.prevent />
+        </el-form-item>
+        <el-form-item
+          v-if="formItemVisible.listenerType === 'delegateExpression'"
+          path="delegateExpression"
+          label="代理条件表达式( Delegate Expression )"
+        >
+          <el-input v-model="newListener.delegateExpression" @keydown.enter.prevent />
+        </el-form-item>
+        <template v-if="formItemVisible.listenerType === 'script' && newListener.script">
+          <el-form-item key="scriptFormat" path="script.scriptFormat" label="脚本格式( Script Format )">
+            <el-input v-model="newListener.script.scriptFormat" @keydown.enter.prevent />
+          </el-form-item>
+          <el-form-item key="scriptType" path="script.scriptType" label="脚本类型( Script Type )">
+            <el-select v-model="newListener.script.scriptType" @change="updateScriptType">
+              <el-option v-for="{ label, value } in scriptTypeOptions" :label="label" :value="value" :key="value" />
+            </el-select>
+          </el-form-item>
+          <el-form-item
+            v-if="formItemVisible.scriptType === 'inline'"
+            key="scriptContent"
+            path="script.value"
+            label="脚本内容( Script Content )"
+          >
+            <el-input v-model="newListener.script.value" type="textarea" @keydown.enter.prevent />
+          </el-form-item>
+          <el-form-item
+            v-if="formItemVisible.scriptType === 'external'"
+            key="scriptResource"
+            path="script.resource"
+            label="外链脚本地址( Script Resource )"
+          >
+            <el-input v-model="newListener.script.resource" @keydown.enter.prevent />
+          </el-form-item>
+        </template>
+      </el-form>
+      <template #footer>
+        <el-button @click="modelVisible = false">取 消</el-button>
+        <el-button type="primary" @click="saveExecutionListener">确 认</el-button>
+      </template>
+    </el-dialog>
+  </el-collapse-item>
+</template>
+
+<script>
+import { listenerTypeOptions, scriptTypeOptions } from "@packages/preset-configuration/enumsOption";
+import {
+  addExecutionListener,
+  getDefaultEvent,
+  getExecutionListeners,
+  getExecutionListenerType,
+  getExecutionListenerTypes,
+  removeExecutionListener,
+  updateExecutionListener
+} from "@packages/bo-utils/executionListenersUtil";
+import { getScriptType } from "@packages/bo-utils/scriptUtil";
+import EventEmitter from "@utils/EventEmitter";
+import { getActive } from "@packages/bpmn-utils/BpmnDesignerUtils";
+
+export default {
+  name: "ElementExecutionListeners",
+  data() {
+    return {
+      modelVisible: false,
+      listeners: [],
+      newListener: {},
+      formRules: {
+        event: { required: true, trigger: ["blur", "change"], message: "事件类型不能为空" },
+        type: { required: true, trigger: ["blur", "change"], message: "监听器类型不能为空" }
+      },
+      formItemVisible: {
+        listenerType: "class",
+        scriptType: "none"
+      },
+      listenerEventTypeOptions: [],
+      listenerTypeOptions: listenerTypeOptions,
+      scriptTypeOptions: scriptTypeOptions
+    };
+  },
+
+  mounted() {
+    this.reloadExtensionListeners();
+    EventEmitter.on("element-update", this.reloadExtensionListeners);
+  },
+  methods: {
+    reloadExtensionListeners() {
+      this.modelVisible = false;
+      this.updateListenerType("class");
+      this.newListener = { event: getDefaultEvent(getActive()), type: "class" };
+      this.listenerEventTypeOptions = getExecutionListenerTypes(getActive());
+      this._listenersRaw = getExecutionListeners(getActive());
+      const list = this._listenersRaw.map((item) => ({
+        ...item,
+        ...(item.script
+          ? {
+              script: { ...item.script, scriptType: getScriptType(item.script) }
+            }
+          : {}),
+        type: getExecutionListenerType(item)
+      }));
+      this.listeners = JSON.parse(JSON.stringify(list));
+    },
+
+    updateListenerType(value) {
+      this.formItemVisible.listenerType = value;
+      this.newListener = {
+        ...this.newListener,
+        type: value,
+        ...(value === "script" ? { script: this.newListener.script || {} } : {})
+      };
+    },
+    updateScriptType(value) {
+      this.formItemVisible.scriptType = value;
+      this.newListener.script = {
+        scriptFormat: this.newListener.script?.scriptFormat,
+        scriptType: value
+      };
+    },
+    removeListener(index) {
+      const listener = this._listenersRaw[index];
+      removeExecutionListener(getActive(), listener);
+      this.reloadExtensionListeners();
+    },
+    async saveExecutionListener(index) {
+      await this.$refs.formRef.validate();
+      this.activeIndex === -1
+        ? addExecutionListener(getActive(), this.newListener)
+        : updateExecutionListener(getActive(), this.newListener, this._listenersRaw[this.activeIndex]);
+      this.reloadExtensionListeners();
+    },
+
+    async openListenerModel(index, listenerData) {
+      this.activeIndex = index;
+      listenerData && (this.newListener = JSON.parse(JSON.stringify(listenerData)));
+      this.updateListenerType(listenerData?.type || "class");
+      this.modelVisible = true;
+      await this.$nextTick();
+      this.$refs.formRef && this.$refs.formRef.clearValidate();
+    }
+  }
+};
+</script>

+ 84 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Panel/components/ElementExtensionField.vue

@@ -0,0 +1,84 @@
+<template>
+  <el-collapse-item name="element-extension-field">
+    <template #title>
+      <collapse-title title="扩展字段">
+        <lucide-icon name="PlayCircle" />
+      </collapse-title>
+    </template>
+    <div class="element-extension-field">
+      <edit-item label="Name">
+        <el-input v-model="field.name" />
+      </edit-item>
+      <edit-item label="Value">
+        <el-input v-model="field.string" />
+      </edit-item>
+
+      <el-button
+        type="primary"
+        class="inline-large-button"
+        @click="setElementExtensionField(-1)"
+      >
+        更新字段
+      </el-button>
+    </div>
+  </el-collapse-item>
+</template>
+
+<script>
+import EventEmitter from "@utils/EventEmitter";
+import { getActive } from "@packages/bpmn-utils/BpmnDesignerUtils";
+import { createModdleElement } from "@packages/bpmn-utils/BpmnExtensionElements";
+import { getModeler } from "@packages/bpmn-utils/BpmnDesignerUtils";
+
+export default {
+  name: "ElementExtensionField",
+  data() {
+    return {
+      field: {},
+    };
+  },
+  mounted() {
+    this.getElementExtensionField();
+    EventEmitter.on("element-update", this.getElementExtensionField);
+  },
+  methods: {
+    getElementExtensionField() {
+      this.$extensionElements =
+        getActive().businessObject.get("extensionElements");
+      this.$otherExtensionElements = [];
+      this.$extensionElements &&
+        this.$extensionElements.get("values").forEach((item) => {
+          if (item.$type !== "flowable:Field") {
+            this.$otherExtensionElements.push(item);
+          } else {
+            this.field = { ...item };
+          }
+        });
+    },
+    setElementExtensionField() {
+      const modeling = getModeler.getModeling();
+      if (!this.$extensionElements) {
+        this.$extensionElements = createModdleElement(
+          "bpmn:ExtensionElements",
+          { values: [] },
+          getActive().businessObject
+        );
+        modeling.updateModdleProperties(
+          getActive(),
+          getActive().businessObject,
+          {
+            extensionElements: this.$extensionElements,
+          }
+        );
+      }
+      const field = getModeler.getModdle().create("flowable:Field", {
+        name: this.field.name,
+        string: this.field.string,
+      });
+      modeling.updateModdleProperties(getActive(), this.$extensionElements, {
+        values: [...this.$otherExtensionElements, field],
+      });
+    },
+  },
+};
+</script>

+ 93 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Panel/components/ElementExtensionProperties.vue

@@ -0,0 +1,93 @@
+<template>
+  <el-collapse-item name="element-extension-properties">
+    <template #title>
+      <collapse-title title="扩展属性">
+        <lucide-icon name="FileCog" />
+      </collapse-title>
+      <number-tag :value="extensions.length" margin-left="12px" />
+    </template>
+    <div class="element-extension-properties">
+      <el-table border :data="extensions" style="width: 100%" height="200px">
+        <el-table-column label="No" type="index" width="50" />
+        <el-table-column label="Name" prop="name" show-overflow-tooltip />
+        <el-table-column label="Value" prop="value" show-overflow-tooltip />
+        <el-table-column label="操作" width="140">
+          <template slot-scope="{ $index }">
+            <el-button type="text" @click="removeProperty($index)">移除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <el-button type="primary" class="inline-large-button" icon="el-icon-plus" @click="openPropertyModel(-1)">
+        添加扩展属性
+      </el-button>
+    </div>
+
+    <el-dialog :visible.sync="modelVisible" title="添加扩展属性" width="640px" append-to-body destroy-on-close>
+      <el-form ref="formRef" :model="newProperty" :rules="rules" aria-modal="true">
+        <el-form-item path="name" label="属性名称( Name )">
+          <el-input v-model="newProperty.name" @keydown.enter.prevent />
+        </el-form-item>
+        <el-form-item path="value" label="属性值( Value )">
+          <el-input v-model="newProperty.value" @keydown.enter.prevent />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="modelVisible = false">取 消</el-button>
+        <el-button type="primary" @click="addProperty">确 认</el-button>
+      </template>
+    </el-dialog>
+  </el-collapse-item>
+</template>
+
+<script>
+import {
+  addExtensionProperty,
+  getExtensionProperties,
+  removeExtensionProperty
+} from "@packages/bo-utils/extensionPropertiesUtil";
+import EventEmitter from "@utils/EventEmitter";
+import { getActive } from "@packages/bpmn-utils/BpmnDesignerUtils";
+
+export default {
+  name: "ElementExtensionProperties",
+  data() {
+    return {
+      extensions: [],
+      newProperty: { name: "", value: "" },
+      rules: {
+        name: { required: true, message: "属性名称不能为空", trigger: ["blur", "change"] },
+        value: { required: true, message: "属性值不能为空", trigger: ["blur", "change"] }
+      },
+      modelVisible: false
+    };
+  },
+  mounted() {
+    this.reloadExtensionProperties();
+    EventEmitter.on("element-update", this.reloadExtensionProperties);
+  },
+  methods: {
+    async reloadExtensionProperties() {
+      this.modelVisible = false;
+      await this.$nextTick();
+      this.newProperty = { name: "", value: "" };
+      this._extensionsRaw = getExtensionProperties(getActive());
+      this.extensions = JSON.parse(JSON.stringify(this._extensionsRaw));
+    },
+    removeProperty(propIndex) {
+      removeExtensionProperty(getActive(), this._extensionsRaw[propIndex]);
+      this.reloadExtensionProperties();
+    },
+    async addProperty() {
+      await this.$refs.formRef.validate();
+      addExtensionProperty(getActive(), this.newProperty);
+      await this.reloadExtensionProperties();
+    },
+    async openPropertyModel() {
+      this.modelVisible = true;
+      await this.$nextTick();
+      this.$refs.formRef.clearValidate();
+    }
+  }
+};
+</script>

+ 87 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Panel/components/ElementGenerations.vue

@@ -0,0 +1,87 @@
+<template>
+  <el-collapse-item name="base-info">
+    <template #title>
+      <collapse-title title="常规信息">
+        <lucide-icon name="Info" />
+      </collapse-title>
+    </template>
+
+    <edit-item label="ID">
+      <el-input v-model="elementId" maxlength="32" @change="updateElementId" />
+    </edit-item>
+
+    <edit-item label="Name">
+      <el-input v-model="elementName" maxlength="20" @change="updateElementName" />
+    </edit-item>
+
+    <template v-if="isProcess">
+      <edit-item key="version" label="Version">
+        <el-input v-model="elementVersion" maxlength="20" @change="updateElementVersion" />
+      </edit-item>
+
+      <edit-item key="executable" label="Executable">
+        <el-switch v-model="elementExecutable" @change="updateElementExecutable" />
+      </edit-item>
+    </template>
+  </el-collapse-item>
+</template>
+
+<script>
+import { catchError } from "@utils/printCatch";
+import { getNameValue, setNameValue } from "@packages/bo-utils/nameUtil";
+import {
+  getProcessExecutable,
+  getProcessVersionTag,
+  setProcessExecutable,
+  setProcessVersionTag
+} from "@packages/bo-utils/processUtil";
+import { setIdValue } from "@packages/bo-utils/idUtil";
+import EventEmitter from "@utils/EventEmitter";
+import { getActive } from "@packages/bpmn-utils/BpmnDesignerUtils";
+
+export default {
+  name: "ElementGenerations",
+  data() {
+    return {
+      elementId: "",
+      elementName: "",
+      elementVersion: "",
+      elementExecutable: true,
+      isProcess: false
+    };
+  },
+
+  mounted() {
+    this.reloadGenerationData();
+    EventEmitter.on("element-update", this.reloadGenerationData);
+  },
+  methods: {
+    reloadGenerationData() {
+      this.isProcess = !!getActive() && getActive().type === "bpmn:Process";
+      this.elementId = getActive().id;
+      this.elementName = getNameValue(getActive()) || "";
+      if (this.isProcess) {
+        this.elementExecutable = getProcessExecutable(getActive());
+        this.elementVersion = getProcessVersionTag(getActive()) || "";
+      }
+    },
+    updateElementName(value) {
+      setNameValue(getActive(), value);
+    },
+    updateElementId(value) {
+      setIdValue(getActive(), value);
+    },
+    updateElementVersion(value) {
+      const reg = /((\d|([1-9](\d*))).){2}(\d|([1-9](\d*)))/;
+      if (reg.test(value)) {
+        setProcessVersionTag(getActive(), value);
+      } else {
+        catchError("版本号必须符合语义化版本2.0.0 要点");
+      }
+    },
+    updateElementExecutable(value) {
+      setProcessExecutable(getActive(), value);
+    }
+  }
+};
+</script>

+ 68 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Panel/components/ElementJobExecution.vue

@@ -0,0 +1,68 @@
+<template>
+  <el-collapse-item name="element-external-task">
+    <template #title>
+      <collapse-title title="执行作业">
+        <lucide-icon name="CalendarClock" />
+      </collapse-title>
+    </template>
+    <div class="element-external-task">
+      <edit-item v-if="tpVisible" label="任务优先级" :label-width="100">
+        <el-input v-model="taskPriority" maxlength="32" @change="setExternalTaskPriority" />
+      </edit-item>
+      <edit-item v-if="rtVisible" label="重试周期" :label-width="100">
+        <el-input v-model="retryTimeCycle" maxlength="32" @change="setRetryTimeCycle" />
+      </edit-item>
+    </div>
+  </el-collapse-item>
+</template>
+
+<script>
+import EventEmitter from "@utils/EventEmitter";
+import {
+  getExternalTaskValue,
+  getRetryTimeCycleValue,
+  retryTimeCycleVisible,
+  setExternalTaskValue,
+  setRetryTimeCycleValue,
+  taskPriorityVisible
+} from "@packages/bo-utils/jobExecutionUtil";
+import { getActive } from "@packages/bpmn-utils/BpmnDesignerUtils";
+
+export default {
+  name: "ElementJobExecution",
+  data() {
+    return {
+      retryTimeCycle: undefined,
+      rtVisible: false,
+      taskPriority: undefined,
+      tpVisible: false
+    };
+  },
+
+  mounted() {
+    this.getRetryTimeCycle();
+    this.getExternalTaskPriority();
+
+    EventEmitter.on("element-update", () => {
+      this.getRetryTimeCycle();
+      this.getExternalTaskPriority();
+    });
+  },
+  methods: {
+    getRetryTimeCycle() {
+      this.rtVisible = retryTimeCycleVisible(getActive());
+      this.retryTimeCycle = getRetryTimeCycleValue(getActive()) || "";
+    },
+    getExternalTaskPriority() {
+      this.tpVisible = taskPriorityVisible(getActive());
+      this.taskPriority = getExternalTaskValue(getActive()) || "";
+    },
+    setRetryTimeCycle(value) {
+      setRetryTimeCycleValue(getActive(), value);
+    },
+    setExternalTaskPriority(value) {
+      setExternalTaskValue(getActive(), value);
+    }
+  }
+};
+</script>

+ 43 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Panel/components/ElementStartInitiator.vue

@@ -0,0 +1,43 @@
+<template>
+  <el-collapse-item name="element-start-initiator">
+    <template #title>
+      <collapse-title title="启动器">
+        <lucide-icon name="PlayCircle" />
+      </collapse-title>
+    </template>
+    <div class="element-start-initiator">
+      <edit-item label="Initiator">
+        <el-input v-model="initiator" @change="setElementInitiator" />
+      </edit-item>
+    </div>
+  </el-collapse-item>
+</template>
+
+<script>
+import { getInitiatorValue, setInitiatorValue } from "@packages/bo-utils/initiatorUtil";
+import EventEmitter from "@utils/EventEmitter";
+import { getActive } from "@packages/bpmn-utils/BpmnDesignerUtils";
+
+export default {
+  name: "ElementStartInitiator",
+  data() {
+    return {
+      initiator: ""
+    };
+  },
+
+  mounted() {
+    this.getElementInitiator();
+
+    EventEmitter.on("element-update", this.getElementInitiator);
+  },
+  methods: {
+    getElementInitiator() {
+      getInitiatorValue(getActive());
+    },
+    setElementInitiator(value) {
+      setInitiatorValue(getActive(), value);
+    }
+  }
+};
+</script>

+ 14 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Panel/components/SubChild/BpmnScript.vue

@@ -0,0 +1,14 @@
+<template>
+  <div class="bpmn-script"></div>
+</template>
+
+<script>
+import { defineComponent } from "vue";
+
+export default {
+  name: "BpmnScript",
+  setup() {
+    return {};
+  }
+};
+</script>

+ 148 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Panel/index.vue

@@ -0,0 +1,148 @@
+<template>
+  <div class="bpmn-panel" ref="panel">
+    <div class="panel-header">
+      <bpmn-icon :name="bpmnIconName" />
+      <p>{{ bpmnElementName }}</p>
+      <p>{{ customTranslate(currentElementType || "Process") }}</p>
+    </div>
+    <el-collapse>
+      <component v-for="cp in this.renderComponents" :key="cp.name" :is="cp" />
+    </el-collapse>
+  </div>
+</template>
+
+<script>
+import { debounce } from "min-dash";
+import Logger from "@utils/Logger";
+import { catchError } from "@utils/printCatch";
+import EventEmitter from "@utils/EventEmitter";
+import BpmnIcon from "@packages/common/BpmnIcon";
+import bpmnIcons from "@packages/bpmn-icons";
+import getBpmnIconType from "@packages/bpmn-icons/getIconType";
+import { customTranslate } from "@packages/additional-modules/Translate";
+import { isCanbeConditional } from "@packages/bo-utils/conditionUtil";
+import { isJobExecutable } from "@packages/bo-utils/jobExecutionUtil";
+import { isExecutable } from "@packages/bo-utils/executionListenersUtil";
+import { isAsynchronous } from "@packages/bo-utils/asynchronousContinuationsUtil";
+import { isStartInitializable } from "@packages/bo-utils/initiatorUtil";
+import { getModeler } from "@packages/bpmn-utils/BpmnDesignerUtils";
+import ElementGenerations from "@packages/Panel/components/ElementGenerations";
+import ElementDocumentations from "@packages/Panel/components/ElementDocumentations";
+import ElementConditional from "@packages/Panel/components/ElementConditional";
+import ElementJobExecution from "@packages/Panel/components/ElementJobExecution";
+import ElementExtensionProperties from "@packages/Panel/components/ElementExtensionProperties";
+import ElementExecutionListeners from "@packages/Panel/components/ElementExecutionListeners";
+import ElementAsyncContinuations from "@packages/Panel/components/ElementAsyncContinuations";
+import ElementStartInitiator from "@packages/Panel/components/ElementStartInitiator";
+import ElementExtensionField from "@packages/Panel/components/ElementExtensionField";
+
+export default {
+  name: "BpmnPanel",
+  components: {
+    BpmnIcon,
+    ElementGenerations,
+    ElementDocumentations,
+    ElementConditional,
+    ElementJobExecution,
+    ElementExtensionProperties,
+    ElementExecutionListeners,
+    ElementAsyncContinuations,
+    ElementStartInitiator,
+    ElementExtensionField,
+  },
+  data() {
+    return {
+      bpmnElementName: "Process",
+      bpmnIconName: "Process",
+      currentElementType: undefined,
+      currentElementId: undefined,
+      customTranslate,
+      renderComponents: [],
+    };
+  },
+  created() {
+    EventEmitter.on("modeler-init", (modeler) => {
+      // 导入完成后默认选中 process 节点
+      modeler.on("import.done", () => {
+        this.setCurrentElement(null);
+      });
+      // 监听选择事件,修改当前激活的元素以及表单
+      modeler.on("selection.changed", ({ newSelection }) => {
+        this.setCurrentElement(newSelection[0] || null);
+      });
+      modeler.on("element.changed", ({ element }) => {
+        // 保证 修改 "默认流转路径" 等类似需要修改多个元素的事件发生的时候,更新表单的元素与原选中元素不一致。
+        if (element && element.id === this.currentElementId) {
+          this.setCurrentElement(element);
+        }
+      });
+    });
+  },
+  mounted() {
+    !this.currentElementId && this.setCurrentElement();
+  },
+  methods: {
+    //
+    setCurrentElement: debounce(function (element) {
+      let activatedElement = element,
+        activatedElementTypeName = "";
+      if (!activatedElement) {
+        const modeler = getModeler();
+        activatedElement =
+          modeler
+            ?.get("elementRegistry")
+            ?.find((el) => el.type === "bpmn:Process") ||
+          modeler
+            ?.get("elementRegistry")
+            ?.find((el) => el.type === "bpmn:Collaboration");
+
+        if (!activatedElement) {
+          return catchError("No Element found!");
+        }
+      }
+      activatedElementTypeName = getBpmnIconType(activatedElement);
+
+      this.$store.commit("setElement", {
+        element: activatedElement,
+        id: activatedElement.id,
+      });
+      this.currentElementId = activatedElement.id;
+      this.currentElementType = activatedElement.type.split(":")[1];
+
+      this.bpmnIconName = bpmnIcons[activatedElementTypeName];
+      this.bpmnElementName = activatedElementTypeName;
+
+      this.setCurrentComponents(activatedElement);
+      EventEmitter.emit("element-update", activatedElement);
+
+      Logger.prettyPrimary(
+        "Selected element changed",
+        `ID: ${activatedElement.id} , type: ${activatedElement.type}`
+      );
+      Logger.prettyInfo(
+        "Selected element businessObject",
+        activatedElement.businessObject
+      );
+    }, 100),
+    //
+    setCurrentComponents(element) {
+      this.renderComponents.splice(0, this.renderComponents.length); // 清空
+      // 重设
+      this.renderComponents.push(ElementGenerations);
+      this.renderComponents.push(ElementDocumentations);
+      isCanbeConditional(element) &&
+        this.renderComponents.push(ElementConditional);
+      isJobExecutable(element) &&
+        this.renderComponents.push(ElementJobExecution);
+      this.renderComponents.push(ElementExtensionProperties);
+      isExecutable(element) &&
+        this.renderComponents.push(ElementExecutionListeners);
+      isAsynchronous(element) &&
+        this.renderComponents.push(ElementAsyncContinuations);
+      isStartInitializable(element) &&
+        this.renderComponents.push(ElementStartInitiator);
+      this.renderComponents.push(ElementExtensionField);
+    },
+  },
+};
+</script>

+ 205 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Settings/index.vue

@@ -0,0 +1,205 @@
+<template>
+  <div class="bpmn-settings" @click.stop>
+    <div class="toggle-button" @click="changeModelVisible">
+      <lucide-icon name="Settings" :size="40" color="#ffffff" />
+    </div>
+    <el-drawer
+      :visible.sync="modelVisible"
+      :size="600"
+      title="偏好设置"
+      append-to-body
+    >
+      <div class="settings-form">
+        <el-form
+          :model="editorSettings"
+          size="mini"
+          label-width="128px"
+          label-suffix=":"
+        >
+          <el-form-item label="流程名称">
+            <el-input v-model="editorSettings.processName" clearable />
+          </el-form-item>
+          <el-form-item label="流程ID">
+            <el-input v-model="editorSettings.processId" clearable />
+          </el-form-item>
+          <el-form-item label="工具栏">
+            <el-switch v-model="editorSettings.toolbar" />
+          </el-form-item>
+          <el-form-item label="小地图">
+            <el-switch v-model="editorSettings.useMinimap" />
+          </el-form-item>
+          <el-form-item label="流程校验">
+            <el-switch v-model="editorSettings.useLint" />
+          </el-form-item>
+          <el-form-item label="流程模拟">
+            <el-switch v-model="editorSettings.useMock" />
+          </el-form-item>
+          <el-form-item label="模板选项扩展">
+            <el-switch v-model="editorSettings.templateChooser" />
+          </el-form-item>
+          <el-form-item label="右键增强">
+            <el-switch v-model="editorSettings.contextmenu" />
+          </el-form-item>
+          <el-form-item label="自定义右键菜单">
+            <el-switch v-model="editorSettings.customContextmenu" />
+          </el-form-item>
+          <el-form-item label="流程引擎">
+            <el-radio-group v-model="editorSettings.processEngine">
+              <el-radio label="camunda">Camunda</el-radio>
+              <el-radio label="activiti">Activiti</el-radio>
+              <el-radio label="flowable">Flowable</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="背景设置">
+            <el-radio-group v-model="editorSettings.bg">
+              <el-radio label="grid-image">自定义网格</el-radio>
+              <el-radio label="grid">默认网点</el-radio>
+              <el-radio label="image">图片</el-radio>
+              <el-radio label="none">空</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="Penal模式">
+            <el-radio-group v-model="editorSettings.penalMode">
+              <el-radio label="default">默认</el-radio>
+              <el-radio label="rewrite" disabled>重写版</el-radio>
+              <el-radio label="custom">自定义</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="Palette模式">
+            <el-radio-group v-model="editorSettings.paletteMode">
+              <el-radio label="default">默认</el-radio>
+              <el-radio label="rewrite">重写版</el-radio>
+              <el-radio label="enhancement">扩展版</el-radio>
+              <el-radio label="custom">自定义</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="ContextPad模式">
+            <el-radio-group v-model="editorSettings.contextPadMode">
+              <el-radio label="default">默认</el-radio>
+              <el-radio label="rewrite">重写版</el-radio>
+              <el-radio label="enhancement">扩展版</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="Renderer模式">
+            <el-radio-group v-model="editorSettings.rendererMode">
+              <el-radio label="default">默认</el-radio>
+              <el-radio label="rewrite">重写版</el-radio>
+              <el-radio label="enhancement">扩展版</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="其他示例扩展">
+            <el-switch v-model="editorSettings.otherModule" />
+          </el-form-item>
+          <el-form-item
+            label="自定义主题"
+            class="theme-list"
+            v-if="editorSettings.rendererMode === 'rewrite'"
+          >
+            <div class="theme-item">
+              <div class="theme-item_label">
+                <el-tooltip
+                  content="该功能只修改了显示部分,路径调整依然沿用折线计算方式,慎用!!!"
+                >
+                  <span slot="">路径曲线 <i class="el-icon-question" />:</span>
+                </el-tooltip>
+              </div>
+              <div>
+                <el-switch v-model="editorSettings.useCurve" />
+              </div>
+            </div>
+            <div
+              class="theme-item"
+              v-for="keyItem in themeColorKeys"
+              :key="keyItem"
+            >
+              <div class="theme-item_label">{{ keyItem }}:</div>
+              <el-color-picker
+                color-format="hex"
+                v-model="editorSettings.customTheme[keyItem]"
+              />
+            </div>
+            <div
+              class="theme-item"
+              v-for="keyItem in themeOpacityKeys"
+              :key="keyItem"
+            >
+              <div class="theme-item_label">{{ keyItem }}:</div>
+              <el-input-number
+                v-model="editorSettings.customTheme[keyItem]"
+                :step="0.1"
+              />
+            </div>
+          </el-form-item>
+        </el-form>
+      </div>
+    </el-drawer>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from "vuex";
+import { defaultSettings } from "@packages/preset-configuration/editor.config";
+import { debounce } from "min-dash";
+
+export default {
+  name: "BpmnSettings",
+  props: {
+    settings: {
+      type: Object,
+      default: () => defaultSettings,
+    },
+  },
+  data() {
+    return {
+      modelVisible: false,
+      themeColorKeys: [
+        "defaultFillColor",
+        "defaultStartEventColor",
+        "defaultEndEventColor",
+        "defaultIntermediateEventColor",
+        "defaultIntermediateThrowEventColor",
+        "defaultIntermediateCatchEventColor",
+        "defaultTaskColor",
+        "defaultLabelColor",
+        "defaultGatewayColor",
+        "defaultSequenceColor",
+      ],
+      themeOpacityKeys: [
+        "defaultStartEventOpacity",
+        "defaultEndEventOpacity",
+        "defaultIntermediateThrowEventOpacity",
+        "defaultIntermediateCatchEventOpacity",
+        "defaultTaskOpacity",
+        "defaultLabelOpacity",
+        "defaultGatewayOpacity",
+        "defaultSequenceOpacity",
+      ],
+      editorSettings: this.settings,
+    };
+  },
+  computed: {
+    ...mapGetters(["getEditor"]),
+  },
+  watch: {
+    editorSettings: {
+      deep: true,
+      handler() {
+        if (this.editorSettings.penalMode !== "custom") {
+          this.editorSettings.processEngine = "camunda";
+        }
+        this.updateEditorState();
+      },
+    },
+  },
+  methods: {
+    changeModelVisible(event) {
+      event.stopPropagation();
+      this.modelVisible = !this.modelVisible;
+    },
+    updateEditorState: debounce(function () {
+      this.editorSettings &&
+        this.$store.commit("setConfiguration", { ...this.editorSettings });
+    }, 100),
+  },
+};
+</script>

+ 27 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Toolbar/index.vue

@@ -0,0 +1,27 @@
+<template>
+  <div class="bpmn-toolbar">
+    <el-button-group>
+      <bpmn-import />
+      <bpmn-exports />
+      <bpmn-previews />
+    </el-button-group>
+    <bpmn-aligns />
+    <bpmn-scales />
+    <bpmn-commands />
+    <bpmn-externals />
+  </div>
+</template>
+
+<script>
+import BpmnImport from "./tools/Import";
+import BpmnExports from "./tools/Exports";
+import BpmnPreviews from "./tools/Previews";
+import BpmnAligns from "./tools/Aligns";
+import BpmnScales from "./tools/Scales";
+import BpmnCommands from "./tools/Commands";
+import BpmnExternals from "./tools/Externals";
+export default {
+  name: "BpmnToolbar",
+  components: { BpmnExternals, BpmnCommands, BpmnScales, BpmnAligns, BpmnPreviews, BpmnExports, BpmnImport }
+};
+</script>

+ 66 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Toolbar/tools/Aligns.vue

@@ -0,0 +1,66 @@
+<template>
+  <el-button-group>
+    <template v-for="(btn, index) in buttons">
+      <el-button
+        v-r-popover:popover="index"
+        class="el-button__no-padding"
+        :key="btn.key"
+        @click="alignElements(btn.key)"
+      >
+        <lucide-icon :name="btn.icon" :size="16" />
+        <el-popover
+          ref="popover"
+          placement="bottom"
+          trigger="hover"
+          popper-class="button-popover"
+          :content="btn.name"
+        />
+      </el-button>
+    </template>
+  </el-button-group>
+</template>
+
+<script>
+import { mapGetters } from "vuex";
+import EventEmitter from "@utils/EventEmitter";
+
+export default {
+  name: "BpmnAligns",
+  computed: {
+    ...mapGetters(["getModeler"]),
+  },
+  data() {
+    return {
+      buttons: [
+        { name: "左对齐", key: "left", icon: "AlignStartVertical" },
+        { name: "水平居中", key: "center", icon: "AlignCenterVertical" },
+        { name: "右对齐", key: "right", icon: "AlignEndVertical" },
+        { name: "上对齐", key: "top", icon: "AlignStartHorizontal" },
+        { name: "垂直居中", key: "middle", icon: "AlignCenterHorizontal" },
+        { name: "下对齐", key: "bottom", icon: "AlignEndHorizontal" },
+      ],
+    };
+  },
+  created() {
+    EventEmitter.on("modeler-init", (modeler) => {
+      this._modeling = modeler.get("modeling");
+      this._selection = modeler.get("selection");
+      this._align = modeler.get("alignElements");
+    });
+  },
+  methods: {
+    alignElements(tag) {
+      if (!this._align) {
+        return this.$message.error("当前模式不支持自动对齐");
+      }
+      if (this._modeling && this._selection) {
+        const SelectedElements = this._selection.get();
+        if (!SelectedElements || SelectedElements.length <= 1) {
+          return this.$message.warning("请按住 Shift 键选择多个元素对齐");
+        }
+        this._align.trigger(SelectedElements, tag);
+      }
+    },
+  },
+};
+</script>

+ 46 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Toolbar/tools/Commands.vue

@@ -0,0 +1,46 @@
+<template>
+  <el-button-group>
+    <el-button v-r-popover:undo class="el-button__no-padding" @click="undo">
+      <lucide-icon name="Undo2" :size="16" />
+      <el-popover ref="undo" placement="bottom" trigger="hover" popper-class="button-popover" content="撤销" />
+    </el-button>
+    <el-button v-r-popover:redo class="el-button__no-padding" @click="redo">
+      <lucide-icon name="Redo2" :size="16" />
+      <el-popover ref="redo" placement="bottom" trigger="hover" popper-class="button-popover" content="恢复" />
+    </el-button>
+    <el-button v-r-popover:restart class="el-button__no-padding" @click="restart">
+      <lucide-icon name="Eraser" :size="16" />
+      <el-popover ref="restart" placement="bottom" trigger="hover" popper-class="button-popover" content="擦除重做" />
+    </el-button>
+  </el-button-group>
+</template>
+
+<script>
+import { mapGetters } from "vuex";
+import { createNewDiagram } from "@utils/xml";
+
+export default {
+  name: "BpmnCommands",
+  computed: {
+    ...mapGetters(["getModeler"])
+  },
+  methods: {
+    getCommand() {
+      return this.getModeler && this.getModeler.get("commandStack");
+    },
+    redo() {
+      const command = this.getCommand();
+      command && command.canRedo() && command.redo();
+    },
+    undo() {
+      const command = this.getCommand();
+      command && command.canUndo() && command.undo();
+    },
+    restart() {
+      const command = this.getCommand();
+      command && command.clear();
+      this.getModeler && createNewDiagram(this.getModeler);
+    }
+  }
+};
+</script>

+ 61 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Toolbar/tools/Exports.vue

@@ -0,0 +1,61 @@
+<template>
+  <el-button v-popover:popover type="primary">
+    导出文件
+    <el-popover ref="popover" placement="bottom" popper-class="button-popover" trigger="hover">
+      <div class="button-list_column">
+        <el-button type="primary" @click="downloadProcessAsBpmn">导出为 Bpmn</el-button>
+        <el-button type="primary" @click="downloadProcessAsXml">导出为 XML</el-button>
+        <el-button type="primary" @click="downloadProcessAsSvg">导出为 SVG</el-button>
+      </div>
+    </el-popover>
+  </el-button>
+</template>
+
+<script>
+import { downloadFile, setEncoded } from "@utils/files";
+import { mapGetters } from "vuex";
+
+export default {
+  name: "BpmnExports",
+  computed: {
+    ...mapGetters(["getModeler"])
+  },
+  methods: {
+    async downloadProcess(type, name = "diagram") {
+      try {
+        if (!this.getModeler) return this.$message.error("流程图引擎初始化失败");
+        const modeler = this.getModeler;
+        // 按需要类型创建文件并下载
+        if (type === "xml" || type === "bpmn") {
+          const { err, xml } = await modeler.saveXML();
+          // 读取异常时抛出异常
+          if (err) {
+            console.error(`[Process Designer Warn ]: ${err.message || err}`);
+          }
+          const { href, filename } = setEncoded(type.toUpperCase(), name, xml);
+          downloadFile(href, filename);
+        } else {
+          const { err, svg } = await modeler.saveSVG();
+          // 读取异常时抛出异常
+          if (err) {
+            return console.error(err);
+          }
+          const { href, filename } = setEncoded("SVG", name, svg);
+          downloadFile(href, filename);
+        }
+      } catch (e) {
+        console.error(`[Process Designer Warn ]: ${e.message || e}`);
+      }
+    },
+    downloadProcessAsBpmn() {
+      this.downloadProcess("bpmn");
+    },
+    downloadProcessAsXml() {
+      this.downloadProcess("xml");
+    },
+    downloadProcessAsSvg() {
+      this.downloadProcess("svg");
+    }
+  }
+};
+</script>

+ 162 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Toolbar/tools/Externals.vue

@@ -0,0 +1,162 @@
+<template>
+  <el-button-group>
+    <el-button
+      v-if="getEditorConfig.useMock"
+      v-r-popover:processMock
+      class="el-button__no-padding"
+      @click="mockSimulationToggle"
+    >
+      <lucide-icon name="Bot" :size="16" />
+      <el-popover
+        ref="processMock"
+        content="开启/关闭流程模拟"
+        placement="bottom"
+        trigger="hover"
+        popper-class="button-popover"
+      />
+    </el-button>
+    <el-button
+      v-if="getEditorConfig.useMinimap"
+      v-r-popover:minimapToggle
+      class="el-button__no-padding"
+      @click="minimapToggle"
+    >
+      <lucide-icon name="Map" :size="16" />
+      <el-popover
+        ref="minimapToggle"
+        content="展开/收起小地图"
+        placement="bottom"
+        trigger="hover"
+        popper-class="button-popover"
+      />
+    </el-button>
+    <el-button v-if="getEditorConfig.useLint" v-r-popover:lintToggle class="el-button__no-padding" @click="lintToggle">
+      <lucide-icon name="FileCheck" :size="16" />
+      <el-popover
+        ref="lintToggle"
+        content="开启/关闭流程校验"
+        placement="bottom"
+        trigger="hover"
+        popper-class="button-popover"
+      />
+    </el-button>
+    <el-button v-r-popover:eventToggle class="el-button__no-padding" @click="eventModelVisible = true">
+      <lucide-icon name="Podcast" :size="16" />
+      <el-popover
+        ref="eventToggle"
+        content="查看bpmn事件"
+        placement="bottom"
+        trigger="hover"
+        popper-class="button-popover"
+      />
+    </el-button>
+    <el-button v-r-popover:keyboard class="el-button__no-padding" @click="keyboardModelVisible = true">
+      <lucide-icon name="Keyboard" :size="16" />
+      <el-popover
+        ref="keyboard"
+        content="键盘快捷键"
+        placement="bottom"
+        trigger="hover"
+        popper-class="button-popover"
+      />
+    </el-button>
+
+    <el-dialog
+      :visible.sync="eventModelVisible"
+      title="Bpmn.js 当前已注册事件"
+      width="560px"
+      append-to-body
+      destroy-on-close
+    >
+      <div class="event-listeners-box">
+        <div class="listener-search">
+          <el-input v-model="listenerFilter" placeholder="事件名称关键字" clearable />
+        </div>
+        <div class="event-listeners-box">
+          <p class="listener-item" v-for="(name, index) in visibleListeners" :key="name">
+            {{ `${index + 1}:${name}` }}
+          </p>
+        </div>
+      </div>
+    </el-dialog>
+
+    <el-dialog :visible.sync="keyboardModelVisible" title="键盘快捷键" width="560px" append-to-body destroy-on-close>
+      <div class="shortcut-keys-model">
+        <p>Undo</p>
+        <p>Ctrl + Z</p>
+        <p>Redo</p>
+        <p>Ctrl + Shift + Z / ctrl + Y</p>
+        <p>Select All</p>
+        <p>Ctrl + A</p>
+        <p>Zoom</p>
+        <p>Ctrl + Mouse Wheel</p>
+        <p>Scrolling (Vertical)</p>
+        <p>Mouse Wheel</p>
+        <p>Scrolling (Horizontal)</p>
+        <p>Shift + Mouse Wheel</p>
+        <p>Direct Editing</p>
+        <p>E</p>
+        <p>Hand Tool</p>
+        <p>H</p>
+        <p>Lasso Tool</p>
+        <p>L</p>
+        <p>Space Tool</p>
+        <p>S</p>
+      </div>
+      <div class="shortcut-keys-model" v-if="templateChooser">
+        <p>Replace Tool</p>
+        <p>R</p>
+        <p>Append anything</p>
+        <p>A</p>
+        <p>Create anything</p>
+        <p>N</p>
+      </div>
+    </el-dialog>
+  </el-button-group>
+</template>
+
+<script>
+import { mapGetters } from "vuex";
+
+export default {
+  name: "BpmnExternals",
+  data() {
+    return {
+      listenerFilter: "",
+      listeners: [],
+      eventModelVisible: false,
+      keyboardModelVisible: false
+    };
+  },
+  computed: {
+    ...mapGetters(["getModeler", "getEditorConfig"]),
+    visibleListeners() {
+      return this.listeners.filter((i) => i.includes(this.listenerFilter));
+    },
+    templateChooser() {
+      return this.getEditorConfig.templateChooser;
+    }
+  },
+  watch: {
+    getModeler: {
+      immediate: true,
+      handler() {
+        if (this.getModeler) {
+          this.listeners = Object.keys(this.getModeler.get("eventBus")?._listeners || {}).sort();
+        }
+      }
+    }
+  },
+  methods: {
+    mockSimulationToggle() {
+      this.getModeler?.get("toggleMode")?.toggleMode();
+    },
+    minimapToggle() {
+      this.getModeler?.get("minimap")?.toggle();
+    },
+    lintToggle() {
+      this.getEditorConfig.useLint && this.getModeler?.get("linting")?.toggle();
+    }
+  }
+};
+</script>

+ 37 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Toolbar/tools/Import.vue

@@ -0,0 +1,37 @@
+<template>
+  <el-button type="primary" @click="openImportWindow">
+    打开文件
+    <input type="file" ref="importRef" style="display: none" accept=".xml,.bpmn" @change="changeImportFile" />
+  </el-button>
+</template>
+
+<script>
+import { catchError } from "@utils/printCatch";
+import { getModeler } from "@packages/bpmn-utils/BpmnDesignerUtils";
+
+export default {
+  name: "BpmnImport",
+  methods: {
+    openImportWindow() {
+      this.$refs.importRef && this.$refs.importRef.click();
+    },
+    changeImportFile() {
+      try {
+        if (this.$refs.importRef && this.$refs.importRef.files) {
+          const file = this.$refs.importRef.files[0];
+          const reader = new FileReader();
+          reader.readAsText(file);
+          reader.onload = function () {
+            const xmlStr = this.result;
+            getModeler() && getModeler().importXML(xmlStr);
+          };
+          this.$refs.importRef.value = null;
+          this.$refs.importRef.files = null;
+        }
+      } catch (e) {
+        catchError(e);
+      }
+    }
+  }
+};
+</script>

+ 64 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Toolbar/tools/Previews.vue

@@ -0,0 +1,64 @@
+<template>
+  <el-button v-popover:popover type="primary">
+    预览文件
+    <el-popover ref="popover" placement="bottom" popper-class="button-popover" trigger="hover">
+      <div class="button-list_column">
+        <el-button type="primary" @click="openXMLPreviewModel">预览 XML</el-button>
+        <el-button type="primary" @click="openJsonPreviewModel">预览 JSON</el-button>
+      </div>
+    </el-popover>
+
+    <el-dialog :title="modelTitle" :visible.sync="codeModelVisible" width="72vw" append-to-body destroy-on-close>
+      <div class="preview-model">
+        <highlightjs :code="codeString" :language="codeLanguage" />
+      </div>
+    </el-dialog>
+  </el-button>
+</template>
+
+<script>
+import { mapGetters } from "vuex";
+import { catchError } from "@utils/printCatch";
+
+export default {
+  name: "BpmnPreviews",
+  computed: {
+    ...mapGetters(["getModeler"]),
+    modelTitle() {
+      return this.codeLanguage === "xml" ? "预览 XML" : "预览 JSON";
+    }
+  },
+  data() {
+    return {
+      codeLanguage: "xml",
+      codeString: "",
+      codeModelVisible: false
+    };
+  },
+  methods: {
+    async openXMLPreviewModel() {
+      try {
+        if (!this.getModeler) return this.$message.error("流程图引擎初始化失败");
+        this.codeLanguage = "xml";
+        this.codeModelVisible = true;
+        const { xml } = await this.getModeler.saveXML({ format: true, preamble: true });
+        this.codeString = xml;
+      } catch (e) {
+        catchError(e);
+      }
+    },
+    async openJsonPreviewModel() {
+      try {
+        if (!this.getModeler) return this.$message.error("流程图引擎初始化失败");
+        this.codeLanguage = "json";
+        this.codeModelVisible = true;
+        const { xml } = await this.getModeler.saveXML({ format: true, preamble: true });
+        const jsonStr = await this.getModeler.get("moddle").fromXML(xml);
+        this.codeString = JSON.stringify(jsonStr, null, 2);
+      } catch (e) {
+        catchError(e);
+      }
+    }
+  }
+};
+</script>

+ 98 - 0
ruoyi-ui/src/views/system/bpmnPro/components/Toolbar/tools/Scales.vue

@@ -0,0 +1,98 @@
+<template>
+  <el-button-group>
+    <el-button
+      v-r-popover:zoomOut
+      class="el-button__no-padding"
+      @click="zoomOut()"
+    >
+      <lucide-icon name="ZoomOut" :size="16" />
+      <el-popover
+        ref="zoomOut"
+        placement="bottom"
+        trigger="hover"
+        popper-class="button-popover"
+        content="缩小视图"
+      />
+    </el-button>
+    <el-button
+      v-r-popover:zoomReset
+      @click="zoomReset('fit-viewport')"
+      style="height: 32px"
+    >
+      <span style="text-align: center; display: inline-block; width: 40px">
+        {{ Math.floor(currentScale * 10) * 10 + "%" }}
+      </span>
+      <el-popover
+        ref="zoomReset"
+        placement="bottom"
+        trigger="hover"
+        popper-class="button-popover"
+        content="重置缩放"
+      />
+    </el-button>
+    <el-button
+      v-r-popover:zoomIn
+      class="el-button__no-padding"
+      @click="zoomIn()"
+    >
+      <lucide-icon name="ZoomIn" :size="16" />
+      <el-popover
+        ref="zoomIn"
+        placement="bottom"
+        trigger="hover"
+        popper-class="button-popover"
+        content="放大视图"
+      />
+    </el-button>
+  </el-button-group>
+</template>
+
+<script>
+import { mapGetters } from "vuex";
+
+import EventEmitter from "@utils/EventEmitter";
+export default {
+  name: "BpmnScales",
+  data() {
+    return {
+      currentScale: 1,
+    };
+  },
+  computed: {
+    ...mapGetters(["getModeler"]),
+  },
+  created() {
+    EventEmitter.on("modeler-init", (modeler) => {
+      try {
+        this._canvas = modeler.get("canvas");
+        this.currentScale = this._canvas.zoom();
+      } finally {
+        modeler.on("canvas.viewbox.changed", ({ viewbox }) => {
+          this.currentScale = viewbox.scale;
+        });
+      }
+    });
+  },
+  methods: {
+    zoomReset(newScale) {
+      this._canvas &&
+        this._canvas.zoom(
+          newScale,
+          newScale === "fit-viewport" ? undefined : { x: 0, y: 0 }
+        );
+    },
+    zoomOut(newScale) {
+      this.currentScale =
+        newScale || Math.floor(this.currentScale * 100 - 0.1 * 100) / 100;
+      this.zoomReset(this.currentScale);
+    },
+    zoomIn(newScale) {
+      this.currentScale =
+        newScale || Math.floor(this.currentScale * 100 + 0.1 * 100) / 100;
+      this.zoomReset(this.currentScale);
+    },
+  },
+};
+</script>
+
+<style scoped></style>

+ 65 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-components/EnhancementContextmenu.js

@@ -0,0 +1,65 @@
+// 右键扩展
+import PopupMenu from "diagram-js/lib/features/popup-menu/PopupMenu";
+import Canvas from "diagram-js/lib/core/Canvas";
+import EventEmitter from "@utils/EventEmitter";
+import { isAppendAction } from "../bpmn-utils/BpmnDesignerUtils";
+
+export default function (modeler, editorConfig) {
+  if (!editorConfig.contextmenu) return;
+  modeler.on("element.contextmenu", 2000, (event) => {
+    const { element, originalEvent } = event;
+
+    // 自定义右键菜单
+    if (editorConfig.customContextmenu) {
+      return EventEmitter.emit("show-contextmenu", originalEvent, element);
+    }
+
+    // 原生面板扩展
+    // 1. 更改元素类型
+    if (!isAppendAction(element)) {
+      return editorConfig.templateChooser
+        ? openEnhancementPopupMenu(modeler, element, originalEvent)
+        : openPopupMenu(modeler, element, originalEvent);
+    }
+    // 2. 创建新元素 (仅开始模板扩展时可以)
+    if (!editorConfig.templateChooser) return;
+    const connectorsExtension = modeler.get("connectorsExtension");
+    connectorsExtension && connectorsExtension.createAnything(originalEvent, getContextMenuPosition(originalEvent));
+  });
+}
+
+// default replace popupMenu
+function openPopupMenu(modeler, element, event) {
+  const popupMenu = modeler.get("popupMenu");
+  if (popupMenu && !popupMenu.isEmpty(element, "bpmn-replace")) {
+    popupMenu.open(element, "bpmn-replace", {
+      cursor: { x: event.clientX + 10, y: event.clientY + 10 }
+    });
+    // 设置画布点击清除事件
+    const canvas = modeler.get("canvas");
+    const container = canvas.getContainer();
+    const closePopupMenu = (ev) => {
+      if (popupMenu && popupMenu.isOpen() && ev.delegateTarget.tagName === "svg") {
+        popupMenu.close();
+        container.removeEventListener("click", closePopupMenu);
+      }
+    };
+    container.addEventListener("click", closePopupMenu);
+  }
+}
+
+// templateChooser enhancement replace popupMenu
+function openEnhancementPopupMenu(modeler, element, event) {
+  const replaceMenu = modeler.get("replaceMenu");
+  if (replaceMenu) {
+    replaceMenu.open(element, getContextMenuPosition(event, true));
+  }
+}
+
+///// utils
+function getContextMenuPosition(event, offset) {
+  return {
+    x: event.clientX + (offset ? 10 : 0),
+    y: event.clientY + (offset ? 25 : 0)
+  };
+}

+ 73 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/AutoPlace/CustomAutoPlace.js

@@ -0,0 +1,73 @@
+import { is } from "bpmn-js/lib/util/ModelUtil";
+import { getMid, asTRBL, getOrientation } from "diagram-js/lib/layout/LayoutUtil";
+import {
+  findFreePosition,
+  generateGetNextPosition,
+  getConnectedDistance
+} from "diagram-js/lib/features/auto-place/AutoPlaceUtil";
+
+class CustomAutoPlace {
+  constructor(config, eventBus) {
+    const { minDistance = 100 } = config || {};
+
+    eventBus.on("autoPlace", 3000, function (context) {
+      const shape = context.shape,
+        source = context.source;
+
+      return getNewShapePosition(source, shape, minDistance);
+    });
+  }
+}
+
+function getVerticalDistance(orientation, minDistance) {
+  if (orientation.indexOf("top") !== -1) {
+    return -1 * minDistance;
+  } else if (orientation.indexOf("bottom") !== -1) {
+    return minDistance;
+  } else {
+    return 0;
+  }
+}
+
+function getNewShapePosition(source, element, minDistance) {
+  if (is(element, "bpmn:FlowNode")) {
+    const sourceTrbl = asTRBL(source);
+    const sourceMid = getMid(source);
+
+    const horizontalDistance = getConnectedDistance(source, {
+      defaultDistance: minDistance,
+      filter: function (connection) {
+        return is(connection, "bpmn:SequenceFlow");
+      }
+    });
+
+    let margin = 30,
+      orientation = "left";
+
+    if (is(source, "bpmn:BoundaryEvent")) {
+      orientation = getOrientation(source, source.host, -25);
+
+      if (orientation.indexOf("top") !== -1) {
+        margin *= -1;
+      }
+    }
+
+    const position = {
+      x: sourceTrbl.right + horizontalDistance + element.width / 2,
+      y: sourceMid.y + getVerticalDistance(orientation, minDistance)
+    };
+
+    const nextPositionDirection = {
+      y: {
+        margin: margin,
+        minDistance: minDistance
+      }
+    };
+
+    return findFreePosition(source, element, position, generateGetNextPosition(nextPositionDirection));
+  }
+}
+
+CustomAutoPlace.$inject = ["config.autoPlace", "eventBus"];
+
+export default CustomAutoPlace;

+ 8 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/AutoPlace/index.js

@@ -0,0 +1,8 @@
+import AutoPlaceModule from "diagram-js/lib/features/auto-place";
+import CustomAutoPlace from "./CustomAutoPlace";
+
+export default {
+  __depends__: [AutoPlaceModule],
+  __init__: ["customAutoPlace"],
+  customAutoPlace: ["type", CustomAutoPlace]
+};

+ 104 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/ContextPad/EnhancementContextPad/enhancementContextPadProvider.js

@@ -0,0 +1,104 @@
+import ContextPadProvider from "bpmn-js/lib/features/context-pad/ContextPadProvider";
+
+class EnhancementContextPadProvider extends ContextPadProvider {
+  constructor(
+    config,
+    injector,
+    eventBus,
+    contextPad,
+    modeling,
+    elementFactory,
+    connect,
+    create,
+    popupMenu,
+    canvas,
+    rules,
+    translate
+  ) {
+    super(
+      config,
+      injector,
+      eventBus,
+      contextPad,
+      modeling,
+      elementFactory,
+      connect,
+      create,
+      popupMenu,
+      canvas,
+      rules,
+      translate,
+      2000
+    );
+
+    this._contextPad = contextPad;
+    this._modeling = modeling;
+    this._elementFactory = elementFactory;
+    this._connect = connect;
+    this._create = create;
+    this._popupMenu = popupMenu;
+    this._canvas = canvas;
+    this._rules = rules;
+    this._translate = translate;
+
+    this._autoPlace = injector.get("autoPlace", false);
+  }
+
+  getContextPadEntries(element) {
+    const actions = {};
+    const modeling = this._modeling;
+
+    const appendUserTask = (event, element) => {
+      const shape = this._elementFactory.createShape({ type: "bpmn:UserTask" });
+      this._create.start(event, shape, {
+        source: element
+      });
+    };
+
+    const append = this._autoPlace
+      ? (event, element) => {
+          const shape = this._elementFactory.createShape({ type: "bpmn:UserTask" });
+          this._autoPlace.append(element, shape);
+        }
+      : appendUserTask;
+
+    // 添加创建用户任务按钮
+    actions["append.append-user-task"] = {
+      group: "model",
+      className: "bpmn-icon-user-task",
+      title: "用户任务",
+      action: {
+        dragstart: appendUserTask,
+        click: append
+      }
+    };
+
+    // 添加一个与edit一组的按钮
+    actions["enhancement-op-1"] = {
+      group: "edit",
+      className: "enhancement-op",
+      title: "扩展操作1",
+      action: {
+        click: function (e) {
+          alert("点击 扩展操作1");
+        }
+      }
+    };
+
+    // 添加一个新分组的自定义按钮
+    actions["enhancement-op"] = {
+      group: "enhancement",
+      className: "enhancement-op",
+      title: "扩展删除",
+      action: {
+        click: function (event, delElement) {
+          modeling.removeElements([...(delElement.incoming || []), ...(delElement.outgoing || []), delElement]);
+        }
+      }
+    };
+
+    return actions;
+  }
+}
+
+export default EnhancementContextPadProvider;

+ 8 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/ContextPad/EnhancementContextPad/index.js

@@ -0,0 +1,8 @@
+import enhancementContextPadProvider from "./enhancementContextPadProvider";
+
+const enhancementContextPad = {
+  __init__: ["enhancementContextPadProvider"],
+  enhancementContextPadProvider: ["type", enhancementContextPadProvider]
+};
+
+export default enhancementContextPad;

+ 8 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/ContextPad/RewriteContextPad/index.js

@@ -0,0 +1,8 @@
+import rewriteContextPadProvider from "./rewriteContextPadProvider";
+
+const rewriteContextPad = {
+  __init__: ["contextPadProvider"],
+  contextPadProvider: ["type", rewriteContextPadProvider]
+};
+
+export default rewriteContextPad;

+ 78 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/ContextPad/RewriteContextPad/rewriteContextPadProvider.js

@@ -0,0 +1,78 @@
+import ContextPadProvider from "bpmn-js/lib/features/context-pad/ContextPadProvider";
+
+class RewriteContextPadProvider extends ContextPadProvider {
+  constructor(
+    config,
+    injector,
+    eventBus,
+    contextPad,
+    modeling,
+    elementFactory,
+    connect,
+    create,
+    popupMenu,
+    canvas,
+    rules,
+    translate
+  ) {
+    super(
+      config,
+      injector,
+      eventBus,
+      contextPad,
+      modeling,
+      elementFactory,
+      connect,
+      create,
+      popupMenu,
+      canvas,
+      rules,
+      translate,
+      2000
+    );
+
+    this._contextPad = contextPad;
+    this._modeling = modeling;
+    this._elementFactory = elementFactory;
+    this._connect = connect;
+    this._create = create;
+    this._popupMenu = popupMenu;
+    this._canvas = canvas;
+    this._rules = rules;
+    this._translate = translate;
+
+    this._autoPlace = injector.get("autoPlace", false);
+  }
+
+  getContextPadEntries(element) {
+    const actions = {};
+
+    // 添加一个与edit一组的按钮
+    actions["enhancement-op-1"] = {
+      group: "edit",
+      className: "enhancement-op",
+      title: "扩展操作1",
+      action: {
+        click: function (e) {
+          alert("点击 扩展操作1");
+        }
+      }
+    };
+
+    // 添加一个新分组的自定义按钮
+    actions["enhancement-op"] = {
+      group: "enhancement",
+      className: "enhancement-op",
+      title: "扩展操作2",
+      action: {
+        click: function (e) {
+          alert("点击 扩展操作2");
+        }
+      }
+    };
+
+    return actions;
+  }
+}
+
+export default RewriteContextPadProvider;

+ 24 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/ElementFactory/CustomElementFactory.js

@@ -0,0 +1,24 @@
+import ElementFactory from "bpmn-js/lib/features/modeling/ElementFactory";
+import { getBusinessObject, is } from "bpmn-js/lib/util/ModelUtil";
+
+class CustomElementFactory extends ElementFactory {
+  constructor(config, bpmnFactory, moddle, translate) {
+    super(bpmnFactory, moddle, translate);
+    this._config = config;
+  }
+
+  getDefaultSize(element, di) {
+    const bo = getBusinessObject(element);
+    const types = Object.keys(this._config || {});
+    for (const type of types) {
+      if (is(bo, type)) {
+        return this._config[type];
+      }
+    }
+    return super.getDefaultSize(element, di);
+  }
+}
+
+CustomElementFactory.$inject = ["config.elementFactory", "bpmnFactory", "moddle", "translate"];
+
+export default CustomElementFactory;

+ 5 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/ElementFactory/index.js

@@ -0,0 +1,5 @@
+import CustomElementFactory from "./CustomElementFactory";
+
+export default {
+  elementFactory: ["type", CustomElementFactory]
+};

+ 95 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Lint/bpmnlint.js

@@ -0,0 +1,95 @@
+/**
+ * 源代码地址:https://codesandbox.io/s/5nj1t?file=/src/index.js
+ * @author felix.mueller
+ */
+
+import rule_0 from "bpmnlint/rules/conditional-flows";
+import rule_1 from "bpmnlint/rules/end-event-required";
+import rule_2 from "bpmnlint/rules/event-sub-process-typed-start-event";
+import rule_3 from "bpmnlint/rules/fake-join";
+import rule_4 from "bpmnlint/rules/label-required";
+import rule_5 from "bpmnlint/rules/no-bpmndi";
+import rule_6 from "bpmnlint/rules/no-complex-gateway";
+import rule_7 from "bpmnlint/rules/no-disconnected";
+import rule_8 from "bpmnlint/rules/no-duplicate-sequence-flows";
+import rule_9 from "bpmnlint/rules/no-gateway-join-fork";
+import rule_10 from "bpmnlint/rules/no-implicit-split";
+import rule_11 from "bpmnlint/rules/no-inclusive-gateway";
+import rule_12 from "bpmnlint/rules/single-blank-start-event";
+import rule_13 from "bpmnlint/rules/single-event-definition";
+import rule_14 from "bpmnlint/rules/start-event-required";
+import rule_15 from "bpmnlint/rules/sub-process-blank-start-event";
+import rule_16 from "bpmnlint/rules/superfluous-gateway";
+
+import taskRequired from "@/views/system/bpmnPro/components/additional-modules/Lint/customLintRules/taskRequired";
+
+const cache = {};
+cache["bpmnlint/conditional-flows"] = rule_0;
+cache["bpmnlint/end-event-required"] = rule_1;
+cache["bpmnlint/event-sub-process-typed-start-event"] = rule_2;
+cache["bpmnlint/fake-join"] = rule_3;
+cache["bpmnlint/label-required"] = rule_4;
+cache["bpmnlint/no-bpmndi"] = rule_5;
+cache["bpmnlint/no-complex-gateway"] = rule_6;
+cache["bpmnlint/no-disconnected"] = rule_7;
+cache["bpmnlint/no-duplicate-sequence-flows"] = rule_8;
+cache["bpmnlint/no-gateway-join-fork"] = rule_9;
+cache["bpmnlint/no-implicit-split"] = rule_10;
+cache["bpmnlint/no-inclusive-gateway"] = rule_11;
+cache["bpmnlint/single-blank-start-event"] = rule_12;
+cache["bpmnlint/single-event-definition"] = rule_13;
+cache["bpmnlint/start-event-required"] = rule_14;
+cache["bpmnlint/sub-process-blank-start-event"] = rule_15;
+cache["bpmnlint/superfluous-gateway"] = rule_16;
+cache["bpmnlint/task-required"] = taskRequired;
+
+/**
+ * A resolver that caches rules and configuration as part of the bundle,
+ * making them accessible in the browser.
+ */
+function Resolver() { }
+
+Resolver.prototype.resolveRule = function (pkg, ruleName) {
+  const rule = cache[pkg + "/" + ruleName];
+
+  if (!rule) {
+    throw new Error("cannot resolve rule <" + pkg + "/" + ruleName + ">");
+  }
+
+  return rule;
+};
+
+Resolver.prototype.resolveConfig = function (pkg, configName) {
+  throw new Error("cannot resolve config <" + configName + "> in <" + pkg + ">");
+};
+
+export const resolver = new Resolver();
+
+export const rules = {
+  "conditional-flows": "error",
+  "end-event-required": "error",
+  "event-sub-process-typed-start-event": "error",
+  "fake-join": "warn",
+  "label-required": "off",
+  "no-bpmndi": "error",
+  "no-complex-gateway": "error",
+  "no-disconnected": "error",
+  "no-duplicate-sequence-flows": "error",
+  "no-gateway-join-fork": "error",
+  "no-implicit-split": "error",
+  "no-inclusive-gateway": "error",
+  "single-blank-start-event": "error",
+  "single-event-definition": "error",
+  "start-event-required": "error",
+  "sub-process-blank-start-event": "error",
+  "superfluous-gateway": "warning"
+};
+
+export const config = {
+  rules: rules
+};
+
+export default {
+  resolver: resolver,
+  config: config
+};

+ 23 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Lint/customLintRules/taskRequired.js

@@ -0,0 +1,23 @@
+const { is, isAny } = require("bpmnlint-utils");
+
+module.exports = function () {
+  function hasStartEvent(node) {
+    const flowElements = node.flowElements || [];
+
+    return flowElements.some((node) => is(node, "bpmn:Task"));
+  }
+
+  function check(node, reporter) {
+    if (!isAny(node, ["bpmn:Process", "bpmn:SubProcess"])) {
+      return;
+    }
+
+    if (!hasStartEvent(node)) {
+      const type = is(node, "bpmn:SubProcess") ? "Sub process" : "Process";
+
+      reporter.report(node.id, type + " is missing task node");
+    }
+  }
+
+  return { check };
+};

+ 106 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Palette/EnhancementPalette/enhancementPaletteProvider.js

@@ -0,0 +1,106 @@
+import { assign } from "min-dash";
+import PaletteProvider from "bpmn-js/lib/features/palette/PaletteProvider";
+
+class EnhancementPaletteProvider extends PaletteProvider {
+  constructor(palette, create, elementFactory, spaceTool, lassoTool, handTool, globalConnect, translate) {
+    super(palette, create, elementFactory, spaceTool, lassoTool, handTool, globalConnect, translate, 2000);
+    this._palette = palette;
+    this._create = create;
+    this._elementFactory = elementFactory;
+    this._spaceTool = spaceTool;
+    this._lassoTool = lassoTool;
+    this._handTool = handTool;
+    this._globalConnect = globalConnect;
+    this._translate = translate;
+  }
+  getPaletteEntries() {
+    const actions = {},
+      create = this._create,
+      elementFactory = this._elementFactory,
+      translate = this._translate;
+
+    function createAction(type, group, className, title, options) {
+      function createListener(event) {
+        const shape = elementFactory.createShape(assign({ type: type }, options));
+
+        if (options) {
+          !shape.businessObject.di && (shape.businessObject.di = {});
+          shape.businessObject.di.isExpanded = options.isExpanded;
+        }
+
+        create.start(event, shape);
+      }
+
+      const shortType = type.replace(/^bpmn:/, "");
+
+      return {
+        group: group,
+        className: className,
+        title: title || translate("Create {type}", { type: shortType }),
+        action: {
+          dragstart: createListener,
+          click: createListener
+        }
+      };
+    }
+
+    function createSqlTask(event) {
+      const sqlTask = elementFactory.createShape({ type: "miyue:SqlTask" });
+      create.start(event, sqlTask);
+    }
+
+    assign(actions, {
+      "create.exclusive-gateway": createAction("bpmn:ExclusiveGateway", "gateway", "bpmn-icon-gateway-none", "网关"),
+      "create.parallel-gateway": createAction(
+        "bpmn:ParallelGateway",
+        "gateway",
+        "bpmn-icon-gateway-parallel",
+        "并行网关"
+      ),
+      "create.event-base-gateway": createAction(
+        "bpmn:EventBasedGateway",
+        "gateway",
+        "bpmn-icon-gateway-eventbased",
+        "事件网关"
+      ),
+      "gateway-separator": {
+        group: "gateway",
+        separator: true
+      },
+      "create.user-task": createAction(
+        "bpmn:UserTask",
+        "activity",
+        "bpmn-icon-user-task",
+        translate("Create User Task")
+      ),
+      "create.sql-task": {
+        group: "activity",
+        className: "miyue-sql-task",
+        title: "数据库任务",
+        action: {
+          click: createSqlTask,
+          dragstart: createSqlTask
+        }
+      },
+      "task-separator": {
+        group: "activity",
+        separator: true
+      }
+    });
+
+    return actions;
+  }
+}
+
+EnhancementPaletteProvider.$inject = [
+  "palette",
+  "create",
+  "elementFactory",
+  "spaceTool",
+  "lassoTool",
+  "handTool",
+  "globalConnect",
+  "translate"
+];
+
+export default EnhancementPaletteProvider;

+ 8 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Palette/EnhancementPalette/index.js

@@ -0,0 +1,8 @@
+import enhancementPaletteProvider from "./enhancementPaletteProvider";
+
+const EnhancementPalette = {
+  __init__: ["enhancementPaletteProvider"],
+  enhancementPaletteProvider: ["type", enhancementPaletteProvider]
+};
+
+export default EnhancementPalette;

+ 9 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Palette/RewritePalette/index.js

@@ -0,0 +1,9 @@
+import rerenderPaletteProvider from "./rewritePaletteProvider";
+
+// 使用 paletteProvider 同名参数 覆盖 默认 paletteProvider 构造函数
+const RerenderPalette = {
+  __init__: ["paletteProvider"],
+  paletteProvider: ["type", rerenderPaletteProvider]
+};
+
+export default RerenderPalette;

+ 191 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Palette/RewritePalette/rewritePaletteProvider.js

@@ -0,0 +1,191 @@
+import PaletteProvider from "bpmn-js/lib/features/palette/PaletteProvider";
+import { assign } from "min-dash";
+import { createAction } from "../utils";
+
+class RewritePaletteProvider extends PaletteProvider {
+  constructor(palette, create, elementFactory, spaceTool, lassoTool, handTool, globalConnect) {
+    super(palette, create, elementFactory, spaceTool, lassoTool, handTool, globalConnect, 2000);
+    this._create = create;
+    this._elementFactory = elementFactory;
+    this._lassoTool = lassoTool;
+    this._handTool = handTool;
+    this._globalConnect = globalConnect;
+  }
+  getPaletteEntries() {
+    const actions = {},
+      create = this._create,
+      elementFactory = this._elementFactory,
+      lassoTool = this._lassoTool,
+      handTool = this._handTool,
+      globalConnect = this._globalConnect;
+
+    function createSqlTask(event) {
+      const sqlTask = elementFactory.createShape({ type: "miyue:SqlTask" });
+
+      create.start(event, sqlTask);
+    }
+
+    function createSubprocess(event) {
+      const subProcess = elementFactory.createShape({
+        type: "bpmn:SubProcess",
+        x: 0,
+        y: 0,
+        isExpanded: true
+      });
+
+      const startEvent = elementFactory.createShape({
+        type: "bpmn:StartEvent",
+        x: 40,
+        y: 82,
+        parent: subProcess
+      });
+
+      create.start(event, [subProcess, startEvent], {
+        hints: {
+          autoSelect: [startEvent]
+        }
+      });
+    }
+
+    assign(actions, {
+      "hand-tool": {
+        group: "tools",
+        className: "bpmn-icon-hand-tool",
+        title: "手型工具",
+        action: {
+          click: function (event) {
+            handTool.activateHand(event);
+          }
+        }
+      },
+      "lasso-tool": {
+        group: "tools",
+        className: "bpmn-icon-lasso-tool",
+        title: "套索工具",
+        action: {
+          click: function (event) {
+            lassoTool.activateSelection(event);
+          }
+        }
+      },
+      "global-connect-tool": {
+        group: "tools",
+        className: "bpmn-icon-connection-multi",
+        title: "全局连线",
+        action: {
+          click: function (event) {
+            globalConnect.toggle(event);
+          }
+        }
+      },
+      "tool-separator": {
+        group: "tools",
+        separator: true
+      },
+      "create.start-event": createAction(
+        elementFactory,
+        create,
+        "bpmn:StartEvent",
+        "events",
+        "bpmn-icon-start-event-none",
+        "开始事件"
+      ),
+      "create.end-event": createAction(
+        elementFactory,
+        create,
+        "bpmn:EndEvent",
+        "events",
+        "bpmn-icon-end-event-none",
+        "结束事件"
+      ),
+      "events-separator": {
+        group: "events",
+        separator: true
+      },
+      "create.exclusive-gateway": createAction(
+        elementFactory,
+        create,
+        "bpmn:ExclusiveGateway",
+        "gateway",
+        "bpmn-icon-gateway-none",
+        "网关"
+      ),
+      "create.parallel-gateway": createAction(
+        elementFactory,
+        create,
+        "bpmn:ParallelGateway",
+        "gateway",
+        "bpmn-icon-gateway-parallel",
+        "并行网关"
+      ),
+      "create.event-base-gateway": createAction(
+        elementFactory,
+        create,
+        "bpmn:EventBasedGateway",
+        "gateway",
+        "bpmn-icon-gateway-eventbased",
+        "事件网关"
+      ),
+      "gateway-separator": {
+        group: "gateway",
+        separator: true
+      },
+      "create.user-task": createAction(
+        elementFactory,
+        create,
+        "bpmn:UserTask",
+        "activity",
+        "bpmn-icon-user-task",
+        "用户任务"
+      ),
+      "create.script-task": createAction(
+        elementFactory,
+        create,
+        "bpmn:ScriptTask",
+        "activity",
+        "bpmn-icon-script-task",
+        "脚本任务"
+      ),
+      "create.service-task": createAction(
+        elementFactory,
+        create,
+        "bpmn:ServiceTask",
+        "activity",
+        "bpmn-icon-service-task",
+        "服务任务"
+      ),
+      "create.sql-task": {
+        group: "activity",
+        className: "miyue-sql-task",
+        title: "数据库任务",
+        action: {
+          click: createSqlTask,
+          dragstart: createSqlTask
+        }
+      },
+      "create.subprocess-expanded": {
+        group: "activity",
+        className: "bpmn-icon-subprocess-expanded",
+        title: "子流程",
+        action: {
+          dragstart: createSubprocess,
+          click: createSubprocess
+        }
+      }
+    });
+
+    return actions;
+  }
+}
+
+RewritePaletteProvider.$inject = [
+  "palette",
+  "create",
+  "elementFactory",
+  "spaceTool",
+  "lassoTool",
+  "handTool",
+  "globalConnect"
+];
+
+export default RewritePaletteProvider;

+ 25 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Palette/utils.js

@@ -0,0 +1,25 @@
+import { assign } from "min-dash";
+import { notNull } from "@/utils/bpmn/tool";
+
+export function createAction(elementFactory, create, type, group, className, title, options) {
+  function createListener(event) {
+    const shape = elementFactory.createShape(assign({ type: type }, options));
+
+    if (options) {
+      !shape.businessObject.di && (shape.businessObject.di = {});
+      notNull(options.isExpanded) && (shape.businessObject.di.isExpanded = options.isExpanded);
+    }
+
+    create.start(event, shape);
+  }
+
+  return {
+    group: group,
+    className: className,
+    title: title,
+    action: {
+      dragstart: createListener,
+      click: createListener
+    }
+  };
+}

+ 3 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/PopupMenu/EnhancementPopupMenu/enhancementPopupMenuProvider.js

@@ -0,0 +1,3 @@
+class EnhancementPopupMenuProvider {}
+
+export default EnhancementPopupMenuProvider;

+ 8 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/PopupMenu/EnhancementPopupMenu/index.js

@@ -0,0 +1,8 @@
+import enhancementPopupMenuProvider from "./enhancementPopupMenuProvider";
+
+const enhancementPopupMenu = {
+  __init__: ["enhancementPopupMenuProvider"],
+  enhancementPopupMenuProvider: ["type", enhancementPopupMenuProvider]
+};
+
+export default enhancementPopupMenu;

+ 8 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/PopupMenu/RewritePopupMenu/index.js

@@ -0,0 +1,8 @@
+import rewritePopupMenuProvider from "./rewritePopupMenuProvider";
+
+const rewritePopupMenu = {
+  __init__: ["replaceMenuProvider"],
+  replaceMenuProvider: ["type", rewritePopupMenuProvider]
+};
+
+export default rewritePopupMenu;

+ 3 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/PopupMenu/RewritePopupMenu/rewritePopupMenuProvider.js

@@ -0,0 +1,3 @@
+class RewritePopupMenuProvider {}
+
+export default RewritePopupMenuProvider;

+ 54 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Renderer/EnhancementRenderer/EnhancementRenderer.js

@@ -0,0 +1,54 @@
+import BpmnRenderer from "bpmn-js/lib/draw/BpmnRenderer";
+import { append as svgAppend, attr as svgAttr, create as svgCreate } from "tiny-svg";
+import renderEventContent from "./renderEventContent";
+import { drawCircle } from "../utils";
+import mysqlIcon from "@packages/theme/process-icons/mysql.png";
+
+class EnhancementRenderer extends BpmnRenderer {
+  constructor(config, eventBus, styles, pathMap, canvas, textRenderer) {
+    super(config, eventBus, styles, pathMap, canvas, textRenderer, 3000);
+
+    this._styles = styles;
+
+    // 重点!!!在这里执行重绘
+    this.handlers["bpmn:Event"] = (parentGfx, element, attrs) => {
+      if (!attrs || !attrs["fillOpacity"]) {
+        !attrs && (attrs = {});
+        attrs["fillOpacity"] = 1;
+        attrs["fill"] = "#1bbc9d";
+        attrs["strokeWidth"] = 0;
+      }
+      return drawCircle(this, parentGfx, element.width, element.height, attrs);
+    };
+    this.handlers["bpmn:EndEvent"] = (parentGfx, element) => {
+      const circle = this.handlers["bpmn:Event"](parentGfx, element, {
+        fillOpacity: 1,
+        strokeWidth: 2,
+        fill: "#e98885",
+        stroke: "#000000"
+      });
+      renderEventContent(this.handlers, element, parentGfx);
+      return circle;
+    };
+
+    // 自定义节点的绘制
+    this.handlers["miyue:SqlTask"] = (parentGfx, element, attr) => {
+      // 渲染外层边框
+      const task = this.handlers["bpmn:Activity"](parentGfx, element);
+      // 自定义节点
+      const customIcon = svgCreate("image");
+      svgAttr(customIcon, {
+        ...(attr || {}),
+        width: element.width,
+        height: element.height,
+        href: mysqlIcon
+      });
+      svgAppend(parentGfx, customIcon);
+      return task;
+    };
+  }
+}
+
+EnhancementRenderer.$inject = ["config.bpmnRenderer", "eventBus", "styles", "pathMap", "canvas", "textRenderer"];
+
+export default EnhancementRenderer;

+ 8 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Renderer/EnhancementRenderer/index.js

@@ -0,0 +1,8 @@
+import EnhancementRenderer from "./EnhancementRenderer";
+
+const enhancementRenderer = {
+  __init__: ["enhancementRenderer"],
+  enhancementRenderer: ["type", EnhancementRenderer]
+};
+
+export default enhancementRenderer;

+ 45 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Renderer/EnhancementRenderer/renderEventContent.js

@@ -0,0 +1,45 @@
+// 绘制事件节点中的元素
+import { getSemantic, isThrowEvent, isTypedEvent } from "../utils";
+
+export default function renderEventContent(handlers, element, parentGfx) {
+  const event = getSemantic(element);
+  const isThrowing = isThrowEvent(event);
+  if (event.eventDefinitions && event.eventDefinitions.length > 1) {
+    if (event.parallelMultiple) {
+      return handlers["bpmn:ParallelMultipleEventDefinition"](parentGfx, element, isThrowing);
+    } else {
+      return handlers["bpmn:MultipleEventDefinition"](parentGfx, element, isThrowing);
+    }
+  }
+  if (isTypedEvent(event, "bpmn:MessageEventDefinition")) {
+    return handlers["bpmn:MessageEventDefinition"](parentGfx, element, isThrowing);
+  }
+  if (isTypedEvent(event, "bpmn:TimerEventDefinition")) {
+    return handlers["bpmn:TimerEventDefinition"](parentGfx, element, isThrowing);
+  }
+  if (isTypedEvent(event, "bpmn:ConditionalEventDefinition")) {
+    return handlers["bpmn:ConditionalEventDefinition"](parentGfx, element);
+  }
+  if (isTypedEvent(event, "bpmn:SignalEventDefinition")) {
+    return handlers["bpmn:SignalEventDefinition"](parentGfx, element, isThrowing);
+  }
+  if (isTypedEvent(event, "bpmn:EscalationEventDefinition")) {
+    return handlers["bpmn:EscalationEventDefinition"](parentGfx, element, isThrowing);
+  }
+  if (isTypedEvent(event, "bpmn:LinkEventDefinition")) {
+    return handlers["bpmn:LinkEventDefinition"](parentGfx, element, isThrowing);
+  }
+  if (isTypedEvent(event, "bpmn:ErrorEventDefinition")) {
+    return handlers["bpmn:ErrorEventDefinition"](parentGfx, element, isThrowing);
+  }
+  if (isTypedEvent(event, "bpmn:CancelEventDefinition")) {
+    return handlers["bpmn:CancelEventDefinition"](parentGfx, element, isThrowing);
+  }
+  if (isTypedEvent(event, "bpmn:CompensateEventDefinition")) {
+    return handlers["bpmn:CompensateEventDefinition"](parentGfx, element, isThrowing);
+  }
+  if (isTypedEvent(event, "bpmn:TerminateEventDefinition")) {
+    return handlers["bpmn:TerminateEventDefinition"](parentGfx, element, isThrowing);
+  }
+  return null;
+}

+ 2015 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Renderer/RewriteRenderer/RewriteRenderer.js

@@ -0,0 +1,2015 @@
+import {
+  append as svgAppend,
+  attr as svgAttr,
+  classes as svgClasses,
+  create as svgCreate,
+  select as svgSelect,
+  on as svgOn
+} from "tiny-svg";
+import Ids from "ids";
+import { assign, forEach, isObject } from "min-dash";
+import { query as domQuery } from "min-dom";
+
+import BaseRenderer from "diagram-js/lib/draw/BaseRenderer";
+import { createLine } from "diagram-js/lib/util/RenderUtil";
+import { rotate, transform, translate } from "diagram-js/lib/util/SvgTransformUtil";
+import {
+  getCirclePath,
+  getDi,
+  getDiamondPath,
+  getFillColor,
+  getLabelColor,
+  getRectPath,
+  getRoundRectPath,
+  getSemantic,
+  getStrokeColor,
+  isCollection,
+  isThrowEvent,
+  isTypedEvent
+} from "bpmn-js/lib/draw/BpmnRenderUtil.js";
+import { is } from "bpmn-js/lib/util/ModelUtil";
+import { getLabel } from "bpmn-js/lib/features/label-editing/LabelUtil";
+import { isEventSubProcess, isExpanded } from "bpmn-js/lib/util/DiUtil";
+
+import mysqlIcon from "@packages/theme/process-icons/mysql.png";
+
+const RENDERER_IDS = new Ids();
+const TASK_BORDER_RADIUS = 10;
+const INNER_OUTER_DIST = 3;
+const DEFAULT_FILL_OPACITY = 0.95;
+const HIGH_FILL_OPACITY = 0.35;
+const ELEMENT_LABEL_DISTANCE = 10;
+
+class RewriteRenderer extends BaseRenderer {
+  constructor(config, eventBus, styles, pathMap, canvas, textRenderer, elementRegistry, interactionEvents, priority) {
+    super(eventBus, priority);
+    this._elementRegistry = elementRegistry;
+    const presetColor = {
+      defaultFillColor: "#ffffff",
+      defaultStartEventColor: "#61c071",
+      defaultEndEventColor: "#d03050",
+      defaultIntermediateEventColor: "#e9a28d",
+      defaultIntermediateThrowEventColor: "#e9a28d",
+      defaultIntermediateCatchEventColor: "#e9a28d",
+      defaultTaskColor: "#9cafcf",
+      defaultLabelColor: "#000000",
+      defaultGatewayColor: "#fb863c",
+      defaultSequenceColor: "#9cafcf"
+    };
+    const presetOpacity = {
+      defaultStartEventOpacity: 0,
+      defaultEndEventOpacity: 0,
+      defaultIntermediateThrowEventOpacity: 0,
+      defaultIntermediateCatchEventOpacity: 1,
+      defaultTaskOpacity: 0,
+      defaultLabelOpacity: 1,
+      defaultGatewayOpacity: 0.2,
+      defaultSequenceOpacity: 1
+    };
+    const {
+      defaultFillColor,
+      defaultStartEventColor,
+      defaultEndEventColor,
+      defaultIntermediateEventColor,
+      defaultIntermediateThrowEventColor,
+      defaultIntermediateCatchEventColor,
+      defaultTaskColor,
+      defaultLabelColor,
+      defaultGatewayColor,
+      defaultSequenceColor
+    } = { ...presetColor, ...config };
+    const {
+      defaultStartEventOpacity,
+      defaultEndEventOpacity,
+      defaultIntermediateThrowEventOpacity,
+      defaultIntermediateCatchEventOpacity,
+      defaultTaskOpacity,
+      defaultLabelOpacity,
+      defaultGatewayOpacity,
+      defaultSequenceOpacity
+    } = { ...presetOpacity, ...config };
+
+    const useCurve = config?.useCurve || false;
+
+    const computeStyle = styles.computeStyle;
+    const rendererId = RENDERER_IDS.next();
+    const markers = {};
+
+    function addMarker(id, options) {
+      const attrs = assign(
+        {
+          fill: "black",
+          strokeWidth: 1,
+          strokeLinecap: "round",
+          strokeDasharray: "none"
+        },
+        options.attrs
+      );
+      const ref = options.ref || { x: 0, y: 0 };
+      const scale = options.scale || 1;
+      if (attrs.strokeDasharray === "none") {
+        attrs.strokeDasharray = [10000, 1];
+      }
+      const marker = svgCreate("marker");
+      svgAttr(options.element, attrs);
+      svgAppend(marker, options.element);
+      svgAttr(marker, {
+        id: id,
+        viewBox: "0 0 20 20",
+        refX: ref.x,
+        refY: ref.y,
+        markerWidth: 20 * scale,
+        markerHeight: 20 * scale,
+        orient: "auto"
+      });
+      let defs = domQuery("defs", canvas._svg);
+      if (!defs) {
+        defs = svgCreate("defs");
+        svgAppend(canvas._svg, defs);
+      }
+      svgAppend(defs, marker);
+      markers[id] = marker;
+    }
+
+    function colorEscape(str) {
+      return str.replace(/[^\da-zA-z]+/g, "_");
+    }
+
+    function marker(type, fill, stroke) {
+      const id = type + "-" + colorEscape(fill) + "-" + colorEscape(stroke) + "-" + rendererId;
+      if (!markers[id]) {
+        createMarker(id, type, fill, stroke);
+      }
+      return "url(#" + id + ")";
+    }
+
+    function createMarker(id, type, fill, stroke) {
+      if (type === "sequenceflow-end") {
+        const sequenceFlowEnd = svgCreate("path");
+        svgAttr(sequenceFlowEnd, { d: "M 1 5 L 11 10 L 1 15 Z" });
+        addMarker(id, {
+          element: sequenceFlowEnd,
+          ref: { x: 11, y: 10 },
+          scale: 0.5,
+          attrs: {
+            fill: stroke,
+            stroke: stroke
+          }
+        });
+      }
+      if (type === "messageflow-start") {
+        const messageflowStart = svgCreate("circle");
+        svgAttr(messageflowStart, { cx: 6, cy: 6, r: 3.5 });
+        addMarker(id, {
+          element: messageflowStart,
+          attrs: {
+            fill: fill,
+            stroke: stroke
+          },
+          ref: { x: 6, y: 6 }
+        });
+      }
+      if (type === "messageflow-end") {
+        const messageflowEnd = svgCreate("path");
+        svgAttr(messageflowEnd, { d: "m 1 5 l 0 -3 l 7 3 l -7 3 z" });
+        addMarker(id, {
+          element: messageflowEnd,
+          attrs: {
+            fill: fill,
+            stroke: stroke,
+            strokeLinecap: "butt"
+          },
+          ref: { x: 8.5, y: 5 }
+        });
+      }
+      if (type === "association-start") {
+        const associationStart = svgCreate("path");
+        svgAttr(associationStart, { d: "M 11 5 L 1 10 L 11 15" });
+        addMarker(id, {
+          element: associationStart,
+          attrs: {
+            fill: "none",
+            stroke: stroke,
+            strokeWidth: 1.5
+          },
+          ref: { x: 1, y: 10 },
+          scale: 0.5
+        });
+      }
+      if (type === "association-end") {
+        const associationEnd = svgCreate("path");
+        svgAttr(associationEnd, { d: "M 1 5 L 11 10 L 1 15" });
+        addMarker(id, {
+          element: associationEnd,
+          attrs: {
+            fill: "none",
+            stroke: stroke,
+            strokeWidth: 1.5
+          },
+          ref: { x: 12, y: 10 },
+          scale: 0.5
+        });
+      }
+      if (type === "conditional-flow-marker") {
+        const conditionalflowMarker = svgCreate("path");
+        svgAttr(conditionalflowMarker, { d: "M 0 10 L 8 6 L 16 10 L 8 14 Z" });
+        addMarker(id, {
+          element: conditionalflowMarker,
+          attrs: {
+            fill: fill,
+            stroke: stroke
+          },
+          ref: { x: -1, y: 10 },
+          scale: 0.5
+        });
+      }
+      if (type === "conditional-default-flow-marker") {
+        const conditionaldefaultflowMarker = svgCreate("path");
+        svgAttr(conditionaldefaultflowMarker, { d: "M 6 4 L 10 16" });
+        addMarker(id, {
+          element: conditionaldefaultflowMarker,
+          attrs: {
+            stroke: stroke
+          },
+          ref: { x: 0, y: 10 },
+          scale: 0.5
+        });
+      }
+    }
+
+    function drawCircle(parentGfx, width, height, offset, attrs) {
+      if (isObject(offset)) {
+        attrs = offset;
+        offset = 0;
+      }
+
+      offset = offset || 0;
+
+      attrs = computeStyle(attrs, {
+        stroke: "black",
+        strokeWidth: 2,
+        fill: "white"
+      });
+
+      if (attrs.fill === "none") {
+        delete attrs.fillOpacity;
+      }
+
+      const cx = width / 2,
+        cy = height / 2;
+
+      const circle = svgCreate("circle");
+      svgAttr(circle, {
+        cx: cx,
+        cy: cy,
+        r: Math.round((width + height) / 4 - offset)
+      });
+      svgAttr(circle, attrs);
+
+      svgAppend(parentGfx, circle);
+
+      return circle;
+    }
+
+    function drawRect(parentGfx, width, height, r, offset, attrs) {
+      if (isObject(offset)) {
+        attrs = offset;
+        offset = 0;
+      }
+
+      offset = offset || 0;
+
+      attrs = computeStyle(attrs, {
+        stroke: "black",
+        strokeWidth: 2,
+        fill: "white"
+      });
+
+      const rect = svgCreate("rect");
+      svgAttr(rect, {
+        x: offset,
+        y: offset,
+        width: width - offset * 2,
+        height: height - offset * 2,
+        rx: r,
+        ry: r
+      });
+      svgAttr(rect, attrs);
+
+      svgAppend(parentGfx, rect);
+
+      return rect;
+    }
+
+    function drawDiamond(parentGfx, width, height, attrs) {
+      const x_2 = width / 2;
+      const y_2 = height / 2;
+
+      const points = [
+        { x: x_2, y: 0 },
+        { x: width, y: y_2 },
+        { x: x_2, y: height },
+        { x: 0, y: y_2 }
+      ];
+
+      const pointsString = points
+        .map(function (point) {
+          return point.x + "," + point.y;
+        })
+        .join(" ");
+
+      attrs = computeStyle(attrs, {
+        stroke: "black",
+        strokeWidth: 2,
+        fill: "white"
+      });
+
+      const polygon = svgCreate("polygon");
+      svgAttr(polygon, {
+        points: pointsString
+      });
+      svgAttr(polygon, attrs);
+
+      svgAppend(parentGfx, polygon);
+
+      return polygon;
+    }
+
+    function drawLine(parentGfx, waypoints, attrs) {
+      attrs = computeStyle(attrs, ["no-fill"], {
+        stroke: "black",
+        strokeWidth: 2,
+        fill: "none"
+      });
+      const line = createLine(waypoints, attrs);
+      svgAppend(parentGfx, line);
+      return line;
+    }
+
+    function drawPath(parentGfx, d, attrs) {
+      attrs = computeStyle(attrs, ["no-fill"], {
+        strokeWidth: 2,
+        stroke: "black"
+      });
+
+      const path = svgCreate("path");
+      svgAttr(path, { d: d });
+      svgAttr(path, attrs);
+
+      svgAppend(parentGfx, path);
+
+      return path;
+    }
+
+    function drawMarker(type, parentGfx, path, attrs) {
+      return drawPath(parentGfx, path, assign({ "data-marker": type }, attrs));
+    }
+
+    function as(type) {
+      return function (parentGfx, element) {
+        return renderer(type)(parentGfx, element);
+      };
+    }
+
+    function renderEventContent(element, parentGfx) {
+      const event = getSemantic(element);
+      const isThrowing = isThrowEvent(event);
+      const colorOptions = {
+        "bpmn:StartEvent": { color: defaultStartEventColor, opacity: defaultStartEventOpacity },
+        "bpmn:EndEvent": { color: defaultEndEventColor, opacity: defaultEndEventOpacity },
+        "bpmn:BoundaryEvent": { color: defaultTaskColor, opacity: defaultTaskOpacity },
+        "bpmn:IntermediateThrowEvent": {
+          color: defaultIntermediateThrowEventColor,
+          opacity: defaultIntermediateThrowEventOpacity
+        },
+        "bpmn:IntermediateCatchEvent": {
+          color: defaultIntermediateCatchEventColor,
+          opacity: defaultIntermediateCatchEventOpacity
+        }
+      };
+      const type = element.type;
+
+      if (event.eventDefinitions && event.eventDefinitions.length > 1) {
+        if (event.parallelMultiple) {
+          return renderer("bpmn:ParallelMultipleEventDefinition")(parentGfx, element, {
+            isThrowing,
+            attrs: colorOptions[type]
+          });
+        } else {
+          return renderer("bpmn:MultipleEventDefinition")(parentGfx, element, {
+            isThrowing,
+            attrs: colorOptions[type]
+          });
+        }
+      }
+
+      if (isTypedEvent(event, "bpmn:MessageEventDefinition")) {
+        return renderer("bpmn:MessageEventDefinition")(parentGfx, element, {
+          isThrowing,
+          attrs: colorOptions[type]
+        });
+      }
+
+      if (isTypedEvent(event, "bpmn:TimerEventDefinition")) {
+        return renderer("bpmn:TimerEventDefinition")(parentGfx, element, {
+          isThrowing,
+          attrs: colorOptions[type]
+        });
+      }
+
+      if (isTypedEvent(event, "bpmn:ConditionalEventDefinition")) {
+        return renderer("bpmn:ConditionalEventDefinition")(parentGfx, element, {
+          attrs: colorOptions[type]
+        });
+      }
+
+      if (isTypedEvent(event, "bpmn:SignalEventDefinition")) {
+        return renderer("bpmn:SignalEventDefinition")(parentGfx, element, {
+          isThrowing,
+          attrs: colorOptions[type]
+        });
+      }
+
+      if (isTypedEvent(event, "bpmn:EscalationEventDefinition")) {
+        return renderer("bpmn:EscalationEventDefinition")(parentGfx, element, {
+          isThrowing,
+          attrs: colorOptions[type]
+        });
+      }
+
+      if (isTypedEvent(event, "bpmn:LinkEventDefinition")) {
+        return renderer("bpmn:LinkEventDefinition")(parentGfx, element, {
+          isThrowing,
+          attrs: colorOptions[type]
+        });
+      }
+
+      if (isTypedEvent(event, "bpmn:ErrorEventDefinition")) {
+        return renderer("bpmn:ErrorEventDefinition")(parentGfx, element, {
+          isThrowing,
+          attrs: colorOptions[type]
+        });
+      }
+
+      if (isTypedEvent(event, "bpmn:CancelEventDefinition")) {
+        return renderer("bpmn:CancelEventDefinition")(parentGfx, element, {
+          isThrowing,
+          attrs: colorOptions[type]
+        });
+      }
+
+      if (isTypedEvent(event, "bpmn:CompensateEventDefinition")) {
+        return renderer("bpmn:CompensateEventDefinition")(parentGfx, element, {
+          isThrowing,
+          attrs: colorOptions[type]
+        });
+      }
+
+      if (isTypedEvent(event, "bpmn:TerminateEventDefinition")) {
+        return renderer("bpmn:TerminateEventDefinition")(parentGfx, element, {
+          isThrowing,
+          attrs: colorOptions[type]
+        });
+      }
+
+      return null;
+    }
+
+    function renderLabel(parentGfx, label, options) {
+      options = assign(
+        {
+          size: {
+            width: 100
+          }
+        },
+        options
+      );
+
+      const text = textRenderer.createText(label || "", options);
+
+      svgClasses(text).add("djs-label");
+
+      svgAppend(parentGfx, text);
+
+      return text;
+    }
+
+    function renderButton(parentGfx, text, options) {
+      const button = svgCreate("rect");
+      const attrs = computeStyle(options, {
+        stroke: "black",
+        strokeWidth: 2,
+        fill: "white"
+      });
+      const offset = 4;
+
+      svgAttr(button, {
+        x: 40,
+        y: 8,
+        width: 60 - offset * 2,
+        height: 36 - offset * 2,
+        rx: offset,
+        ry: offset
+      });
+      svgAttr(button, attrs);
+
+      svgAppend(parentGfx, button);
+      svgOn(
+        button,
+        "click",
+        function (event) {
+          event.stopPropagation();
+          alert("task button click");
+        },
+        false
+      );
+      return button;
+    }
+
+    function renderEmbeddedLabel(parentGfx, element, align) {
+      const semantic = getSemantic(element);
+
+      return renderLabel(parentGfx, semantic.name, {
+        box: element,
+        align: align,
+        padding: 5,
+        style: {
+          fill: getLabelColor(element, defaultLabelColor, defaultTaskColor)
+        }
+      });
+    }
+
+    function renderExternalLabel(parentGfx, element) {
+      const box = {
+        width: 90,
+        height: 30,
+        x: element.width / 2 + element.x,
+        y: element.height / 2 + element.y
+      };
+
+      return renderLabel(parentGfx, getLabel(element), {
+        box: box,
+        fitBox: true,
+        style: assign({}, textRenderer.getExternalStyle(), {
+          fill: getLabelColor(element, defaultLabelColor, defaultTaskColor)
+        })
+      });
+    }
+
+    function renderLaneLabel(parentGfx, text, element) {
+      const textBox = renderLabel(parentGfx, text, {
+        box: {
+          height: 30,
+          width: element.height
+        },
+        align: "center-middle",
+        style: {
+          fill: getLabelColor(element, defaultLabelColor, defaultTaskColor)
+        }
+      });
+
+      const top = -1 * element.height;
+
+      transform(textBox, 0, -top, 270);
+    }
+
+    function createPathFromConnection(connection) {
+      const waypoints = connection.waypoints;
+
+      // 起始点
+      let pathData = "m " + waypoints[0].x + "," + waypoints[0].y;
+
+      // 原始折线
+      if (!useCurve) {
+        for (let i = 1; i < waypoints.length; i++) {
+          pathData += "L" + waypoints[i].x + "," + waypoints[i].y + " ";
+        }
+        return pathData;
+      }
+
+      // 曲线绘制部分
+      // 不同 length 对应的 svg path关键字,两个点时使用直线,三个点用Q,其余数量使用三次贝塞尔曲线
+      const curveCodeMap = {
+        2: " L ",
+        3: " Q ",
+        0: " C " // 置空作用
+      };
+      // 点数总数
+      const waypointCount = waypoints.length;
+      // 路径组装
+      if (waypointCount === 2) {
+        for (let i = 1; i < waypointCount; i++) {
+          pathData += curveCodeMap[waypointCount] + waypoints[i].x + "," + waypoints[i].y + " ";
+        }
+      } else {
+        pathData += curveCodeMap[waypointCount] || "C";
+        for (let i = 1; i < waypoints.length; i++) {
+          pathData += waypoints[i].x + "," + waypoints[i].y + " ";
+        }
+      }
+
+      console.log("pathData", pathData);
+      return pathData;
+    }
+
+    function attachTaskMarkers(parentGfx, element, taskMarkers) {
+      const obj = getSemantic(element);
+
+      const subprocess = taskMarkers && taskMarkers.indexOf("SubProcessMarker") !== -1;
+      let position;
+
+      if (subprocess) {
+        position = {
+          seq: -21,
+          parallel: -22,
+          compensation: -42,
+          loop: -18,
+          adhoc: 10
+        };
+      } else {
+        position = {
+          seq: -3,
+          parallel: -6,
+          compensation: -27,
+          loop: 0,
+          adhoc: 10
+        };
+      }
+
+      forEach(taskMarkers, function (marker) {
+        renderer(marker)(parentGfx, element, position);
+      });
+
+      if (obj.isForCompensation) {
+        renderer("CompensationMarker")(parentGfx, element, position);
+      }
+
+      if (obj.$type === "bpmn:AdHocSubProcess") {
+        renderer("AdhocMarker")(parentGfx, element, position);
+      }
+
+      const loopCharacteristics = obj.loopCharacteristics,
+        isSequential = loopCharacteristics && loopCharacteristics.isSequential;
+
+      if (loopCharacteristics) {
+        if (isSequential === undefined) {
+          renderer("LoopMarker")(parentGfx, element, position);
+        }
+
+        if (isSequential === false) {
+          renderer("ParallelMarker")(parentGfx, element, position);
+        }
+
+        if (isSequential === true) {
+          renderer("SequentialMarker")(parentGfx, element, position);
+        }
+      }
+    }
+
+    function renderDataItemCollection(parentGfx, element) {
+      const yPosition = (element.height - 18) / element.height;
+
+      const pathData = pathMap.getScaledPath("DATA_OBJECT_COLLECTION_PATH", {
+        xScaleFactor: 1,
+        yScaleFactor: 1,
+        containerWidth: element.width,
+        containerHeight: element.height,
+        position: {
+          mx: 0.33,
+          my: yPosition
+        }
+      });
+
+      /* collection path */ drawPath(parentGfx, pathData, {
+        strokeWidth: 2
+      });
+    }
+
+    this._drawPath = function drawPath(parentGfx, d, attrs) {
+      attrs = computeStyle(attrs, ["no-fill"], {
+        strokeWidth: 2,
+        stroke: "black"
+      });
+      const path = svgCreate("path");
+      svgAttr(path, { d: d });
+      svgAttr(path, attrs);
+      svgAppend(parentGfx, path);
+      return path;
+    };
+    const renderer = (this._renderer = function (type) {
+      return handlers[type];
+    });
+    const handlers = (this.handlers = {
+      "bpmn:Event": function (parentGfx, element, attrs = {}) {
+        if (!("fillOpacity" in attrs)) {
+          attrs["fillOpacity"] = DEFAULT_FILL_OPACITY;
+        }
+        return drawCircle(parentGfx, element.width, element.height, attrs);
+      },
+      "bpmn:StartEvent": function (parentGfx, element) {
+        let attrs = {
+          fill: getFillColor(element, defaultStartEventColor),
+          fillOpacity: defaultStartEventOpacity,
+          stroke: getStrokeColor(element, defaultStartEventColor)
+        };
+        const semantic = getSemantic(element);
+        if (!semantic.isInterrupting) {
+          attrs = {
+            strokeDasharray: "6",
+            strokeLinecap: "round",
+            fill: getFillColor(element, defaultStartEventColor),
+            stroke: getStrokeColor(element, defaultStartEventColor),
+            fillOpacity: defaultStartEventOpacity
+          };
+        }
+        const circle = renderer("bpmn:Event")(parentGfx, element, attrs);
+        renderEventContent(element, parentGfx);
+        return circle;
+      },
+      "bpmn:MessageEventDefinition": function (parentGfx, element, options) {
+        const pathData = pathMap.getScaledPath("EVENT_MESSAGE", {
+          xScaleFactor: 0.9,
+          yScaleFactor: 0.9,
+          containerWidth: element.width,
+          containerHeight: element.height,
+          position: {
+            mx: 0.235,
+            my: 0.315
+          }
+        });
+        const fill = options.isThrowing ? getFillColor(element, options?.attrs?.color) : defaultFillColor;
+        const stroke = options.isThrowing ? defaultFillColor : getStrokeColor(element, options?.attrs?.color);
+        return drawPath(parentGfx, pathData, {
+          strokeWidth: 1,
+          fill,
+          stroke
+        });
+      },
+      "bpmn:TimerEventDefinition": function (parentGfx, element, options) {
+        const fill = options.isThrowing ? getFillColor(element, options?.attrs?.color) : defaultFillColor;
+        const stroke = options.isThrowing ? defaultFillColor : getStrokeColor(element, options?.attrs?.color);
+        const circle = drawCircle(parentGfx, element.width, element.height, element.height * 0.2, {
+          strokeWidth: 2,
+          fill,
+          stroke
+        });
+        const pathData = pathMap.getScaledPath("EVENT_TIMER_WH", {
+          xScaleFactor: 0.75,
+          yScaleFactor: 0.75,
+          containerWidth: element.width,
+          containerHeight: element.height,
+          position: {
+            mx: 0.5,
+            my: 0.5
+          }
+        });
+        drawPath(parentGfx, pathData, {
+          strokeWidth: 2,
+          strokeLinecap: "square",
+          stroke
+        });
+        for (let i = 0; i < 12; i++) {
+          const linePathData = pathMap.getScaledPath("EVENT_TIMER_LINE", {
+            xScaleFactor: 0.75,
+            yScaleFactor: 0.75,
+            containerWidth: element.width,
+            containerHeight: element.height,
+            position: {
+              mx: 0.5,
+              my: 0.5
+            }
+          });
+
+          const width = element.width / 2;
+          const height = element.height / 2;
+
+          drawPath(parentGfx, linePathData, {
+            strokeWidth: 1,
+            strokeLinecap: "square",
+            transform: "rotate(" + i * 30 + "," + height + "," + width + ")",
+            stroke
+          });
+        }
+        return circle;
+      },
+      "bpmn:EscalationEventDefinition": function (parentGfx, event, options) {
+        const pathData = pathMap.getScaledPath("EVENT_ESCALATION", {
+          xScaleFactor: 1,
+          yScaleFactor: 1,
+          containerWidth: event.width,
+          containerHeight: event.height,
+          position: {
+            mx: 0.5,
+            my: 0.2
+          }
+        });
+        const fill = options.isThrowing ? getFillColor(event, options?.attrs?.color) : defaultFillColor;
+        const stroke = options.isThrowing ? defaultFillColor : getStrokeColor(event, options?.attrs?.color);
+        return drawPath(parentGfx, pathData, {
+          strokeWidth: 1,
+          fill,
+          stroke
+        });
+      },
+      "bpmn:ConditionalEventDefinition": function (parentGfx, event, options) {
+        const pathData = pathMap.getScaledPath("EVENT_CONDITIONAL", {
+          xScaleFactor: 1,
+          yScaleFactor: 1,
+          containerWidth: event.width,
+          containerHeight: event.height,
+          position: {
+            mx: 0.5,
+            my: 0.222
+          }
+        });
+        const stroke = options.isThrowing ? defaultFillColor : getStrokeColor(event, options?.attrs?.color);
+
+        return drawPath(parentGfx, pathData, {
+          strokeWidth: 1,
+          stroke
+        });
+      },
+      "bpmn:LinkEventDefinition": function (parentGfx, event, options) {
+        const pathData = pathMap.getScaledPath("EVENT_LINK", {
+          xScaleFactor: 1,
+          yScaleFactor: 1,
+          containerWidth: event.width,
+          containerHeight: event.height,
+          position: {
+            mx: 0.57,
+            my: 0.263
+          }
+        });
+        const fill = options.isThrowing ? getFillColor(event, options?.attrs?.color) : defaultFillColor;
+        const stroke = options.isThrowing ? defaultFillColor : getStrokeColor(event, options?.attrs?.color);
+        return drawPath(parentGfx, pathData, {
+          strokeWidth: 1,
+          fill,
+          stroke
+        });
+      },
+      "bpmn:ErrorEventDefinition": function (parentGfx, event, options) {
+        const pathData = pathMap.getScaledPath("EVENT_ERROR", {
+          xScaleFactor: 1.1,
+          yScaleFactor: 1.1,
+          containerWidth: event.width,
+          containerHeight: event.height,
+          position: {
+            mx: 0.2,
+            my: 0.722
+          }
+        });
+        const fill = options.isThrowing ? getFillColor(event, options?.attrs?.color) : defaultFillColor;
+        const stroke = options.isThrowing ? defaultFillColor : getStrokeColor(event, options?.attrs?.color);
+        return drawPath(parentGfx, pathData, {
+          strokeWidth: 1,
+          fill,
+          stroke
+        });
+      },
+      "bpmn:CancelEventDefinition": function (parentGfx, event, options) {
+        const pathData = pathMap.getScaledPath("EVENT_CANCEL_45", {
+          xScaleFactor: 1.0,
+          yScaleFactor: 1.0,
+          containerWidth: event.width,
+          containerHeight: event.height,
+          position: {
+            mx: 0.638,
+            my: -0.055
+          }
+        });
+        const fill = options.isThrowing ? getFillColor(event, options?.attrs?.color) : defaultFillColor;
+        const stroke = options.isThrowing ? defaultFillColor : getStrokeColor(event, options?.attrs?.color);
+        const path = drawPath(parentGfx, pathData, {
+          strokeWidth: 1,
+          fill,
+          stroke
+        });
+
+        rotate(path, 45);
+
+        return path;
+      },
+      "bpmn:CompensateEventDefinition": function (parentGfx, event, options) {
+        const pathData = pathMap.getScaledPath("EVENT_COMPENSATION", {
+          xScaleFactor: 1,
+          yScaleFactor: 1,
+          containerWidth: event.width,
+          containerHeight: event.height,
+          position: {
+            mx: 0.22,
+            my: 0.5
+          }
+        });
+        const fill = options.isThrowing ? getFillColor(event, options?.attrs?.color) : defaultFillColor;
+        const stroke = options.isThrowing ? defaultFillColor : getStrokeColor(event, options?.attrs?.color);
+        return drawPath(parentGfx, pathData, {
+          strokeWidth: 1,
+          fill,
+          stroke
+        });
+      },
+      "bpmn:SignalEventDefinition": function (parentGfx, event, options) {
+        const pathData = pathMap.getScaledPath("EVENT_SIGNAL", {
+          xScaleFactor: 0.9,
+          yScaleFactor: 0.9,
+          containerWidth: event.width,
+          containerHeight: event.height,
+          position: {
+            mx: 0.5,
+            my: 0.2
+          }
+        });
+        const fill = options.isThrowing ? getFillColor(event, options?.attrs?.color) : defaultFillColor;
+        const stroke = options.isThrowing ? defaultFillColor : getStrokeColor(event, options?.attrs?.color);
+        return drawPath(parentGfx, pathData, {
+          strokeWidth: 1,
+          fill,
+          stroke
+        });
+      },
+      "bpmn:MultipleEventDefinition": function (parentGfx, event, options) {
+        const pathData = pathMap.getScaledPath("EVENT_MULTIPLE", {
+          xScaleFactor: 1.1,
+          yScaleFactor: 1.1,
+          containerWidth: event.width,
+          containerHeight: event.height,
+          position: {
+            mx: 0.222,
+            my: 0.36
+          }
+        });
+        const fill = options.isThrowing ? getFillColor(event, options?.attrs?.color) : defaultFillColor;
+        const stroke = options.isThrowing ? defaultFillColor : getStrokeColor(event, options?.attrs?.color);
+        return drawPath(parentGfx, pathData, {
+          strokeWidth: 1,
+          fill,
+          stroke
+        });
+      },
+      "bpmn:ParallelMultipleEventDefinition": function (parentGfx, event, options) {
+        const pathData = pathMap.getScaledPath("EVENT_PARALLEL_MULTIPLE", {
+          xScaleFactor: 1.2,
+          yScaleFactor: 1.2,
+          containerWidth: event.width,
+          containerHeight: event.height,
+          position: {
+            mx: 0.458,
+            my: 0.194
+          }
+        });
+        const fill = options.isThrowing ? getFillColor(event, options?.attrs?.color) : defaultFillColor;
+        const stroke = options.isThrowing ? defaultFillColor : getStrokeColor(event, options?.attrs?.color);
+        return drawPath(parentGfx, pathData, {
+          strokeWidth: 1,
+          fill,
+          stroke
+        });
+      },
+      "bpmn:EndEvent": function (parentGfx, element) {
+        const circle = renderer("bpmn:Event")(parentGfx, element, {
+          strokeWidth: 4,
+          fill: getFillColor(element, defaultEndEventColor),
+          fillOpacity: defaultEndEventOpacity,
+          stroke: getStrokeColor(element, defaultEndEventColor)
+        });
+        renderEventContent(element, parentGfx);
+        return circle;
+      },
+      "bpmn:TerminateEventDefinition": function (parentGfx, element) {
+        return drawCircle(parentGfx, element.width, element.height, 8, {
+          strokeWidth: 4,
+          fill: getStrokeColor(element, defaultEndEventColor),
+          stroke: getStrokeColor(element, defaultEndEventColor)
+        });
+      },
+      "bpmn:IntermediateEvent": function (parentGfx, element) {
+        const outer = renderer("bpmn:Event")(parentGfx, element, {
+          strokeWidth: 1,
+          stroke: getStrokeColor(element, defaultIntermediateEventColor)
+        });
+        drawCircle(parentGfx, element.width, element.height, INNER_OUTER_DIST, {
+          strokeWidth: 1,
+          stroke: getStrokeColor(element, defaultIntermediateEventColor)
+        });
+        renderEventContent(element, parentGfx);
+        return outer;
+      },
+      "bpmn:IntermediateCatchEvent": as("bpmn:IntermediateEvent"),
+      "bpmn:IntermediateThrowEvent": as("bpmn:IntermediateEvent"),
+      "bpmn:Activity": function (parentGfx, element, attrs) {
+        attrs = attrs || {};
+
+        if (!("fillOpacity" in attrs)) {
+          attrs.fillOpacity = DEFAULT_FILL_OPACITY;
+        }
+
+        return drawRect(parentGfx, element.width, element.height, TASK_BORDER_RADIUS, attrs);
+      },
+      "bpmn:Task": function (parentGfx, element) {
+        const attrs = {
+          fill: getFillColor(element, defaultFillColor),
+          fillOpacity: defaultTaskOpacity,
+          stroke: getStrokeColor(element, defaultTaskColor)
+        };
+
+        const rect = renderer("bpmn:Activity")(parentGfx, element, attrs);
+
+        renderEmbeddedLabel(parentGfx, element, "center-middle");
+        attachTaskMarkers(parentGfx, element);
+
+        return rect;
+      },
+      "bpmn:ServiceTask": function (parentGfx, element) {
+        const task = renderer("bpmn:Task")(parentGfx, element);
+
+        const pathDataBG = pathMap.getScaledPath("TASK_TYPE_SERVICE", {
+          abspos: {
+            x: 12,
+            y: 18
+          }
+        });
+
+        /* service bg */ drawPath(parentGfx, pathDataBG, {
+          strokeWidth: 1,
+          fill: getFillColor(element, defaultFillColor),
+          stroke: getStrokeColor(element, defaultTaskColor)
+        });
+
+        const fillPathData = pathMap.getScaledPath("TASK_TYPE_SERVICE_FILL", {
+          abspos: {
+            x: 17.2,
+            y: 18
+          }
+        });
+
+        /* service fill */ drawPath(parentGfx, fillPathData, {
+          strokeWidth: 0,
+          fill: getFillColor(element, defaultFillColor)
+        });
+
+        const pathData = pathMap.getScaledPath("TASK_TYPE_SERVICE", {
+          abspos: {
+            x: 17,
+            y: 22
+          }
+        });
+
+        /* service */ drawPath(parentGfx, pathData, {
+          strokeWidth: 1,
+          fill: getFillColor(element, defaultFillColor),
+          stroke: getStrokeColor(element, defaultTaskColor)
+        });
+
+        return task;
+      },
+      "bpmn:UserTask": function (parentGfx, element) {
+        const task = renderer("bpmn:Task")(parentGfx, element);
+
+        const x = 15;
+        const y = 12;
+
+        const pathData = pathMap.getScaledPath("TASK_TYPE_USER_1", {
+          abspos: {
+            x: x,
+            y: y
+          }
+        });
+
+        /* user path */ drawPath(parentGfx, pathData, {
+          strokeWidth: 0.5,
+          fill: getFillColor(element, defaultFillColor),
+          stroke: getStrokeColor(element, defaultTaskColor)
+        });
+
+        const pathData2 = pathMap.getScaledPath("TASK_TYPE_USER_2", {
+          abspos: {
+            x: x,
+            y: y
+          }
+        });
+
+        /* user2 path */ drawPath(parentGfx, pathData2, {
+          strokeWidth: 0.5,
+          fill: getFillColor(element, defaultFillColor),
+          stroke: getStrokeColor(element, defaultTaskColor)
+        });
+
+        const pathData3 = pathMap.getScaledPath("TASK_TYPE_USER_3", {
+          abspos: {
+            x: x,
+            y: y
+          }
+        });
+
+        /* user3 path */ drawPath(parentGfx, pathData3, {
+          strokeWidth: 0.5,
+          fill: getStrokeColor(element, defaultTaskColor),
+          stroke: getStrokeColor(element, defaultTaskColor)
+        });
+
+        return task;
+      },
+      "bpmn:ManualTask": function (parentGfx, element) {
+        const task = renderer("bpmn:Task")(parentGfx, element);
+
+        const pathData = pathMap.getScaledPath("TASK_TYPE_MANUAL", {
+          abspos: {
+            x: 17,
+            y: 15
+          }
+        });
+
+        /* manual path */ drawPath(parentGfx, pathData, {
+          strokeWidth: 0.5, // 0.25,
+          fill: getFillColor(element, defaultFillColor),
+          stroke: getStrokeColor(element, defaultTaskColor)
+        });
+
+        return task;
+      },
+      "bpmn:SendTask": function (parentGfx, element) {
+        const task = renderer("bpmn:Task")(parentGfx, element);
+
+        const pathData = pathMap.getScaledPath("TASK_TYPE_SEND", {
+          xScaleFactor: 1,
+          yScaleFactor: 1,
+          containerWidth: 21,
+          containerHeight: 14,
+          position: {
+            mx: 0.285,
+            my: 0.357
+          }
+        });
+
+        /* send path */ drawPath(parentGfx, pathData, {
+          strokeWidth: 1,
+          fill: getStrokeColor(element, defaultTaskColor),
+          stroke: getFillColor(element, defaultFillColor)
+        });
+
+        return task;
+      },
+      "bpmn:ReceiveTask": function (parentGfx, element) {
+        const semantic = getSemantic(element);
+
+        const task = renderer("bpmn:Task")(parentGfx, element);
+        let pathData;
+
+        if (semantic.instantiate) {
+          drawCircle(parentGfx, 28, 28, 20 * 0.22, { strokeWidth: 1 });
+
+          pathData = pathMap.getScaledPath("TASK_TYPE_INSTANTIATING_SEND", {
+            abspos: {
+              x: 7.77,
+              y: 9.52
+            }
+          });
+        } else {
+          pathData = pathMap.getScaledPath("TASK_TYPE_SEND", {
+            xScaleFactor: 0.9,
+            yScaleFactor: 0.9,
+            containerWidth: 21,
+            containerHeight: 14,
+            position: {
+              mx: 0.3,
+              my: 0.4
+            }
+          });
+        }
+
+        /* receive path */ drawPath(parentGfx, pathData, {
+          strokeWidth: 1,
+          fill: getFillColor(element, defaultFillColor),
+          stroke: getStrokeColor(element, defaultTaskColor)
+        });
+
+        return task;
+      },
+      "bpmn:ScriptTask": function (parentGfx, element) {
+        const task = renderer("bpmn:Task")(parentGfx, element);
+
+        const pathData = pathMap.getScaledPath("TASK_TYPE_SCRIPT", {
+          abspos: {
+            x: 15,
+            y: 20
+          }
+        });
+
+        /* script path */ drawPath(parentGfx, pathData, {
+          strokeWidth: 1,
+          stroke: getStrokeColor(element, defaultTaskColor)
+        });
+
+        return task;
+      },
+      "bpmn:BusinessRuleTask": function (parentGfx, element) {
+        const task = renderer("bpmn:Task")(parentGfx, element);
+
+        const headerPathData = pathMap.getScaledPath("TASK_TYPE_BUSINESS_RULE_HEADER", {
+          abspos: {
+            x: 8,
+            y: 8
+          }
+        });
+
+        const businessHeaderPath = drawPath(parentGfx, headerPathData);
+        svgAttr(businessHeaderPath, {
+          strokeWidth: 1,
+          fill: getFillColor(element, "#aaaaaa"),
+          stroke: getStrokeColor(element, defaultTaskColor)
+        });
+
+        const headerData = pathMap.getScaledPath("TASK_TYPE_BUSINESS_RULE_MAIN", {
+          abspos: {
+            x: 8,
+            y: 8
+          }
+        });
+
+        const businessPath = drawPath(parentGfx, headerData);
+        svgAttr(businessPath, {
+          strokeWidth: 1,
+          stroke: getStrokeColor(element, defaultTaskColor)
+        });
+
+        return task;
+      },
+      "bpmn:SubProcess": function (parentGfx, element, attrs) {
+        attrs = assign(
+          {
+            fill: getFillColor(element, defaultFillColor),
+            stroke: getStrokeColor(element, defaultTaskColor)
+          },
+          attrs
+        );
+
+        const rect = renderer("bpmn:Activity")(parentGfx, element, attrs);
+
+        const expanded = isExpanded(element);
+
+        if (isEventSubProcess(element)) {
+          svgAttr(rect, {
+            strokeDasharray: "1,2"
+          });
+        }
+
+        renderEmbeddedLabel(parentGfx, element, expanded ? "center-top" : "center-middle");
+
+        if (expanded) {
+          attachTaskMarkers(parentGfx, element);
+        } else {
+          attachTaskMarkers(parentGfx, element, ["SubProcessMarker"]);
+        }
+
+        return rect;
+      },
+      "bpmn:AdHocSubProcess": function (parentGfx, element) {
+        return renderer("bpmn:SubProcess")(parentGfx, element);
+      },
+      "bpmn:Transaction": function (parentGfx, element) {
+        const outer = renderer("bpmn:SubProcess")(parentGfx, element);
+
+        const innerAttrs = styles.style(["no-fill", "no-events"], {
+          stroke: getStrokeColor(element, defaultTaskColor)
+        });
+
+        /* inner path */ drawRect(
+          parentGfx,
+          element.width,
+          element.height,
+          TASK_BORDER_RADIUS - 2,
+          INNER_OUTER_DIST,
+          innerAttrs
+        );
+
+        return outer;
+      },
+      "bpmn:CallActivity": function (parentGfx, element) {
+        return renderer("bpmn:SubProcess")(parentGfx, element, {
+          strokeWidth: 5
+        });
+      },
+      "bpmn:Participant": function (parentGfx, element) {
+        const attrs = {
+          fillOpacity: DEFAULT_FILL_OPACITY,
+          fill: getFillColor(element, defaultFillColor),
+          stroke: getStrokeColor(element, defaultTaskColor)
+        };
+
+        const lane = renderer("bpmn:Lane")(parentGfx, element, attrs);
+
+        const expandedPool = isExpanded(element);
+
+        if (expandedPool) {
+          drawLine(
+            parentGfx,
+            [
+              { x: 30, y: 0 },
+              { x: 30, y: element.height }
+            ],
+            {
+              stroke: getStrokeColor(element, defaultTaskColor)
+            }
+          );
+          const text = getSemantic(element).name;
+          renderLaneLabel(parentGfx, text, element);
+        } else {
+          // Collapsed pool draw text inline
+          const text2 = getSemantic(element).name;
+          renderLabel(parentGfx, text2, {
+            box: element,
+            align: "center-middle",
+            style: {
+              fill: getLabelColor(element, defaultLabelColor, defaultTaskColor)
+            }
+          });
+        }
+
+        const participantMultiplicity = !!getSemantic(element).participantMultiplicity;
+
+        if (participantMultiplicity) {
+          renderer("ParticipantMultiplicityMarker")(parentGfx, element);
+        }
+
+        return lane;
+      },
+      "bpmn:Lane": function (parentGfx, element, attrs) {
+        const rect = drawRect(
+          parentGfx,
+          element.width,
+          element.height,
+          0,
+          assign(
+            {
+              fill: getFillColor(element, defaultFillColor),
+              fillOpacity: HIGH_FILL_OPACITY,
+              stroke: getStrokeColor(element, defaultTaskColor)
+            },
+            attrs
+          )
+        );
+
+        const semantic = getSemantic(element);
+
+        if (semantic.$type === "bpmn:Lane") {
+          const text = semantic.name;
+          renderLaneLabel(parentGfx, text, element);
+        }
+
+        return rect;
+      },
+      "bpmn:InclusiveGateway": function (parentGfx, element) {
+        const diamond = renderer("bpmn:Gateway")(parentGfx, element);
+
+        /* circle path */
+        drawCircle(parentGfx, element.width, element.height, element.height * 0.24, {
+          strokeWidth: 2.5,
+          fill: getStrokeColor(element, defaultGatewayColor),
+          stroke: getStrokeColor(element, defaultGatewayColor)
+        });
+
+        return diamond;
+      },
+      "bpmn:ExclusiveGateway": function (parentGfx, element) {
+        const diamond = renderer("bpmn:Gateway")(parentGfx, element);
+
+        const pathData = pathMap.getScaledPath("GATEWAY_EXCLUSIVE", {
+          xScaleFactor: 0.4,
+          yScaleFactor: 0.4,
+          containerWidth: element.width,
+          containerHeight: element.height,
+          position: {
+            mx: 0.32,
+            my: 0.3
+          }
+        });
+
+        if (getDi(element).isMarkerVisible) {
+          drawPath(parentGfx, pathData, {
+            strokeWidth: 1,
+            fill: getStrokeColor(element, defaultGatewayColor),
+            stroke: getStrokeColor(element, defaultGatewayColor)
+          });
+        }
+
+        return diamond;
+      },
+      "bpmn:ComplexGateway": function (parentGfx, element) {
+        const diamond = renderer("bpmn:Gateway")(parentGfx, element);
+
+        const pathData = pathMap.getScaledPath("GATEWAY_COMPLEX", {
+          xScaleFactor: 0.5,
+          yScaleFactor: 0.5,
+          containerWidth: element.width,
+          containerHeight: element.height,
+          position: {
+            mx: 0.46,
+            my: 0.26
+          }
+        });
+
+        /* complex path */ drawPath(parentGfx, pathData, {
+          strokeWidth: 1,
+          fill: getStrokeColor(element, defaultGatewayColor),
+          stroke: getStrokeColor(element, defaultGatewayColor)
+        });
+
+        return diamond;
+      },
+      "bpmn:ParallelGateway": function (parentGfx, element) {
+        const diamond = renderer("bpmn:Gateway")(parentGfx, element);
+
+        const pathData = pathMap.getScaledPath("GATEWAY_PARALLEL", {
+          xScaleFactor: 0.6,
+          yScaleFactor: 0.6,
+          containerWidth: element.width,
+          containerHeight: element.height,
+          position: {
+            mx: 0.46,
+            my: 0.2
+          }
+        });
+
+        /* parallel path */ drawPath(parentGfx, pathData, {
+          strokeWidth: 1,
+          fill: getStrokeColor(element, defaultGatewayColor),
+          stroke: getStrokeColor(element, defaultGatewayColor)
+        });
+
+        return diamond;
+      },
+      "bpmn:EventBasedGateway": function (parentGfx, element) {
+        const semantic = getSemantic(element);
+
+        const diamond = renderer("bpmn:Gateway")(parentGfx, element);
+
+        /* outer circle path */
+        drawCircle(parentGfx, element.width, element.height, element.height * 0.2, {
+          strokeWidth: 1,
+          fill: "none",
+          stroke: getStrokeColor(element, defaultGatewayColor)
+        });
+
+        const type = semantic.eventGatewayType;
+        const instantiate = !!semantic.instantiate;
+
+        function drawEvent() {
+          const pathData = pathMap.getScaledPath("GATEWAY_EVENT_BASED", {
+            xScaleFactor: 0.18,
+            yScaleFactor: 0.18,
+            containerWidth: element.width,
+            containerHeight: element.height,
+            position: {
+              mx: 0.36,
+              my: 0.44
+            }
+          });
+
+          const attrs = {
+            strokeWidth: 2,
+            fill: getFillColor(element, "none"),
+            stroke: getStrokeColor(element, defaultGatewayColor)
+          };
+          drawPath(parentGfx, pathData, attrs);
+        }
+
+        if (type === "Parallel") {
+          const pathData = pathMap.getScaledPath("GATEWAY_PARALLEL", {
+            xScaleFactor: 0.4,
+            yScaleFactor: 0.4,
+            containerWidth: element.width,
+            containerHeight: element.height,
+            position: {
+              mx: 0.474,
+              my: 0.296
+            }
+          });
+
+          const parallelPath = drawPath(parentGfx, pathData);
+          svgAttr(parallelPath, {
+            strokeWidth: 1,
+            fill: "none"
+          });
+        } else if (type === "Exclusive") {
+          if (!instantiate) {
+            const innerCircle = drawCircle(parentGfx, element.width, element.height, element.height * 0.26);
+            svgAttr(innerCircle, {
+              strokeWidth: 1,
+              fill: "none",
+              stroke: getStrokeColor(element, defaultGatewayColor)
+            });
+          }
+
+          drawEvent();
+        }
+
+        return diamond;
+      },
+      "bpmn:Gateway": function (parentGfx, element) {
+        const attrs = {
+          fill: getFillColor(element, defaultGatewayColor),
+          fillOpacity: defaultGatewayOpacity,
+          stroke: getStrokeColor(element, defaultGatewayColor)
+        };
+
+        return drawDiamond(parentGfx, element.width, element.height, attrs);
+      },
+      "bpmn:SequenceFlow": function (parentGfx, element) {
+        const pathData = createPathFromConnection(element);
+        const fill = getFillColor(element, defaultFillColor),
+          stroke = getStrokeColor(element, defaultSequenceColor);
+        const attrs = {
+          strokeLinejoin: "round",
+          markerEnd: marker("sequenceflow-end", fill, stroke),
+          stroke: getStrokeColor(element, defaultSequenceColor)
+        };
+        const path = drawPath(parentGfx, pathData, attrs);
+        const sequenceflow = getSemantic(element);
+        let source;
+        if (element.source) {
+          source = element.source.businessObject;
+          if (sequenceflow.conditionExpression && source.$instanceOf("bpmn:Activity")) {
+            svgAttr(path, {
+              markerStart: marker("conditional-flow-marker", fill, stroke)
+            });
+          }
+          if (
+            source.default &&
+            (source.$instanceOf("bpmn:Gateway") || source.$instanceOf("bpmn:Activity")) &&
+            source.default === sequenceflow
+          ) {
+            svgAttr(path, {
+              markerStart: marker("conditional-default-flow-marker", fill, stroke)
+            });
+          }
+        }
+
+        return path;
+      },
+      "bpmn:Association": function (parentGfx, element, attrs) {
+        const semantic = getSemantic(element);
+
+        const fill = getFillColor(element, defaultFillColor),
+          stroke = getStrokeColor(element, defaultTaskColor);
+
+        attrs = assign(
+          {
+            strokeDasharray: "0.5, 5",
+            strokeLinecap: "round",
+            strokeLinejoin: "round",
+            stroke: getStrokeColor(element, defaultTaskColor)
+          },
+          attrs || {}
+        );
+
+        if (semantic.associationDirection === "One" || semantic.associationDirection === "Both") {
+          attrs.markerEnd = marker("association-end", fill, stroke);
+        }
+
+        if (semantic.associationDirection === "Both") {
+          attrs.markerStart = marker("association-start", fill, stroke);
+        }
+
+        return drawLine(parentGfx, element.waypoints, attrs);
+      },
+      "bpmn:DataInputAssociation": function (parentGfx, element) {
+        const fill = getFillColor(element, defaultFillColor),
+          stroke = getStrokeColor(element, defaultTaskColor);
+
+        return renderer("bpmn:Association")(parentGfx, element, {
+          markerEnd: marker("association-end", fill, stroke)
+        });
+      },
+      "bpmn:DataOutputAssociation": function (parentGfx, element) {
+        const fill = getFillColor(element, defaultFillColor),
+          stroke = getStrokeColor(element, defaultTaskColor);
+
+        return renderer("bpmn:Association")(parentGfx, element, {
+          markerEnd: marker("association-end", fill, stroke)
+        });
+      },
+      "bpmn:MessageFlow": function (parentGfx, element) {
+        const semantic = getSemantic(element),
+          di = getDi(element);
+
+        const fill = getFillColor(element, defaultFillColor),
+          stroke = getStrokeColor(element, defaultTaskColor);
+
+        const pathData = createPathFromConnection(element);
+
+        const attrs = {
+          markerEnd: marker("messageflow-end", fill, stroke),
+          markerStart: marker("messageflow-start", fill, stroke),
+          strokeDasharray: "10, 12",
+          strokeLinecap: "round",
+          strokeLinejoin: "round",
+          strokeWidth: "1.5px",
+          stroke: getStrokeColor(element, defaultTaskColor)
+        };
+
+        const path = drawPath(parentGfx, pathData, attrs);
+
+        if (semantic.messageRef) {
+          const midPoint = path.getPointAtLength(path.getTotalLength() / 2);
+
+          const markerPathData = pathMap.getScaledPath("MESSAGE_FLOW_MARKER", {
+            abspos: {
+              x: midPoint.x,
+              y: midPoint.y
+            }
+          });
+
+          const messageAttrs = { strokeWidth: 1 };
+
+          if (di.messageVisibleKind === "initiating") {
+            messageAttrs.fill = "white";
+            messageAttrs.stroke = "black";
+          } else {
+            messageAttrs.fill = "#888";
+            messageAttrs.stroke = "white";
+          }
+
+          const message = drawPath(parentGfx, markerPathData, messageAttrs);
+
+          const labelText = semantic.messageRef.name;
+          const label = renderLabel(parentGfx, labelText, {
+            align: "center-top",
+            fitBox: true,
+            style: {
+              fill: getStrokeColor(element, defaultLabelColor, defaultTaskColor)
+            }
+          });
+
+          const messageBounds = message.getBBox(),
+            labelBounds = label.getBBox();
+
+          const translateX = midPoint.x - labelBounds.width / 2,
+            translateY = midPoint.y + messageBounds.height / 2 + ELEMENT_LABEL_DISTANCE;
+
+          transform(label, translateX, translateY, 0);
+        }
+
+        return path;
+      },
+      "bpmn:DataObject": function (parentGfx, element) {
+        const pathData = pathMap.getScaledPath("DATA_OBJECT_PATH", {
+          xScaleFactor: 1,
+          yScaleFactor: 1,
+          containerWidth: element.width,
+          containerHeight: element.height,
+          position: {
+            mx: 0.474,
+            my: 0.296
+          }
+        });
+
+        const elementObject = drawPath(parentGfx, pathData, {
+          fill: getFillColor(element, defaultFillColor),
+          fillOpacity: DEFAULT_FILL_OPACITY,
+          stroke: getStrokeColor(element, defaultTaskColor)
+        });
+
+        const semantic = getSemantic(element);
+
+        if (isCollection(semantic)) {
+          renderDataItemCollection(parentGfx, element);
+        }
+
+        return elementObject;
+      },
+      "bpmn:DataObjectReference": as("bpmn:DataObject"),
+      "bpmn:DataInput": function (parentGfx, element) {
+        const arrowPathData = pathMap.getRawPath("DATA_ARROW");
+
+        // page
+        const elementObject = renderer("bpmn:DataObject")(parentGfx, element);
+
+        /* input arrow path */ drawPath(parentGfx, arrowPathData, { strokeWidth: 1 });
+
+        return elementObject;
+      },
+      "bpmn:DataOutput": function (parentGfx, element) {
+        const arrowPathData = pathMap.getRawPath("DATA_ARROW");
+
+        // page
+        const elementObject = renderer("bpmn:DataObject")(parentGfx, element);
+
+        /* output arrow path */ drawPath(parentGfx, arrowPathData, {
+          strokeWidth: 1,
+          fill: "black"
+        });
+
+        return elementObject;
+      },
+      "bpmn:DataStoreReference": function (parentGfx, element) {
+        const DATA_STORE_PATH = pathMap.getScaledPath("DATA_STORE", {
+          xScaleFactor: 1,
+          yScaleFactor: 1,
+          containerWidth: element.width,
+          containerHeight: element.height,
+          position: {
+            mx: 0,
+            my: 0.133
+          }
+        });
+
+        return drawPath(parentGfx, DATA_STORE_PATH, {
+          strokeWidth: 2,
+          fill: getFillColor(element, defaultFillColor),
+          fillOpacity: DEFAULT_FILL_OPACITY,
+          stroke: getStrokeColor(element, defaultTaskColor)
+        });
+      },
+      "bpmn:BoundaryEvent": function (parentGfx, element) {
+        const semantic = getSemantic(element),
+          cancel = semantic.cancelActivity;
+
+        const attrs = {
+          strokeWidth: 1,
+          fill: getFillColor(element, defaultFillColor),
+          stroke: getStrokeColor(element, defaultTaskColor)
+        };
+
+        if (!cancel) {
+          attrs.strokeDasharray = "6";
+          attrs.strokeLinecap = "round";
+        }
+
+        // apply fillOpacity
+        const outerAttrs = assign({}, attrs, {
+          fillOpacity: 1
+        });
+
+        // apply no-fill
+        const innerAttrs = assign({}, attrs, {
+          fill: "none"
+        });
+
+        const outer = renderer("bpmn:Event")(parentGfx, element, outerAttrs);
+
+        /* inner path */ drawCircle(parentGfx, element.width, element.height, INNER_OUTER_DIST, innerAttrs);
+
+        renderEventContent(element, parentGfx);
+
+        return outer;
+      },
+      "bpmn:Group": function (parentGfx, element) {
+        return drawRect(parentGfx, element.width, element.height, TASK_BORDER_RADIUS, {
+          stroke: getStrokeColor(element, defaultTaskColor),
+          strokeWidth: 1,
+          strokeDasharray: "8,3,1,3",
+          fill: "none",
+          pointerEvents: "none"
+        });
+      },
+      label: function (parentGfx, element) {
+        return renderExternalLabel(parentGfx, element);
+      },
+      "bpmn:TextAnnotation": function (parentGfx, element) {
+        const style = {
+          fill: "none",
+          stroke: "none"
+        };
+
+        const textElement = drawRect(parentGfx, element.width, element.height, 0, 0, style);
+
+        const textPathData = pathMap.getScaledPath("TEXT_ANNOTATION", {
+          xScaleFactor: 1,
+          yScaleFactor: 1,
+          containerWidth: element.width,
+          containerHeight: element.height,
+          position: {
+            mx: 0.0,
+            my: 0.0
+          }
+        });
+
+        drawPath(parentGfx, textPathData, {
+          stroke: getStrokeColor(element, defaultTaskColor)
+        });
+
+        const text = getSemantic(element).text || "";
+        renderLabel(parentGfx, text, {
+          box: element,
+          align: "left-top",
+          padding: 5,
+          style: {
+            fill: getLabelColor(element, defaultLabelColor, defaultTaskColor)
+          }
+        });
+
+        return textElement;
+      },
+      ParticipantMultiplicityMarker: function (parentGfx, element) {
+        const markerPath = pathMap.getScaledPath("MARKER_PARALLEL", {
+          xScaleFactor: 1,
+          yScaleFactor: 1,
+          containerWidth: element.width,
+          containerHeight: element.height,
+          position: {
+            mx: element.width / 2 / element.width,
+            my: (element.height - 15) / element.height
+          }
+        });
+
+        return drawMarker("participant-multiplicity", parentGfx, markerPath, {
+          strokeWidth: 2,
+          fill: getFillColor(element, defaultFillColor),
+          stroke: getStrokeColor(element, defaultTaskColor)
+        });
+      },
+      SubProcessMarker: function (parentGfx, element) {
+        const markerRect = drawRect(parentGfx, 14, 14, 0, {
+          strokeWidth: 1,
+          fill: getFillColor(element, defaultFillColor),
+          stroke: getStrokeColor(element, defaultTaskColor)
+        });
+
+        // Process marker is placed in the middle of the box
+        // therefore fixed values can be used here
+        translate(markerRect, element.width / 2 - 7.5, element.height - 20);
+
+        const markerPath = pathMap.getScaledPath("MARKER_SUB_PROCESS", {
+          xScaleFactor: 1.5,
+          yScaleFactor: 1.5,
+          containerWidth: element.width,
+          containerHeight: element.height,
+          position: {
+            mx: (element.width / 2 - 7.5) / element.width,
+            my: (element.height - 20) / element.height
+          }
+        });
+
+        return drawMarker("sub-process", parentGfx, markerPath, {
+          fill: getFillColor(element, defaultFillColor),
+          stroke: getStrokeColor(element, defaultTaskColor)
+        });
+      },
+      ParallelMarker: function (parentGfx, element, position) {
+        const markerPath = pathMap.getScaledPath("MARKER_PARALLEL", {
+          xScaleFactor: 1,
+          yScaleFactor: 1,
+          containerWidth: element.width,
+          containerHeight: element.height,
+          position: {
+            mx: (element.width / 2 + position.parallel) / element.width,
+            my: (element.height - 20) / element.height
+          }
+        });
+
+        return drawMarker("parallel", parentGfx, markerPath, {
+          fill: getFillColor(element, defaultFillColor),
+          stroke: getStrokeColor(element, defaultTaskColor)
+        });
+      },
+      SequentialMarker: function (parentGfx, element, position) {
+        const markerPath = pathMap.getScaledPath("MARKER_SEQUENTIAL", {
+          xScaleFactor: 1,
+          yScaleFactor: 1,
+          containerWidth: element.width,
+          containerHeight: element.height,
+          position: {
+            mx: (element.width / 2 + position.seq) / element.width,
+            my: (element.height - 19) / element.height
+          }
+        });
+
+        return drawMarker("sequential", parentGfx, markerPath, {
+          fill: getFillColor(element, defaultFillColor),
+          stroke: getStrokeColor(element, defaultTaskColor)
+        });
+      },
+      CompensationMarker: function (parentGfx, element, position) {
+        const markerMath = pathMap.getScaledPath("MARKER_COMPENSATION", {
+          xScaleFactor: 1,
+          yScaleFactor: 1,
+          containerWidth: element.width,
+          containerHeight: element.height,
+          position: {
+            mx: (element.width / 2 + position.compensation) / element.width,
+            my: (element.height - 13) / element.height
+          }
+        });
+
+        return drawMarker("compensation", parentGfx, markerMath, {
+          strokeWidth: 1,
+          fill: getFillColor(element, defaultFillColor),
+          stroke: getStrokeColor(element, defaultTaskColor)
+        });
+      },
+      LoopMarker: function (parentGfx, element, position) {
+        const markerPath = pathMap.getScaledPath("MARKER_LOOP", {
+          xScaleFactor: 1,
+          yScaleFactor: 1,
+          containerWidth: element.width,
+          containerHeight: element.height,
+          position: {
+            mx: (element.width / 2 + position.loop) / element.width,
+            my: (element.height - 7) / element.height
+          }
+        });
+
+        return drawMarker("loop", parentGfx, markerPath, {
+          strokeWidth: 1,
+          fill: getFillColor(element, defaultFillColor),
+          stroke: getStrokeColor(element, defaultTaskColor),
+          strokeLinecap: "round",
+          strokeMiterlimit: 0.5
+        });
+      },
+      AdhocMarker: function (parentGfx, element, position) {
+        const markerPath = pathMap.getScaledPath("MARKER_ADHOC", {
+          xScaleFactor: 1,
+          yScaleFactor: 1,
+          containerWidth: element.width,
+          containerHeight: element.height,
+          position: {
+            mx: (element.width / 2 + position.adhoc) / element.width,
+            my: (element.height - 15) / element.height
+          }
+        });
+
+        return drawMarker("adhoc", parentGfx, markerPath, {
+          strokeWidth: 1,
+          fill: getStrokeColor(element, defaultTaskColor),
+          stroke: getStrokeColor(element, defaultTaskColor)
+        });
+      },
+
+      // 自定义节点的绘制
+      "miyue:SqlTask": function (parentGfx, element, attr) {
+        // 渲染外层边框
+        const attrs = {
+          fill: getFillColor(element, defaultFillColor),
+          fillOpacity: defaultTaskOpacity,
+          stroke: getStrokeColor(element, defaultTaskColor)
+        };
+        renderer("bpmn:Activity")(parentGfx, element, attrs);
+        // 自定义节点
+        const customIcon = svgCreate("image");
+        svgAttr(customIcon, {
+          ...(attr || {}),
+          width: element.width,
+          height: element.height,
+          href: mysqlIcon
+        });
+        svgAppend(parentGfx, customIcon);
+        return customIcon;
+      }
+    });
+  }
+
+  canRender(element) {
+    return is(element, "bpmn:BaseElement");
+  }
+  drawConnection(parentGfx, connection) {
+    const type = connection.type;
+    const h = this._renderer(type);
+    return h(parentGfx, connection);
+  }
+  drawShape(parentGfx, element) {
+    const type = element.type;
+    const h = this._renderer(type);
+    return h(parentGfx, element);
+  }
+  getConnectionPath(connection) {
+    return undefined;
+  }
+  getShapePath(shape) {
+    if (is(shape, "bpmn:Event")) {
+      return getCirclePath(shape);
+    }
+
+    if (is(shape, "bpmn:Activity")) {
+      return getRoundRectPath(shape, TASK_BORDER_RADIUS);
+    }
+
+    if (is(shape, "bpmn:Gateway")) {
+      return getDiamondPath(shape);
+    }
+
+    return getRectPath(shape);
+  }
+
+  setElementColors(element, colors) {
+    const svg = this._elementRegistry?.getGraphics(element);
+    if (!svg) return;
+    const paths = svgSelect(svg, ".djs-visual");
+    // @ts-ignore
+    paths && paths.childNodes.forEach((el) => svgAttr(el, colors));
+  }
+}
+
+RewriteRenderer.$inject = [
+  "config.bpmnRenderer",
+  "eventBus",
+  "styles",
+  "pathMap",
+  "canvas",
+  "textRenderer",
+  "elementRegistry",
+  "interactionEvents"
+];
+
+export default RewriteRenderer;

+ 8 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Renderer/RewriteRenderer/index.js

@@ -0,0 +1,8 @@
+import RewriteRenderer from "./RewriteRenderer";
+
+const rewriteRenderer = {
+  __init__: ["bpmnRenderer"],
+  bpmnRenderer: ["type", RewriteRenderer]
+};
+
+export default rewriteRenderer;

文件差異過大導致無法顯示
+ 2 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Renderer/RewriteRenderer/rewritePaths.js


+ 59 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Renderer/utils.js

@@ -0,0 +1,59 @@
+import { every, some, isObject } from "min-dash";
+import { append as svgAppend, attr as svgAttr, create as svgCreate } from "tiny-svg";
+
+//////////////////// 校验部分
+export function isTypedEvent(event, eventDefinitionType, filter) {
+  function matches(definition, filter) {
+    return every(filter, function (val, key) {
+      return definition[key] === val;
+    });
+  }
+  return some(event.eventDefinitions, function (definition) {
+    return definition.$type === eventDefinitionType && matches(event, filter);
+  });
+}
+
+export function getSemantic(element) {
+  return element.businessObject;
+}
+
+export function isThrowEvent(event) {
+  return event.$type === "bpmn:IntermediateThrowEvent" || event.$type === "bpmn:EndEvent";
+}
+
+//////////////////// svg 图形绘制部分
+// 绘制圆形
+export function drawCircle(renderer, parentGfx, width, height, offset, attrs) {
+  if (isObject(offset)) {
+    attrs = offset;
+    offset = 0;
+  }
+  offset = offset || 0;
+  attrs = renderer._styles.computeStyle(attrs);
+  if (attrs.fill === "none") {
+    delete attrs.fillOpacity;
+  }
+  const cx = width / 2,
+    cy = height / 2;
+  const circle = svgCreate("circle");
+  svgAttr(circle, {
+    cx: cx,
+    cy: cy,
+    r: Math.round((width + height) / 4 - offset)
+  });
+  svgAttr(circle, attrs);
+  svgAppend(parentGfx, circle);
+  return circle;
+}
+// 绘制路径元素
+export function drawPath(renderer, parentGfx, d, attrs) {
+  attrs = renderer._styles.computeStyle(attrs, ["no-fill"], {
+    strokeWidth: 2,
+    stroke: "black"
+  });
+  const path = svgCreate("path");
+  svgAttr(path, { d: d });
+  svgAttr(path, attrs);
+  svgAppend(parentGfx, path);
+  return path;
+}

+ 21 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Rules/CustomRules.js

@@ -0,0 +1,21 @@
+import RuleProvider from "diagram-js/lib/features/rules/RuleProvider";
+
+class CustomRules extends RuleProvider {
+  constructor(eventBus) {
+    super(eventBus);
+    this.init();
+  }
+
+  init() {
+    // 禁止删除开始和结束
+    this.addRule(["elements.delete"], 2000, function (context) {
+      const [element] = context.elements;
+      return element.type !== "bpmn:StartEvent" && element.type !== "bpmn:EndEvent";
+    });
+  }
+}
+
+// @ts-ignore
+CustomRules.$inject = ["eventBus"];
+
+export default CustomRules;

+ 6 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Rules/index.js

@@ -0,0 +1,6 @@
+import CustomRules from "./CustomRules";
+
+export default {
+  __init__: ["customRules"],
+  customRules: ["type", CustomRules]
+};

+ 17 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Translate/index.js

@@ -0,0 +1,17 @@
+import zhCN from "./zh-cn";
+
+export function customTranslate(template, replacements) {
+  replacements = replacements || {};
+
+  // Translate
+  template = zhCN[template] || template;
+
+  // Replace
+  return template.replace(/{([^}]+)}/g, function (_, key) {
+    return replacements[key] || "{" + key + "}";
+  });
+}
+
+export default {
+  translate: ["value", customTranslate]
+};

+ 48 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Translate/zh-cn/events.js

@@ -0,0 +1,48 @@
+export default {
+  StartEvent: "开始事件",
+  EndEvent: "结束事件",
+  IntermediateThrowEvent: "中间抛出事件",
+  IntermediateCatchEvent: "中间捕获事件",
+  "Message Start Event": "消息开始事件",
+  "Timer Start Event": "定时开始事件",
+  "Conditional Start Event": "条件开始事件",
+  "Signal Start Event": "信号开始事件",
+  "Error Start Event": "错误开始事件",
+  "Escalation Start Event": "升级开始事件",
+  "Compensation Start Event": "补偿开始事件",
+  "Message Start Event (non-interrupting)": "消息开始事件(非中断)",
+  "Timer Start Event (non-interrupting)": "定时开始事件(非中断)",
+  "Conditional Start Event (non-interrupting)": "条件开始事件(非中断)",
+  "Signal Start Event (non-interrupting)": "信号开始事件(非中断)",
+  "Escalation Start Event (non-interrupting)": "升级开始事件(非中断)",
+  "Message Intermediate Catch Event": "消息中间捕获事件",
+  "Message Intermediate Throw Event": "消息中间抛出事件",
+  "Timer Intermediate Catch Event": "定时中间捕获事件",
+  "Escalation Intermediate Throw Event": "升级中间抛出事件",
+  "Conditional Intermediate Catch Event": "条件中间捕获事件",
+  "Link Intermediate Catch Event": "链接中间捕获事件",
+  "Link Intermediate Throw Event": "链接中间抛出事件",
+  "Compensation Intermediate Throw Event": "补偿中间抛出事件",
+  "Signal Intermediate Catch Event": "信号中间捕获事件",
+  "Signal Intermediate Throw Event": "信号中间抛出事件",
+  "Message End Event": "消息结束事件",
+  "Escalation End Event": "定时结束事件",
+  "Error End Event": "错误结束事件",
+  "Cancel End Event": "取消结束事件",
+  "Compensation End Event": "补偿结束事件",
+  "Signal End Event": "信号结束事件",
+  "Terminate End Event": "终止结束事件",
+  "Message Boundary Event": "消息边界事件",
+  "Message Boundary Event (non-interrupting)": "消息边界事件(非中断)",
+  "Timer Boundary Event": "定时边界事件",
+  "Timer Boundary Event (non-interrupting)": "定时边界事件(非中断)",
+  "Escalation Boundary Event": "升级边界事件",
+  "Escalation Boundary Event (non-interrupting)": "升级边界事件(非中断)",
+  "Conditional Boundary Event": "条件边界事件",
+  "Conditional Boundary Event (non-interrupting)": "条件边界事件(非中断)",
+  "Error Boundary Event": "错误边界事件",
+  "Cancel Boundary Event": "取消边界事件",
+  "Signal Boundary Event": "信号边界事件",
+  "Signal Boundary Event (non-interrupting)": "信号边界事件(非中断)",
+  "Compensation Boundary Event": "补偿边界事件"
+};

+ 13 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Translate/zh-cn/gateway.js

@@ -0,0 +1,13 @@
+export default {
+  Gateway: "网关",
+  "Exclusive Gateway": "互斥网关",
+  "Parallel Gateway": "并行网关",
+  "Inclusive Gateway": "相容网关",
+  "Complex Gateway": "复杂网关",
+  "Event based Gateway": "事件网关",
+  ExclusiveGateway: "互斥网关",
+  ParallelGateway: "并行网关",
+  InclusiveGateway: "相容网关",
+  ComplexGateway: "复杂网关",
+  EventBasedGateway: "事件网关"
+};

+ 13 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Translate/zh-cn/index.js

@@ -0,0 +1,13 @@
+import tasks from "./tasks";
+import events from "./events";
+import gateway from "./gateway";
+import lint from "./lint";
+import other from "./other";
+
+export default {
+  ...other,
+  ...events,
+  ...gateway,
+  ...lint,
+  ...tasks
+};

+ 4 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Translate/zh-cn/lint.js

@@ -0,0 +1,4 @@
+export default {
+  Errors: "错误",
+  Warnings: "警告"
+};

+ 186 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Translate/zh-cn/other.js

@@ -0,0 +1,186 @@
+export default {
+  Process: "业务流程",
+  "Append EndEvent": "追加结束事件",
+  "Append Gateway": "追加网关",
+  "Append Task": "追加任务",
+  "Append Intermediate/Boundary Event": "追加中间抛出事件/边界事件",
+  "Activate the global connect tool": "激活全局连接工具",
+  "Append {type}": "添加 {type}",
+  "Add Lane above": "在上面添加道",
+  "Divide into two Lanes": "分割成两个道",
+  "Divide into three Lanes": "分割成三个道",
+  "Add Lane below": "在下面添加道",
+  "Append compensation activity": "追加补偿活动",
+  "Change type": "修改类型",
+  "Connect using Association": "使用关联连接",
+  "Connect using Sequence/MessageFlow or Association": "使用顺序/消息流或者关联连接",
+  "Connect using DataInputAssociation": "使用数据输入关联连接",
+  Remove: "移除",
+  "Activate the hand tool": "激活抓手工具",
+  "Activate the lasso tool": "激活套索工具",
+  "Activate the create/remove space tool": "激活创建/删除空间工具",
+  "Create expanded SubProcess": "创建扩展子过程",
+  "Create IntermediateThrowEvent/BoundaryEvent": "创建中间抛出事件/边界事件",
+  "Create Pool/Participant": "创建池/参与者",
+  "Parallel Multi Instance": "并行多重事件",
+  "Sequential Multi Instance": "时序多重事件",
+  DataObjectReference: "数据对象参考",
+  DataStoreReference: "数据存储参考",
+  Loop: "循环",
+  "Ad-hoc": "即席",
+  "Create {type}": "创建 {type}",
+  "Create StartEvent": "创建开始事件",
+  "Create EndEvent": "创建结束事件",
+  "Create Task": "创建任务",
+  "Create User Task": "创建用户任务",
+  "Create Gateway": "创建网关",
+  "Create DataObjectReference": "创建数据对象",
+  "Create DataStoreReference": "创建数据存储",
+  "Create Group": "创建分组",
+  "Create Intermediate/Boundary Event": "创建中间/边界事件",
+  "Message Start Event": "消息开始事件",
+  "Timer Start Event": "定时开始事件",
+  "Conditional Start Event": "条件开始事件",
+  "Signal Start Event": "信号开始事件",
+  "Error Start Event": "错误开始事件",
+  "Escalation Start Event": "升级开始事件",
+  "Compensation Start Event": "补偿开始事件",
+  "Message Start Event (non-interrupting)": "消息开始事件(非中断)",
+  "Timer Start Event (non-interrupting)": "定时开始事件(非中断)",
+  "Conditional Start Event (non-interrupting)": "条件开始事件(非中断)",
+  "Signal Start Event (non-interrupting)": "信号开始事件(非中断)",
+  "Escalation Start Event (non-interrupting)": "升级开始事件(非中断)",
+  "Message Intermediate Catch Event": "消息中间捕获事件",
+  "Message Intermediate Throw Event": "消息中间抛出事件",
+  "Timer Intermediate Catch Event": "定时中间捕获事件",
+  "Escalation Intermediate Throw Event": "升级中间抛出事件",
+  "Conditional Intermediate Catch Event": "条件中间捕获事件",
+  "Link Intermediate Catch Event": "链接中间捕获事件",
+  "Link Intermediate Throw Event": "链接中间抛出事件",
+  "Compensation Intermediate Throw Event": "补偿中间抛出事件",
+  "Signal Intermediate Catch Event": "信号中间捕获事件",
+  "Signal Intermediate Throw Event": "信号中间抛出事件",
+  "Collapsed Pool": "折叠池",
+  "Expanded Pool": "展开池",
+  "no parent for {element} in {parent}": "在{parent}里,{element}没有父类",
+  "no shape type specified": "没有指定的形状类型",
+  "flow elements must be children of pools/participants": "流元素必须是池/参与者的子类",
+  "out of bounds release": "out of bounds release",
+  "more than {count} child lanes": "子道大于{count} ",
+  "element required": "元素不能为空",
+  "diagram not part of bpmn:Definitions": "流程图不符合bpmn规范",
+  "no diagram to display": "没有可展示的流程图",
+  "no process or collaboration to display": "没有可展示的流程/协作",
+  "element {element} referenced by {referenced}#{property} not yet drawn":
+    "由{referenced}#{property}引用的{element}元素仍未绘制",
+  "already rendered {element}": "{element} 已被渲染",
+  "failed to import {element}": "导入{element}失败",
+  Id: "编号",
+  Name: "名称",
+  General: "常规",
+  Details: "详情",
+  "Message Name": "消息名称",
+  Message: "消息",
+  Initiator: "创建者",
+  "Asynchronous Continuations": "持续异步",
+  "Asynchronous Before": "异步前",
+  "Asynchronous After": "异步后",
+  "Job Configuration": "工作配置",
+  Exclusive: "排除",
+  "Job Priority": "工作优先级",
+  "Retry Time Cycle": "重试时间周期",
+  Documentation: "文档",
+  "Element Documentation": "元素文档",
+  "History Configuration": "历史配置",
+  "History Time To Live": "历史的生存时间",
+  Forms: "表单",
+  "Form Key": "表单key",
+  "Form Fields": "表单字段",
+  "Business Key": "业务key",
+  "Form Field": "表单字段",
+  ID: "编号",
+  Type: "类型",
+  Label: "名称",
+  "Default Value": "默认值",
+  "Default Flow": "默认流转路径",
+  "Conditional Flow": "条件流转路径",
+  "Sequence Flow": "普通流转路径",
+  Validation: "校验",
+  "Add Constraint": "添加约束",
+  Config: "配置",
+  Properties: "属性",
+  "Add Property": "添加属性",
+  Value: "值",
+  Listeners: "监听器",
+  "Execution Listener": "执行监听",
+  "Event Type": "事件类型",
+  "Listener Type": "监听器类型",
+  "Java Class": "Java类",
+  Expression: "表达式",
+  "Must provide a value": "必须提供一个值",
+  "Delegate Expression": "代理表达式",
+  Script: "脚本",
+  "Script Format": "脚本格式",
+  "Script Type": "脚本类型",
+  "Inline Script": "内联脚本",
+  "External Script": "外部脚本",
+  Resource: "资源",
+  "Field Injection": "字段注入",
+  Extensions: "扩展",
+  "Input/Output": "输入/输出",
+  "Input Parameters": "输入参数",
+  "Output Parameters": "输出参数",
+  Parameters: "参数",
+  "Output Parameter": "输出参数",
+  "Timer Definition Type": "定时器定义类型",
+  "Timer Definition": "定时器定义",
+  Date: "日期",
+  Duration: "持续",
+  Cycle: "循环",
+  Signal: "信号",
+  "Signal Name": "信号名称",
+  Escalation: "升级",
+  Error: "错误",
+  "Link Name": "链接名称",
+  Condition: "条件名称",
+  "Variable Name": "变量名称",
+  "Variable Event": "变量事件",
+  "Specify more than one variable change event as a comma separated list.": "多个变量事件以逗号隔开",
+  "Wait for Completion": "等待完成",
+  "Activity Ref": "活动参考",
+  "Version Tag": "版本标签",
+  Executable: "可执行文件",
+  "External Task Configuration": "扩展任务配置",
+  "Task Priority": "任务优先级",
+  External: "外部",
+  Connector: "连接器",
+  "Must configure Connector": "必须配置连接器",
+  "Connector Id": "连接器编号",
+  Implementation: "实现方式",
+  "Field Injections": "字段注入",
+  Fields: "字段",
+  "Result Variable": "结果变量",
+  Topic: "主题",
+  "Configure Connector": "配置连接器",
+  "Input Parameter": "输入参数",
+  Assignee: "代理人",
+  "Candidate Users": "候选用户",
+  "Candidate Groups": "候选组",
+  "Due Date": "到期时间",
+  "Follow Up Date": "跟踪日期",
+  Priority: "优先级",
+  "The follow up date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)":
+    "跟踪日期必须符合EL表达式,如: ${someDate} ,或者一个ISO标准日期,如:2015-06-26T09:54:00",
+  "The due date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)":
+    "跟踪日期必须符合EL表达式,如: ${someDate} ,或者一个ISO标准日期,如:2015-06-26T09:54:00",
+  Variables: "变量",
+  "Candidate Starter Configuration": "候选人起动器配置",
+  "Candidate Starter Groups": "候选人起动器组",
+  "This maps to the process definition key.": "这映射到流程定义键。",
+  "Candidate Starter Users": "候选人起动器的用户",
+  "Specify more than one user as a comma separated list.": "指定多个用户作为逗号分隔的列表。",
+  "Tasklist Configuration": "Tasklist配置",
+  Startable: "启动",
+  "Specify more than one group as a comma separated list.": "指定多个组作为逗号分隔的列表。",
+  "Execution listeners": "执行监听器"
+};

+ 27 - 0
ruoyi-ui/src/views/system/bpmnPro/components/additional-modules/Translate/zh-cn/tasks.js

@@ -0,0 +1,27 @@
+export default {
+  Task: "任务",
+  "Send Task": "发送任务",
+  "Receive Task": "接收任务",
+  "User Task": "用户任务",
+  "Manual Task": "手工任务",
+  "Business Rule Task": "业务规则任务",
+  "Service Task": "服务任务",
+  "Script Task": "脚本任务",
+  "Call Activity": "调用活动",
+  "Sub Process (collapsed)": "子流程(折叠的)",
+  "Sub Process (expanded)": "子流程(展开的)",
+  "Start Event": "开始事件",
+  "Intermediate Throw Event": "中间事件",
+  "End Event": "结束事件",
+  StartEvent: "开始事件",
+  EndEvent: "结束事件",
+  SendTask: "发送任务",
+  ReceiveTask: "接收任务",
+  UserTask: "用户任务",
+  ManualTask: "手工任务",
+  BusinessRuleTask: "业务规则任务",
+  ServiceTask: "服务任务",
+  ScriptTask: "脚本任务",
+  CallActivity: "调用活动",
+  SubProcess: "子流程"
+};

+ 63 - 0
ruoyi-ui/src/views/system/bpmnPro/components/bo-utils/asynchronousContinuationsUtil.js

@@ -0,0 +1,63 @@
+import { is } from "bpmn-js/lib/util/ModelUtil";
+import { getModeler, getProcessEngine } from "@packages/bpmn-utils/BpmnDesignerUtils";
+
+////////// only in element extends bpmn:Task
+export function getACBefore(element) {
+  const prefix = getProcessEngine();
+  return isAsyncBefore(element.businessObject, prefix);
+}
+export function setACBefore(element, value) {
+  const prefix = getProcessEngine();
+  const modeling = getModeler.get("modeling");
+  // overwrite the legacy `async` property, we will use the more explicit `asyncBefore`
+  modeling.updateModdleProperties(element, element.businessObject, {
+    [`${prefix}:asyncBefore`]: value,
+    [`${prefix}:async`]: undefined
+  });
+}
+
+export function getACAfter(element) {
+  const prefix = getProcessEngine();
+  return isAsyncAfter(element.businessObject, prefix);
+}
+export function setACAfter(element, value) {
+  const prefix = getProcessEngine();
+  const modeling = getModeler.get("modeling");
+  modeling.updateModdleProperties(element, element.businessObject, {
+    [`${prefix}:asyncAfter`]: value
+  });
+}
+
+export function getACExclusive(element) {
+  const prefix = getProcessEngine();
+  return isExclusive(element.businessObject, prefix);
+}
+export function setACExclusive(element, value) {
+  const prefix = getProcessEngine();
+  const modeling = getModeler.get("modeling");
+  modeling.updateModdleProperties(element, element.businessObject, {
+    [`${prefix}:exclusive`]: value
+  });
+}
+
+//////////////////// helper
+// 是否支持异步属性
+export function isAsynchronous(element) {
+  const prefix = getProcessEngine();
+  return is(element, `${prefix}:AsyncCapable`);
+}
+
+// Returns true if the attribute 'asyncBefore' is set to true.
+function isAsyncBefore(bo, prefix) {
+  return !!(bo.get(`${prefix}:asyncBefore`) || bo.get(`${prefix}:async`));
+}
+
+// Returns true if the attribute 'asyncAfter' is set to true.
+function isAsyncAfter(bo, prefix) {
+  return !!bo.get(`${prefix}:asyncAfter`);
+}
+
+// Returns true if the attribute 'exclusive' is set to true.
+function isExclusive(bo, prefix) {
+  return !!bo.get(`${prefix}:exclusive`);
+}

+ 188 - 0
ruoyi-ui/src/views/system/bpmnPro/components/bo-utils/conditionUtil.js

@@ -0,0 +1,188 @@
+import { getBusinessObject, is, isAny } from "bpmn-js/lib/util/ModelUtil";
+import { getEventDefinition } from "@packages/bpmn-utils/BpmnEventDefinition";
+import { createModdleElement } from "@packages/bpmn-utils/BpmnExtensionElements";
+import { getModeler, getProcessEngine } from "@packages/bpmn-utils/BpmnDesignerUtils";
+
+///////////////////////////////// 配置项可见性
+const CONDITIONAL_SOURCES = ["bpmn:Activity", "bpmn:ExclusiveGateway", "bpmn:InclusiveGateway", "bpmn:ComplexGateway"];
+const defaultConditionTypeOptions = [
+  { label: "无条件( None )", value: "none" },
+  { label: "默认路径( Default )", value: "default" },
+  { label: "条件表达式( Expression )", value: "expression" },
+  { label: "条件脚本( Script )", value: "script" }
+];
+// 父节点符合条件的连线
+export function isConditionalSource(element) {
+  return isAny(element, CONDITIONAL_SOURCES);
+}
+// 是否是 定义条件的事件 ( 控制变量 Variables 配置 )
+export function isConditionEventDefinition(element) {
+  return is(element, "bpmn:Event") && !!getEventDefinition(element, "bpmn:ConditionalEventDefinition");
+}
+export function isExtendStartEvent(element) {
+  return is(element, "bpmn:StartEvent");
+}
+// 元素 是否符合 可以设置条件 的情况
+export function isCanbeConditional(element) {
+  return (
+    (is(element, "bpmn:SequenceFlow") && isConditionalSource(element?.source)) || isConditionEventDefinition(element)
+  );
+}
+
+///////////////////////////
+// 1. 条件变量部分
+export function getVariableNameValue(element) {
+  if (getConditionalEventDefinition(element)) {
+    return getConditionalEventDefinition(element).get("variableName");
+  }
+}
+export function setVariableNameValue(element, value) {
+  const modeling = getModeler.get("modeling");
+  const eventDefinition = getConditionalEventDefinition(element);
+  if (eventDefinition) {
+    modeling.updateModdleProperties(element, eventDefinition, { variableName: value || "" });
+  }
+}
+
+// 2. 条件事件部分
+export function getVariableEventsValue(element) {
+  if (getConditionalEventDefinition(element)) {
+    return getConditionalEventDefinition(element).get("variableEvents");
+  }
+}
+export function setVariableEventsValue(element, value) {
+  const modeling = getModeler.get("modeling");
+  const eventDefinition = getConditionalEventDefinition(element);
+  if (eventDefinition) {
+    modeling.updateModdleProperties(element, eventDefinition, { variableName: value || "" });
+  }
+}
+
+// 3. 元素条件类型
+export function getConditionTypeValue(element) {
+  const conditionExpression = getConditionExpression(element);
+  if (conditionExpression) {
+    return conditionExpression.get("language") === undefined ? "expression" : "script";
+  }
+  if (element.source?.businessObject?.default === element.businessObject) return "default";
+  return "none";
+}
+export function setConditionTypeValue(element, value) {
+  if (!value || value === "none" || value === "default") {
+    updateCondition(element);
+    return setDefaultCondition(element, value === "default");
+  }
+  const attributes = {
+    // body: '',
+    language: value === "script" ? "" : undefined
+  };
+  const parent = is(element, "bpmn:SequenceFlow") ? getBusinessObject(element) : getConditionalEventDefinition(element);
+  const formalExpressionElement = createModdleElement("bpmn:FormalExpression", attributes, parent);
+  updateCondition(element, formalExpressionElement);
+}
+
+// 4. 元素条件表达式
+export function getConditionExpressionValue(element) {
+  const conditionExpression = getConditionExpression(element);
+  if (conditionExpression) {
+    return conditionExpression.get("body");
+  }
+}
+export function setConditionExpressionValue(element, body) {
+  const parent = is(element, "bpmn:SequenceFlow") ? getBusinessObject(element) : getConditionalEventDefinition(element);
+  const formalExpressionElement = createModdleElement("bpmn:FormalExpression", { body }, parent);
+  updateCondition(element, formalExpressionElement);
+}
+
+// 5. 元素脚本来源类型
+export function getConditionScriptTypeValue(element) {
+  const prefix = getProcessEngine();
+  const conditionExpression = getConditionExpression(element);
+  console.log(conditionExpression);
+  if (conditionExpression.get("body") !== undefined) return "inline";
+  if (conditionExpression.get(`${prefix}:resource`) !== undefined) return "external";
+  return "none";
+}
+export function setConditionScriptTypeValue(element, value) {
+  const prefix = getProcessEngine();
+  const modeling = getModeler.get("modeling");
+  let props;
+  if (!value || value === "none") {
+    props = { body: undefined, [`${prefix}:resource`]: undefined };
+  }
+  if (value === "inline") {
+    props = { body: "", [`${prefix}:resource`]: undefined };
+  }
+  if (value === "external") {
+    props = { body: undefined, [`${prefix}:resource`]: "" };
+  }
+  modeling.updateModdleProperties(element, getConditionExpression(element), props);
+}
+
+// 6. 元素脚本 语言类型
+export function getConditionScriptLanguageValue(element) {
+  return getConditionExpression(element)?.get("language");
+}
+export function setConditionScriptLanguageValue(element, value) {
+  const modeling = getModeler.get("modeling");
+  modeling.updateModdleProperties(element, getConditionExpression(element), { language: value });
+}
+
+// 7. 元素脚本 body
+export function getConditionScriptBodyValue(element) {
+  return getConditionExpression(element)?.get("body");
+}
+export function setConditionScriptBodyValue(element, value) {
+  const modeling = getModeler.get("modeling");
+  modeling.updateModdleProperties(element, getConditionExpression(element), { body: value });
+}
+
+// 8. 元素脚本 source
+export function getConditionScriptResourceValue(element) {
+  const prefix = getProcessEngine();
+  return getConditionExpression(element)?.get(`${prefix}:resource`);
+}
+export function setConditionScriptResourceValue(element, value) {
+  const modeling = getModeler.get("modeling");
+  const prefix = getProcessEngine();
+  modeling.updateModdleProperties(element, getConditionExpression(element), {
+    [`${prefix}:resource`]: value
+  });
+}
+
+///////// helpers
+// 获取事件的条件定义
+export function getConditionTypeOptions(element) {
+  if (is(element, "bpmn:SequenceFlow")) {
+    return defaultConditionTypeOptions;
+  }
+  return defaultConditionTypeOptions.filter((condition) => condition.value !== "default");
+}
+function getConditionalEventDefinition(element) {
+  if (!is(element, "bpmn:Event")) return false;
+  return getEventDefinition(element, "bpmn:ConditionalEventDefinition");
+}
+//获取给定元素的条件表达式的值
+function getConditionExpression(element) {
+  const businessObject = getBusinessObject(element);
+  if (is(businessObject, "bpmn:SequenceFlow")) {
+    return businessObject.get("conditionExpression");
+  }
+  if (getConditionalEventDefinition(businessObject)) {
+    return getConditionalEventDefinition(businessObject).get("condition");
+  }
+}
+//
+function updateCondition(element, condition) {
+  const modeling = getModeler.get("modeling");
+  if (is(element, "bpmn:SequenceFlow")) {
+    modeling.updateProperties(element, { conditionExpression: condition });
+  } else {
+    modeling.updateModdleProperties(element, getConditionalEventDefinition(element), { condition });
+  }
+}
+//
+function setDefaultCondition(element, isDefault) {
+  const modeling = getModeler.get("modeling");
+  modeling.updateProperties(element.source, { default: isDefault ? element : undefined });
+}

+ 45 - 0
ruoyi-ui/src/views/system/bpmnPro/components/bo-utils/documentationUtil.js

@@ -0,0 +1,45 @@
+import { without } from "min-dash";
+import { getModeler } from "@packages/bpmn-utils/BpmnDesignerUtils";
+
+export function getDocumentValue(element) {
+  const businessObject = element?.businessObject;
+  const documentation = businessObject && findDocumentation(businessObject.get("documentation"));
+  return documentation && documentation.text;
+}
+
+export function setDocumentValue(element, value) {
+  const modeling = getModeler.get("modeling");
+  const bpmnFactory = getModeler.get("bpmnFactory");
+
+  const businessObject = element.businessObject;
+  const documentation = findDocumentation(businessObject && businessObject.get("documentation"));
+  // (1) 更新或者移除 原有 documentation
+  if (documentation) {
+    if (value) {
+      return modeling.updateModdleProperties(element, documentation, { text: value });
+    } else {
+      return modeling.updateModdleProperties(element, businessObject, {
+        documentation: without(businessObject.get("documentation"), documentation)
+      });
+    }
+  }
+  // (2) 创建新的 documentation
+  if (value) {
+    const newDocumentation = bpmnFactory?.create("bpmn:Documentation", {
+      text: value
+    });
+    return modeling.updateModdleProperties(element, businessObject, {
+      documentation: [...businessObject.get("documentation"), newDocumentation]
+    });
+  }
+}
+
+//////////// helpers
+
+const DOCUMENTATION_TEXT_FORMAT = "text/plain";
+
+function findDocumentation(docs) {
+  return docs.find(function (d) {
+    return (d.textFormat || DOCUMENTATION_TEXT_FORMAT) === DOCUMENTATION_TEXT_FORMAT;
+  });
+}

+ 114 - 0
ruoyi-ui/src/views/system/bpmnPro/components/bo-utils/executionListenersUtil.js

@@ -0,0 +1,114 @@
+import { getBusinessObject, is, isAny } from "bpmn-js/lib/util/ModelUtil";
+import { getModeler, getProcessEngine, LISTENER_ALLOWED_TYPES } from "@packages/bpmn-utils/BpmnDesignerUtils";
+import {
+  getExtensionElementsList,
+  addExtensionElements,
+  removeExtensionElements
+} from "@packages/bpmn-utils/BpmnExtensionElements";
+import { createScript } from "@packages/bo-utils/scriptUtil";
+
+export const EXECUTION_LISTENER_TYPE = {
+  class: "Java class",
+  expression: "Expression",
+  delegateExpression: "Delegate expression",
+  script: "Script"
+};
+
+// execution listener list
+export function getExecutionListeners(element) {
+  const prefix = getProcessEngine();
+  const businessObject = getListenersContainer(element);
+  return getExtensionElementsList(businessObject, `${prefix}:ExecutionListener`);
+}
+
+// create an empty execution listener and update element's businessObject
+export function addEmptyExtensionListener(element) {
+  const prefix = getProcessEngine();
+  const moddle = getModeler.getModdle();
+  const listener = moddle.create(`${prefix}:ExecutionListener`, {
+    event: getDefaultEvent(element),
+    class: ""
+  });
+  const businessObject = getListenersContainer(element);
+  addExtensionElements(element, businessObject, listener);
+}
+
+// create an execution listener with props
+export function addExecutionListener(element, props) {
+  const prefix = getProcessEngine();
+  const moddle = getModeler.getModdle();
+  const businessObject = getListenersContainer(element);
+  const listener = moddle.create(`${prefix}:ExecutionListener`, {});
+  updateListenerProperty(element, listener, props);
+  addExtensionElements(element, businessObject, listener);
+}
+
+// update execution listener's property
+export function updateExecutionListener(element, props, listener) {
+  removeExtensionElements(element, getListenersContainer(element), listener);
+  addExecutionListener(element, props);
+}
+
+// remove an execution listener
+export function removeExecutionListener(element, listener) {
+  removeExtensionElements(element, getListenersContainer(element), listener);
+}
+
+////////////// helpers
+export function isExecutable(element) {
+  if (isAny(element, LISTENER_ALLOWED_TYPES)) return true;
+  if (is(element, "bpmn:Participant")) {
+    return !!element.businessObject.processRef;
+  }
+  return false;
+}
+
+export function getExecutionListenerType(listener) {
+  const prefix = getProcessEngine();
+  if (isAny(listener, [`${prefix}:ExecutionListener`])) {
+    if (listener.get(`${prefix}:class`)) return "class";
+    if (listener.get(`${prefix}:expression`)) return "expression";
+    if (listener.get(`${prefix}:delegateExpression`)) return "delegateExpression";
+    if (listener.get("script")) return "script";
+  }
+  return "";
+}
+
+export function getListenersContainer(element) {
+  const businessObject = getBusinessObject(element);
+  return businessObject?.get("processRef") || businessObject;
+}
+
+export function getDefaultEvent(element) {
+  return is(element, "bpmn:SequenceFlow") ? "take" : "start";
+}
+
+export function getExecutionListenerTypes(element) {
+  if (is(element, "bpmn:SequenceFlow")) {
+    return [{ label: "Take", value: "take" }];
+  }
+  return [
+    { label: "Start", value: "start" },
+    { label: "End", value: "end" }
+  ];
+}
+
+function updateListenerProperty(element, listener, props) {
+  const modeling = getModeler.getModeling();
+  const prefix = getProcessEngine();
+  const { event, class: listenerClass, expression, delegateExpression, script, type, fields } = props;
+
+  const updateProperty = (key, value) =>
+    modeling.updateModdleProperties(element, listener, { [`${prefix}:${key}`]: value });
+
+  event && updateProperty("event", event);
+  listenerClass && updateProperty("class", listenerClass);
+  expression && updateProperty("expression", expression);
+  delegateExpression && updateProperty("delegateExpression", delegateExpression);
+  console.log(props);
+
+  if (script) {
+    const bpmnScript = createScript(script);
+    modeling.updateModdleProperties(element, listener, { script: bpmnScript });
+  }
+}

+ 77 - 0
ruoyi-ui/src/views/system/bpmnPro/components/bo-utils/extensionPropertiesUtil.js

@@ -0,0 +1,77 @@
+import { getBusinessObject, is } from "bpmn-js/lib/util/ModelUtil";
+import { without } from "min-dash";
+import { createModdleElement, getExtensionElementsList } from "@packages/bpmn-utils/BpmnExtensionElements";
+import { getModeler, getProcessEngine } from "@packages/bpmn-utils/BpmnDesignerUtils";
+
+/////// 功能函数
+export function getExtensionProperties(element) {
+  const businessObject = getRelevantBusinessObject(element);
+
+  if (!businessObject) return [];
+  return getPropertiesList(businessObject) || [];
+}
+
+export function addExtensionProperty(element, property) {
+  try {
+    const modeling = getModeler.getModeling();
+    const prefix = getProcessEngine();
+
+    const businessObject = getRelevantBusinessObject(element);
+
+    // 判断 extensionElements
+    let extensionElements = businessObject.get("extensionElements");
+    if (!extensionElements) {
+      extensionElements = createModdleElement("bpmn:ExtensionElements", { values: [] }, businessObject);
+      modeling.updateModdleProperties(element, businessObject, { extensionElements });
+    }
+    // 判断 extensionElements 是否有 properties
+    let properties = getProperties(businessObject);
+    if (!properties) {
+      properties = createModdleElement(`${prefix}:Properties`, { values: [] }, extensionElements);
+      modeling.updateModdleProperties(element, extensionElements, {
+        values: [...extensionElements.get("values"), properties]
+      });
+    }
+    // 创建新属性并添加
+    const newProperty = createModdleElement(`${prefix}:Property`, property, properties);
+    modeling.updateModdleProperties(element, properties, {
+      values: [...properties?.get("values"), newProperty]
+    });
+  } catch (e) {
+    console.log(e);
+  }
+}
+
+export function removeExtensionProperty(element, property) {
+  const businessObject = getRelevantBusinessObject(element);
+  const extensionElements = businessObject.get("extensionElements");
+  const properties = getProperties(businessObject);
+  if (!properties) return;
+
+  const modeling = getModeler.getModeling();
+
+  const values = without(properties.get("values"), property);
+  modeling.updateModdleProperties(element, properties, { values });
+
+  if (!values || !values.length) {
+    modeling.updateModdleProperties(element, extensionElements, {
+      values: without(extensionElements.get("values"), properties)
+    });
+  }
+}
+
+///// helpers
+function getRelevantBusinessObject(element) {
+  const businessObject = getBusinessObject(element);
+  if (is(element, "bpmn:Participant")) {
+    return businessObject.get("processRef");
+  }
+  return businessObject;
+}
+function getPropertiesList(bo) {
+  const properties = getProperties(bo);
+  return properties && properties.get("values");
+}
+function getProperties(bo) {
+  return getExtensionElementsList(bo)[0];
+}

+ 21 - 0
ruoyi-ui/src/views/system/bpmnPro/components/bo-utils/idUtil.js

@@ -0,0 +1,21 @@
+import { catchError } from "@utils/printCatch";
+import { getModeler } from "@packages/bpmn-utils/BpmnDesignerUtils";
+import { isIdValid } from "@packages/bpmn-utils/BpmnValidator";
+
+export function getIdValue(element) {
+  return element.businessObject.id;
+}
+
+export function setIdValue(element, value) {
+  const errorMsg = isIdValid(element.businessObject, value);
+
+  if (errorMsg && errorMsg.length) {
+    return catchError(errorMsg);
+  }
+
+  const modeling = getModeler.getModeling();
+
+  modeling.updateProperties(element, {
+    id: value
+  });
+}

+ 23 - 0
ruoyi-ui/src/views/system/bpmnPro/components/bo-utils/initiatorUtil.js

@@ -0,0 +1,23 @@
+import { getBusinessObject, is } from "bpmn-js/lib/util/ModelUtil";
+import { getModeler, getProcessEngine } from "@packages/bpmn-utils/BpmnDesignerUtils";
+
+////////// only in bpmn:StartEvent
+export function getInitiatorValue(element) {
+  const prefix = getProcessEngine();
+  const businessObject = getBusinessObject(element);
+
+  return businessObject.get(`${prefix}:initiator`);
+}
+export function setInitiatorValue(element, value) {
+  const prefix = getProcessEngine();
+  const modeling = getModeler.getModeling();
+  const businessObject = getBusinessObject(element);
+  modeling.updateModdleProperties(element, businessObject, {
+    [`${prefix}:initiator`]: value
+  });
+}
+
+export function isStartInitializable(element) {
+  const prefix = getProcessEngine();
+  return is(element, `${prefix}:Initiator`) && !is(element.parent, "bpmn:SubProcess");
+}

+ 94 - 0
ruoyi-ui/src/views/system/bpmnPro/components/bo-utils/jobExecutionUtil.js

@@ -0,0 +1,94 @@
+import { getBusinessObject, is } from "bpmn-js/lib/util/ModelUtil";
+import { getModeler, getProcessEngine } from "@packages/bpmn-utils/BpmnDesignerUtils";
+import { createModdleElement, getExtensionElementsList } from "@packages/bpmn-utils/BpmnExtensionElements";
+import { getServiceTaskLikeBusinessObject } from "@packages/bpmn-utils/BpmnImplementationType";
+import { getTimerEventDefinition } from "@packages/bpmn-utils/BpmnEventDefinition";
+import { isAsync } from "@packages/bpmn-utils/BpmnAsyncElement";
+
+//
+export function retryTimeCycleVisible(element) {
+  const prefix = getProcessEngine();
+  const businessObject = getBusinessObject(element);
+  return (is(element, `${prefix}:AsyncCapable`) && isAsync(businessObject)) || !!isTimerEvent(element);
+}
+export function taskPriorityVisible(element) {
+  const prefix = getProcessEngine();
+  const businessObject = getBusinessObject(element);
+  return (
+    (is(element, `${prefix}:JobPriorized`) && isAsync(businessObject)) ||
+    is(element, "bpmn:Process") ||
+    (is(element, "bpmn:Participant") && businessObject.get("processRef")) ||
+    !!isTimerEvent(element)
+  );
+}
+export function isJobExecutable(element) {
+  return retryTimeCycleVisible(element) || taskPriorityVisible(element);
+}
+
+// 任务优先级
+export function getExternalTaskValue(element) {
+  const prefix = getProcessEngine();
+  const businessObject = getRelativeBusinessObject(element);
+  return businessObject.get(`${prefix}:taskPriority`);
+}
+export function setExternalTaskValue(element, value) {
+  const prefix = getProcessEngine();
+  const modeling = getModeler.getModeling();
+  const businessObject = getRelativeBusinessObject(element);
+  modeling.updateModdleProperties(element, businessObject, {
+    [`${prefix}:taskPriority`]: value
+  });
+}
+
+// 重试周期
+export function getRetryTimeCycleValue(element) {
+  const prefix = getProcessEngine();
+  const businessObject = getBusinessObject(element);
+  const failedJobRetryTimeCycle = getExtensionElementsList(businessObject, `${prefix}:FailedJobRetryTimeCycle`)[0];
+  return failedJobRetryTimeCycle && failedJobRetryTimeCycle.body;
+}
+export function setRetryTimeCycleValue(element, value) {
+  const prefix = getProcessEngine();
+  const modeling = getModeler.getModeling();
+  const businessObject = getBusinessObject(element);
+
+  let extensionElements = businessObject.get("extensionElements");
+  if (!extensionElements) {
+    extensionElements = createModdleElement("bpmn:ExtensionElements", { values: [] }, businessObject);
+    modeling.updateModdleProperties(element, businessObject, { extensionElements });
+  }
+
+  let failedJobRetryTimeCycle = getExtensionElementsList(businessObject, `${prefix}:FailedJobRetryTimeCycle`)[0];
+  if (!failedJobRetryTimeCycle) {
+    failedJobRetryTimeCycle = createModdleElement(`${prefix}:FailedJobRetryTimeCycle`, {}, extensionElements);
+    modeling.updateModdleProperties(element, extensionElements, {
+      values: [...extensionElements.get("values"), failedJobRetryTimeCycle]
+    });
+  }
+
+  modeling.updateModdleProperties(element, failedJobRetryTimeCycle, { body: value });
+}
+
+/////////// helpers
+function isExternalTaskLike(element) {
+  const prefix = getProcessEngine();
+  const bo = getServiceTaskLikeBusinessObject(element),
+    type = bo && bo.get(`${prefix}:type`);
+  return bo && is(bo, `${prefix}:ServiceTaskLike`) && type && type === "external";
+}
+
+function getRelativeBusinessObject(element) {
+  let businessObject;
+  if (is(element, "bpmn:Participant")) {
+    businessObject = getBusinessObject(element).get("processRef");
+  } else if (isExternalTaskLike(element)) {
+    businessObject = getServiceTaskLikeBusinessObject(element);
+  } else {
+    businessObject = getBusinessObject(element);
+  }
+  return businessObject;
+}
+
+function isTimerEvent(element) {
+  return is(element, "bpmn:Event") && getTimerEventDefinition(element);
+}

+ 63 - 0
ruoyi-ui/src/views/system/bpmnPro/components/bo-utils/nameUtil.js

@@ -0,0 +1,63 @@
+import { getBusinessObject, is } from "bpmn-js/lib/util/ModelUtil";
+import { isAny } from "bpmn-js/lib/features/modeling/util/ModelingUtil";
+import { add as collectionAdd } from "diagram-js/lib/util/Collections";
+import { getModeler } from "@packages/bpmn-utils/BpmnDesignerUtils";
+
+// 根据元素获取 name 属性
+export function getNameValue(element) {
+  if (isAny(element, ["bpmn:Collaboration", "bpmn:DataAssociation", "bpmn:Association"])) {
+    return undefined;
+  }
+  if (is(element, "bpmn:TextAnnotation")) {
+    return element.businessObject.text;
+  }
+  if (is(element, "bpmn:Group")) {
+    const businessObject = getBusinessObject(element),
+      categoryValueRef = businessObject?.categoryValueRef;
+    return categoryValueRef?.value;
+  }
+  return element?.businessObject.name;
+}
+
+// 根据输入内容设置 name 属性
+export function setNameValue(element, value) {
+  const modeling = getModeler.getModeling();
+  const canvas = getModeler.getCanvas();
+  const bpmnFactory = getModeler.getModeler?.get("bpmnFactory");
+
+  if (isAny(element, ["bpmn:Collaboration", "bpmn:DataAssociation", "bpmn:Association"])) {
+    return undefined;
+  }
+  if (is(element, "bpmn:TextAnnotation")) {
+    return modeling?.updateProperties(element, { text: value });
+  }
+  if (is(element, "bpmn:Group")) {
+    const businessObject = getBusinessObject(element),
+      categoryValueRef = businessObject.categoryValueRef;
+    if (!categoryValueRef) {
+      initializeCategory(businessObject, canvas?.getRootElement(), bpmnFactory);
+    }
+    return modeling?.updateLabel(element, value);
+  }
+  modeling?.updateProperties(element, { name: value });
+}
+
+////////////////  helpers
+
+function createCategoryValue(definitions, bpmnFactory) {
+  const categoryValue = bpmnFactory.create("bpmn:CategoryValue");
+  const category = bpmnFactory.create("bpmn:Category", {
+    categoryValue: [categoryValue]
+  });
+  collectionAdd(definitions.get("rootElements"), category);
+  getBusinessObject(category).$parent = definitions;
+  getBusinessObject(categoryValue).$parent = category;
+
+  return categoryValue;
+}
+
+function initializeCategory(businessObject, rootElement, bpmnFactory) {
+  const definitions = getBusinessObject(rootElement).$parent;
+
+  businessObject.categoryValueRef = createCategoryValue(definitions, bpmnFactory);
+}

+ 27 - 0
ruoyi-ui/src/views/system/bpmnPro/components/bo-utils/processUtil.js

@@ -0,0 +1,27 @@
+import { getModeler, getProcessEngine } from "@packages/bpmn-utils/BpmnDesignerUtils";
+
+export function getProcessExecutable(element) {
+  return !!element.businessObject.isExecutable;
+}
+
+export function setProcessExecutable(element, value) {
+  const modeling = getModeler.getModeling();
+  modeling.updateProperties(element, {
+    isExecutable: value
+  });
+}
+
+export function getProcessVersionTag(element) {
+  const prefix = getProcessEngine();
+
+  return element.businessObject.get(`${prefix}:versionTag`);
+}
+
+export function setProcessVersionTag(element, value) {
+  const modeling = getModeler.getModeling();
+  const prefix = getProcessEngine();
+
+  modeling.updateProperties(element, {
+    [`${prefix}:versionTag`]: value
+  });
+}

+ 19 - 0
ruoyi-ui/src/views/system/bpmnPro/components/bo-utils/scriptUtil.js

@@ -0,0 +1,19 @@
+import { getModeler, getProcessEngine } from "@packages/bpmn-utils/BpmnDesignerUtils";
+
+export function createScript(props) {
+  const prefix = getProcessEngine();
+  const moddle = getModeler.getModdle();
+  const { scriptFormat, value, resource } = props;
+
+  return moddle.create(`${prefix}:Script`, { scriptFormat, value, resource });
+}
+
+export function getScriptType(script) {
+  if (script.get("resource")) {
+    return "External Resource";
+  }
+  if (script.get("value")) {
+    return "Inline Script";
+  }
+  return "none";
+}

+ 14 - 0
ruoyi-ui/src/views/system/bpmnPro/components/bpmn-icons/bpmn-empty-state.svg

@@ -0,0 +1,14 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64" viewBox="0 0 64 64">
+  <defs>
+    <rect id="empty-state-bpmn-a" width="57" height="47" x="3" y="8" rx="7"/>
+    <mask id="empty-state-bpmn-b" width="57" height="47" x="0" y="0" fill="#fff" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox">
+      <use xlink:href="#empty-state-bpmn-a"/>
+    </mask>
+  </defs>
+  <g fill="none" fill-rule="evenodd">
+    <path fill="#818798" d="M52,11 C54.7614237,11 57,13.2385763 57,16 L57,47 C57,49.7614237 54.7614237,52 52,52 L11,52 C8.23857625,52 6,49.7614237 6,47 L6,16 C6,13.2385763 8.23857625,11 11,11 L52,11 Z M52,13 L11,13 C9.40231912,13 8.09633912,14.24892 8.00509269,15.8237272 L8,16 L8,47 C8,48.5976809 9.24891996,49.9036609 10.8237272,49.9949073 L11,50 L52,50 C53.5976809,50 54.9036609,48.75108 54.9949073,47.1762728 L55,47 L55,16 C55,14.4023191 53.75108,13.0963391 52.1762728,13.0050927 L52,13 Z"/>
+    <rect width="31" height="6" x="16" y="24" fill="#D5D7DD"/>
+    <rect width="21" height="6" x="21" y="33" fill="#D5D7DD"/>
+    <use stroke="#B9BCC6" stroke-dasharray="5 2" stroke-width="2" mask="url(#empty-state-bpmn-b)" xlink:href="#empty-state-bpmn-a"/>
+  </g>
+</svg>

+ 3 - 0
ruoyi-ui/src/views/system/bpmnPro/components/bpmn-icons/bpmn-icon-association.svg

@@ -0,0 +1,3 @@
+<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+    <line x1="1.5" y1="30.5" x2="30.5" y2="1.5" stroke="#000" stroke-width="2" fill="none" stroke-dasharray="3.3,6" stroke-linecap="square"/>
+</svg>

+ 6 - 0
ruoyi-ui/src/views/system/bpmnPro/components/bpmn-icons/bpmn-icon-business-rule-task.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
+  <g transform="translate(0 3)">
+    <path d="M6.49417317,0 C2.91556567,0 0,2.90326667 0,6.475 L0,19.8583333 C0,23.4300667 2.91556567,26.3333333 6.49417317,26.3333333 L25.5058267,26.3333333 C29.084435,26.3333333 32,23.4300667 32,19.8583333 L32,6.475 C32,2.90326667 29.084435,0 25.5058267,0 L6.49417317,0 Z M6.49417317,2 L25.5058267,2 C28.0147767,2 30,3.98043383 30,6.475 L30,19.8583333 C30,22.3529 28.0147767,24.3333333 25.5058267,24.3333333 L6.49417317,24.3333333 C3.98522267,24.3333333 2,22.3529 2,19.8583333 L2,6.475 C2,3.98043383 3.98522267,2 6.49417317,2 Z M5.29550783,4.39847 L5.29550783,17.0631183 L22.1658533,17.0631183 L22.1658533,4.39847 L5.29550783,4.39847 Z M6.01393233,8.78356117 L21.4474617,8.78356117 L21.4474617,12.223405 L9.984668,12.223405 L9.984668,8.79205733 L9.2662435,8.79205733 L9.2662435,12.223405 L6.01393233,12.223405 L6.01393233,8.78356117 Z M6.01393233,12.9418295 L9.2662435,12.9418295 L9.2662435,16.3446933 L6.01393233,16.3446933 L6.01393233,12.9418295 Z M9.984668,12.9418295 L21.4474617,12.9418295 L21.4474617,16.3446933 L9.984668,16.3446933 L9.984668,12.9418295 Z"/>
+    <polygon points="6.079 5.209 6.079 8.796 21.44 8.796 21.44 5.209"/>
+  </g>
+</svg>

部分文件因文件數量過多而無法顯示