瀏覽代碼

Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into dev

YunaiV 6 月之前
父節點
當前提交
c514c799ff
共有 87 個文件被更改,包括 5683 次插入1555 次删除
  1. 3 0
      .env.dev
  2. 3 0
      .env.local
  3. 3 0
      .env.prod
  4. 3 0
      .env.stage
  5. 3 0
      .env.test
  6. 5 5
      package.json
  7. 309 130
      pnpm-lock.yaml
  8. 8 7
      src/api/login/index.ts
  9. 1 0
      src/assets/svgs/bpm/delay.svg
  10. 4 0
      src/components/Echart/src/Echart.vue
  11. 3 3
      src/components/Editor/src/Editor.vue
  12. 17 0
      src/components/SimpleProcessDesignerV2/src/NodeHandler.vue
  13. 7 0
      src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue
  14. 165 38
      src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue
  15. 27 19
      src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue
  16. 36 0
      src/components/SimpleProcessDesignerV2/src/consts.ts
  17. 27 11
      src/components/SimpleProcessDesignerV2/src/node.ts
  18. 28 18
      src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue
  19. 189 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/DelayTimerNodeConfig.vue
  20. 32 4
      src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue
  21. 9 1
      src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue
  22. 98 0
      src/components/SimpleProcessDesignerV2/src/nodes/DelayTimerNode.vue
  23. 6 1
      src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss
  24. 29 10
      src/components/UserSelectForm/index.vue
  25. 23 62
      src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue
  26. 157 0
      src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json
  27. 12 0
      src/components/bpmnProcessDesigner/package/designer/plugins/palette/CustomPalette.js
  28. 6 0
      src/components/bpmnProcessDesigner/package/designer/plugins/palette/paletteProvider.js
  29. 2 0
      src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js
  30. 95 47
      src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue
  31. 20 264
      src/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue
  32. 252 0
      src/components/bpmnProcessDesigner/package/penal/custom-config/components/BoundaryEventTimer.vue
  33. 623 0
      src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue
  34. 13 0
      src/components/bpmnProcessDesigner/package/penal/custom-config/data.ts
  35. 134 5
      src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue
  36. 1 2
      src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue
  37. 4 20
      src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue
  38. 36 0
      src/components/bpmnProcessDesigner/package/penal/task/data.ts
  39. 280 0
      src/components/bpmnProcessDesigner/package/penal/task/task-components/CallActivity.vue
  40. 91 0
      src/components/bpmnProcessDesigner/package/penal/task/task-components/ServiceTask.vue
  41. 220 15
      src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
  42. 6 8
      src/components/bpmnProcessDesigner/package/theme/process-designer.scss
  43. 1 2
      src/config/axios/service.ts
  44. 12 9
      src/directives/permission/hasPermi.ts
  45. 1 1
      src/layout/components/useRenderLayout.tsx
  46. 4 1
      src/locales/en.ts
  47. 4 1
      src/locales/zh-CN.ts
  48. 24 0
      src/router/modules/remaining.ts
  49. 1 2
      src/utils/routerHelper.ts
  50. 3 0
      src/utils/tree.ts
  51. 3 1
      src/views/Login/Login.vue
  52. 1 0
      src/views/Login/SocialLogin.vue
  53. 278 0
      src/views/Login/components/ForgetPasswordForm.vue
  54. 11 5
      src/views/Login/components/LoginForm.vue
  55. 1 0
      src/views/Login/components/RegisterForm.vue
  56. 2 1
      src/views/Login/components/index.ts
  57. 12 33
      src/views/bpm/model/CategoryDraggableModel.vue
  58. 161 22
      src/views/bpm/model/ModelForm.vue
  59. 212 48
      src/views/bpm/model/editor/index.vue
  60. 301 0
      src/views/bpm/model/form/BasicInfo.vue
  61. 137 0
      src/views/bpm/model/form/FormDesign.vue
  62. 235 0
      src/views/bpm/model/form/ProcessDesign.vue
  63. 439 0
      src/views/bpm/model/form/index.vue
  64. 9 1
      src/views/bpm/model/index.vue
  65. 0 404
      src/views/bpm/model/index_old.vue
  66. 45 6
      src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue
  67. 261 161
      src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue
  68. 2 2
      src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue
  69. 15 7
      src/views/bpm/processInstance/detail/index.vue
  70. 38 38
      src/views/bpm/processInstance/index.vue
  71. 142 6
      src/views/bpm/simple/SimpleModelDesign.vue
  72. 94 19
      src/views/bpm/task/done/index.vue
  73. 76 18
      src/views/bpm/task/todo/index.vue
  74. 3 3
      src/views/crm/business/BusinessForm.vue
  75. 3 3
      src/views/crm/contract/ContractForm.vue
  76. 6 1
      src/views/erp/purchase/order/components/PurchaseOrderItemForm.vue
  77. 6 4
      src/views/infra/build/index.vue
  78. 10 0
      src/views/infra/file/index.vue
  79. 0 1
      src/views/mall/promotion/bargain/activity/index.vue
  80. 9 2
      src/views/mall/statistics/product/components/ProductRank.vue
  81. 1 1
      src/views/mp/draft/components/NewsForm.vue
  82. 1 1
      src/views/report/goview/index.vue
  83. 12 8
      src/views/system/area/index.vue
  84. 113 72
      src/views/system/menu/index.vue
  85. 1 0
      src/views/system/user/index.vue
  86. 1 0
      types/env.d.ts
  87. 2 1
      vite.config.ts

+ 3 - 0
.env.dev

@@ -32,3 +32,6 @@ VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
 
 # 验证码的开关
 VITE_APP_CAPTCHA_ENABLE=true
+
+# GoView域名
+VITE_GOVIEW_URL='http://127.0.0.1:3000'

+ 3 - 0
.env.local

@@ -29,3 +29,6 @@ VITE_MALL_H5_DOMAIN='http://localhost:3000'
 
 # 验证码的开关
 VITE_APP_CAPTCHA_ENABLE=false
+
+# GoView域名
+VITE_GOVIEW_URL='http://127.0.0.1:3000'

+ 3 - 0
.env.prod

@@ -29,3 +29,6 @@ VITE_OUT_DIR=dist-prod
 
 # 商城H5会员端域名
 VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
+
+# GoView域名
+VITE_GOVIEW_URL='http://127.0.0.1:3000'

+ 3 - 0
.env.stage

@@ -29,3 +29,6 @@ VITE_OUT_DIR=dist-stage
 
 # 商城H5会员端域名
 VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
+
+# GoView域名
+VITE_GOVIEW_URL='http://127.0.0.1:3000'

+ 3 - 0
.env.test

@@ -29,3 +29,6 @@ VITE_OUT_DIR=dist-test
 
 # 商城H5会员端域名
 VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
+
+# GoView域名
+VITE_GOVIEW_URL='http://127.0.0.1:3000'

+ 5 - 5
package.json

@@ -1,6 +1,6 @@
 {
   "name": "yudao-ui-admin-vue3",
-  "version": "2.3.0-snapshot",
+  "version": "2.4.0-snapshot",
   "description": "基于vue3、vite4、element-plus、typesScript",
   "author": "xingyu",
   "private": false,
@@ -38,7 +38,7 @@
     "animate.css": "^4.1.1",
     "axios": "^1.6.8",
     "benz-amr-recorder": "^1.1.5",
-    "bpmn-js-token-simulation": "^0.10.0",
+    "bpmn-js-token-simulation": "^0.36.0",
     "camunda-bpmn-moddle": "^7.0.1",
     "cropperjs": "^1.6.1",
     "crypto-js": "^4.2.0",
@@ -47,7 +47,7 @@
     "driver.js": "^1.3.1",
     "echarts": "^5.5.0",
     "echarts-wordcloud": "^2.1.0",
-    "element-plus": "2.8.4",
+    "element-plus": "2.9.1",
     "fast-xml-parser": "^4.3.2",
     "highlight.js": "^11.9.0",
     "jsencrypt": "^3.3.2",
@@ -96,8 +96,8 @@
     "@vitejs/plugin-vue": "^5.0.4",
     "@vitejs/plugin-vue-jsx": "^3.1.0",
     "autoprefixer": "^10.4.17",
-    "bpmn-js": "8.10.0",
-    "bpmn-js-properties-panel": "0.46.0",
+    "bpmn-js": "^17.9.2",
+    "bpmn-js-properties-panel": "5.23.0",
     "consola": "^3.2.3",
     "eslint": "^8.57.0",
     "eslint-config-prettier": "^9.1.0",

+ 309 - 130
pnpm-lock.yaml

@@ -72,8 +72,8 @@ dependencies:
     specifier: ^2.1.0
     version: 2.1.0(echarts@5.5.1)
   element-plus:
-    specifier: 2.8.4
-    version: 2.8.4(vue@3.5.12)
+    specifier: 2.9.1
+    version: 2.9.1(vue@3.5.12)
   fast-xml-parser:
     specifier: ^4.3.2
     version: 4.5.0
@@ -215,11 +215,11 @@ devDependencies:
     specifier: ^10.4.17
     version: 10.4.20(postcss@8.4.49)
   bpmn-js:
-    specifier: 8.10.0
-    version: 8.10.0
+    specifier: ^17.9.2
+    version: 17.11.1
   bpmn-js-properties-panel:
-    specifier: 0.46.0
-    version: 0.46.0(bpmn-js@8.10.0)
+    specifier: 5.23.0
+    version: 5.23.0(@bpmn-io/properties-panel@3.25.0)(bpmn-js@17.11.1)(camunda-bpmn-js-behaviors@1.7.2)(diagram-js@12.8.1)
   consola:
     specifier: ^3.2.3
     version: 3.2.3
@@ -1465,29 +1465,117 @@ packages:
       '@babel/helper-string-parser': 7.25.9
       '@babel/helper-validator-identifier': 7.25.9
 
+  /@bpmn-io/cm-theme@0.1.0-alpha.2:
+    resolution: {integrity: sha512-ZILgiYzxk3KMvxplUXmdRFQo45/JehDPg5k9tWfehmzUOSE13ssyLPil8uCloMQnb3yyzyOWTjb/wzKXTHlFQw==, tarball: https://registry.npmmirror.com/@bpmn-io/cm-theme/-/cm-theme-0.1.0-alpha.2.tgz}
+    dependencies:
+      '@codemirror/language': 6.10.6
+      '@codemirror/view': 6.35.0
+      '@lezer/highlight': 1.2.1
+    dev: true
+
   /@bpmn-io/diagram-js-ui@0.2.3:
     resolution: {integrity: sha512-OGyjZKvGK8tHSZ0l7RfeKhilGoOGtFDcoqSGYkX0uhFlo99OVZ9Jn1K7TJGzcE9BdKwvA5Y5kGqHEhdTxHvFfw==, tarball: https://registry.npmmirror.com/@bpmn-io/diagram-js-ui/-/diagram-js-ui-0.2.3.tgz}
     dependencies:
       htm: 3.1.1
       preact: 10.25.0
-    dev: false
 
-  /@bpmn-io/element-templates-validator@0.2.0:
-    resolution: {integrity: sha512-/ogp0+6zUFdoiY09NYaHL5JtapB8zN1spG8hpML96qetXDCODRxnsqlHTvSwxtZHUDcgun+lxcK8b4wgtCP+6Q==, tarball: https://registry.npmmirror.com/@bpmn-io/element-templates-validator/-/element-templates-validator-0.2.0.tgz}
+  /@bpmn-io/extract-process-variables@0.8.0:
+    resolution: {integrity: sha512-yAS7ZYX+D56K+luC36u96eRMLb4VHcPUwTUqMZ/Z/Je2gou2DJLRbuBTHAB4jjKt4wFCHSG4B8Y+TrBciEYf4w==, tarball: https://registry.npmmirror.com/@bpmn-io/extract-process-variables/-/extract-process-variables-0.8.0.tgz}
     dependencies:
-      '@camunda/element-templates-json-schema': 0.4.0
-      json-source-map: 0.6.1
-      min-dash: 3.8.1
+      min-dash: 4.2.2
     dev: true
 
-  /@bpmn-io/extract-process-variables@0.4.5:
-    resolution: {integrity: sha512-LtHx5b9xqS8avRLrq/uTlKhWzMeV3bWQKIdDic2bdo5n9roitX13GRb01u2S0hSsKDWEhXQtydFYN2b6G7bqfw==, tarball: https://registry.npmmirror.com/@bpmn-io/extract-process-variables/-/extract-process-variables-0.4.5.tgz}
+  /@bpmn-io/feel-editor@1.9.1(@lezer/common@1.2.3):
+    resolution: {integrity: sha512-UxSORdh5cwKM4fib4f9ov6J1/BHGpQVNtA+wPyEdKQyCyz3wqwE2/xe5wneVR1j5QFC5m2Na8nTy4a1TDFvZTw==, tarball: https://registry.npmmirror.com/@bpmn-io/feel-editor/-/feel-editor-1.9.1.tgz}
+    engines: {node: '>= 16'}
     dependencies:
-      min-dash: 3.8.1
+      '@bpmn-io/feel-lint': 1.3.1
+      '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3)
+      '@codemirror/commands': 6.7.1
+      '@codemirror/language': 6.10.6
+      '@codemirror/lint': 6.8.4
+      '@codemirror/state': 6.4.1
+      '@codemirror/view': 6.35.0
+      '@lezer/highlight': 1.2.1
+      lang-feel: 2.2.0
+      min-dom: 4.2.1
+    transitivePeerDependencies:
+      - '@lezer/common'
+    dev: true
+
+  /@bpmn-io/feel-lint@1.3.1:
+    resolution: {integrity: sha512-wcFkJKhOm/iqCt5bzkKvxL5Dr9wKwUD+t164bQYbJsTYouAqmkkxiGsoqck42hXwdIhMSguZ+vqQ3hj5QdiYCA==, tarball: https://registry.npmmirror.com/@bpmn-io/feel-lint/-/feel-lint-1.3.1.tgz}
+    dependencies:
+      '@codemirror/language': 6.10.6
+      lezer-feel: 1.4.0
+    dev: true
+
+  /@bpmn-io/properties-panel@3.25.0(@lezer/common@1.2.3):
+    resolution: {integrity: sha512-SRGgj8uJc1Yyjcht2g36Q+xKR7sTx5VZXvcwDrdmQKlx5Y3nRmvmMjDGzeGDJDb7pNU1DSlaBJic84uISDBMWg==, tarball: https://registry.npmmirror.com/@bpmn-io/properties-panel/-/properties-panel-3.25.0.tgz}
+    dependencies:
+      '@bpmn-io/feel-editor': 1.9.1(@lezer/common@1.2.3)
+      '@codemirror/view': 6.35.0
+      classnames: 2.5.1
+      feelers: 1.4.0
+      focus-trap: 7.6.2
+      min-dash: 4.2.2
+      min-dom: 4.2.1
+    transitivePeerDependencies:
+      - '@lezer/common'
+    dev: true
+
+  /@codemirror/autocomplete@6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3):
+    resolution: {integrity: sha512-1dNIOmiM0z4BIBwxmxEfA1yoxh1MF/6KPBbh20a5vphGV0ictKlgQsbJs6D6SkR6iJpGbpwRsa6PFMNlg9T9pQ==, tarball: https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.18.3.tgz}
+    peerDependencies:
+      '@codemirror/language': ^6.0.0
+      '@codemirror/state': ^6.0.0
+      '@codemirror/view': ^6.0.0
+      '@lezer/common': ^1.0.0
+    dependencies:
+      '@codemirror/language': 6.10.6
+      '@codemirror/state': 6.4.1
+      '@codemirror/view': 6.35.0
+      '@lezer/common': 1.2.3
+    dev: true
+
+  /@codemirror/commands@6.7.1:
+    resolution: {integrity: sha512-llTrboQYw5H4THfhN4U3qCnSZ1SOJ60ohhz+SzU0ADGtwlc533DtklQP0vSFaQuCPDn3BPpOd1GbbnUtwNjsrw==, tarball: https://registry.npmmirror.com/@codemirror/commands/-/commands-6.7.1.tgz}
+    dependencies:
+      '@codemirror/language': 6.10.6
+      '@codemirror/state': 6.4.1
+      '@codemirror/view': 6.35.0
+      '@lezer/common': 1.2.3
+    dev: true
+
+  /@codemirror/language@6.10.6:
+    resolution: {integrity: sha512-KrsbdCnxEztLVbB5PycWXFxas4EOyk/fPAfruSOnDDppevQgid2XZ+KbJ9u+fDikP/e7MW7HPBTvTb8JlZK9vA==, tarball: https://registry.npmmirror.com/@codemirror/language/-/language-6.10.6.tgz}
+    dependencies:
+      '@codemirror/state': 6.4.1
+      '@codemirror/view': 6.35.0
+      '@lezer/common': 1.2.3
+      '@lezer/highlight': 1.2.1
+      '@lezer/lr': 1.4.2
+      style-mod: 4.1.2
+    dev: true
+
+  /@codemirror/lint@6.8.4:
+    resolution: {integrity: sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==, tarball: https://registry.npmmirror.com/@codemirror/lint/-/lint-6.8.4.tgz}
+    dependencies:
+      '@codemirror/state': 6.4.1
+      '@codemirror/view': 6.35.0
+      crelt: 1.0.6
     dev: true
 
-  /@camunda/element-templates-json-schema@0.4.0:
-    resolution: {integrity: sha512-M5xW61ba7z2maBxfoT4c1bjuLD8OIL7863et/hULiNG6+R/B9CZ4Qze1juuIfXv4zpF2fYSuUsTPkTtiZrcspQ==, tarball: https://registry.npmmirror.com/@camunda/element-templates-json-schema/-/element-templates-json-schema-0.4.0.tgz}
+  /@codemirror/state@6.4.1:
+    resolution: {integrity: sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==, tarball: https://registry.npmmirror.com/@codemirror/state/-/state-6.4.1.tgz}
+    dev: true
+
+  /@codemirror/view@6.35.0:
+    resolution: {integrity: sha512-I0tYy63q5XkaWsJ8QRv5h6ves7kvtrBWjBcnf/bzohFJQc5c14a1AQRdE8QpPF9eMp5Mq2FMm59TCj1gDfE7kw==, tarball: https://registry.npmmirror.com/@codemirror/view/-/view-6.35.0.tgz}
+    dependencies:
+      '@codemirror/state': 6.4.1
+      style-mod: 4.1.2
+      w3c-keyname: 2.2.8
     dev: true
 
   /@commitlint/cli@19.6.0(@types/node@20.17.9)(typescript@5.3.3):
@@ -2034,7 +2122,7 @@ packages:
       '@form-create/element-ui': 3.2.14(vue@3.5.12)
       '@form-create/utils': 3.2.14
       codemirror: 6.65.7
-      element-plus: 2.8.4(vue@3.5.12)
+      element-plus: 2.9.1(vue@3.5.12)
       vue: 3.5.12(typescript@5.3.3)
       vuedraggable: 4.1.0(vue@3.5.12)
     transitivePeerDependencies:
@@ -2274,6 +2362,29 @@ packages:
       '@jridgewell/sourcemap-codec': 1.5.0
     dev: true
 
+  /@lezer/common@1.2.3:
+    resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==, tarball: https://registry.npmmirror.com/@lezer/common/-/common-1.2.3.tgz}
+    dev: true
+
+  /@lezer/highlight@1.2.1:
+    resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==, tarball: https://registry.npmmirror.com/@lezer/highlight/-/highlight-1.2.1.tgz}
+    dependencies:
+      '@lezer/common': 1.2.3
+    dev: true
+
+  /@lezer/lr@1.4.2:
+    resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==, tarball: https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.2.tgz}
+    dependencies:
+      '@lezer/common': 1.2.3
+    dev: true
+
+  /@lezer/markdown@1.3.2:
+    resolution: {integrity: sha512-Wu7B6VnrKTbBEohqa63h5vxXjiC4pO5ZQJ/TDbhJxPQaaIoRD/6UVDhSDtVsCwVZV12vvN9KxuLL3ATMnlG0oQ==, tarball: https://registry.npmmirror.com/@lezer/markdown/-/markdown-1.3.2.tgz}
+    dependencies:
+      '@lezer/common': 1.2.3
+      '@lezer/highlight': 1.2.1
+    dev: true
+
   /@microsoft/fetch-event-source@2.0.1:
     resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==, tarball: https://registry.npmmirror.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz}
     dev: false
@@ -4264,6 +4375,11 @@ packages:
     resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==, tarball: https://registry.npmmirror.com/array-ify/-/array-ify-1.0.0.tgz}
     dev: true
 
+  /array-move@4.0.0:
+    resolution: {integrity: sha512-+RY54S8OuVvg94THpneQvFRmqWdAHeqtMzgMW6JNurHxe8rsS07cHQdfGkXnTUXiBcyZ0j3SiDIxxj0RPiqCkQ==, tarball: https://registry.npmmirror.com/array-move/-/array-move-4.0.0.tgz}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+    dev: true
+
   /array-union@2.1.0:
     resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==, tarball: https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz}
     engines: {node: '>=8'}
@@ -4446,21 +4562,23 @@ packages:
   /boolbase@1.0.0:
     resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==, tarball: https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz}
 
-  /bpmn-js-properties-panel@0.46.0(bpmn-js@8.10.0):
-    resolution: {integrity: sha512-8MlNvHklIZZQH9vtoKf0A0A1v0sHO4Iz19jGhHeX15czOOiCfdavjo+q23GHWNKzQA9347F91XYFcrnM6FO8zw==, tarball: https://registry.npmmirror.com/bpmn-js-properties-panel/-/bpmn-js-properties-panel-0.46.0.tgz}
+  /bpmn-js-properties-panel@5.23.0(@bpmn-io/properties-panel@3.25.0)(bpmn-js@17.11.1)(camunda-bpmn-js-behaviors@1.7.2)(diagram-js@12.8.1):
+    resolution: {integrity: sha512-4B27LM8oV14A2QWRvazV17h4NxbkNERcqU+AGJmxKImMlLhu9893MWR+pCdTQCTphBdBkuD8ksWm+1wVCedJ7g==, tarball: https://registry.npmmirror.com/bpmn-js-properties-panel/-/bpmn-js-properties-panel-5.23.0.tgz}
     peerDependencies:
-      bpmn-js: ^3.x || ^4.x || ^5.x || ^6.x || ^7.x || ^8.x
+      '@bpmn-io/properties-panel': '>= 3.7'
+      bpmn-js: '>= 11.5'
+      camunda-bpmn-js-behaviors: '>= 0.4'
+      diagram-js: '>= 11.9'
     dependencies:
-      '@bpmn-io/element-templates-validator': 0.2.0
-      '@bpmn-io/extract-process-variables': 0.4.5
-      bpmn-js: 8.10.0
+      '@bpmn-io/extract-process-variables': 0.8.0
+      '@bpmn-io/properties-panel': 3.25.0(@lezer/common@1.2.3)
+      array-move: 4.0.0
+      bpmn-js: 17.11.1
+      camunda-bpmn-js-behaviors: 1.7.2(bpmn-js@17.11.1)(camunda-bpmn-moddle@7.0.1)(zeebe-bpmn-moddle@1.7.0)
+      diagram-js: 12.8.1
       ids: 1.0.5
-      inherits: 2.0.4
-      lodash: 4.17.21
-      min-dom: 3.2.1
-      scroll-tabs: 1.0.1
-      selection-update: 0.1.2
-      semver: 6.3.1
+      min-dash: 4.2.2
+      min-dom: 4.2.1
     dev: true
 
   /bpmn-js-token-simulation@0.10.0:
@@ -4471,27 +4589,25 @@ packages:
       svg.js: 2.7.1
     dev: false
 
-  /bpmn-js@8.10.0:
-    resolution: {integrity: sha512-NozeOi01qL0ZdVq8+5hWZcikyEvgrP1yzCBqlhSufJdHFsnEMBCwn2bJJ0B/6JgX+IBwy1sk/Uw+Ds8rQ8vfrw==, tarball: https://registry.npmmirror.com/bpmn-js/-/bpmn-js-8.10.0.tgz}
+  /bpmn-js@17.11.1:
+    resolution: {integrity: sha512-ywCeTg5kvN8lYkU+fHE+YXTGlfKc55lRBn7zW3k1//toeMNPy/PS/uQiujRWdFhMrH5dbtDvlwWukNw2pjWw8Q==, tarball: https://registry.npmmirror.com/bpmn-js/-/bpmn-js-17.11.1.tgz}
     dependencies:
-      bpmn-moddle: 7.1.3
-      css.escape: 1.5.1
-      diagram-js: 7.9.0
-      diagram-js-direct-editing: 1.8.0(diagram-js@7.9.0)
+      bpmn-moddle: 8.1.0
+      diagram-js: 14.11.3
+      diagram-js-direct-editing: 3.2.0(diagram-js@14.11.3)
       ids: 1.0.5
-      inherits: 2.0.4
-      min-dash: 3.8.1
-      min-dom: 3.2.1
-      object-refs: 0.3.0
-      tiny-svg: 2.2.4
+      inherits-browser: 0.1.0
+      min-dash: 4.2.2
+      min-dom: 4.2.1
+      tiny-svg: 3.1.3
     dev: true
 
-  /bpmn-moddle@7.1.3:
-    resolution: {integrity: sha512-ZcBfw0NSOdYTSXFKEn7MOXHItz7VfLZTrFYKO8cK6V8ZzGjCcdiLIOiw7Lctw1PJsihhLiZQS8Htj2xKf+NwCg==, tarball: https://registry.npmmirror.com/bpmn-moddle/-/bpmn-moddle-7.1.3.tgz}
+  /bpmn-moddle@8.1.0:
+    resolution: {integrity: sha512-yI5OAFfYVJwViKTsTsonVfCBPtB3MlefADUORwNIxxBOMp21vnoxuxsdgUWlPH/dvAEZh/+mr8UtqOBNu8NC5Q==, tarball: https://registry.npmmirror.com/bpmn-moddle/-/bpmn-moddle-8.1.0.tgz}
     dependencies:
-      min-dash: 3.8.1
-      moddle: 5.0.4
-      moddle-xml: 9.0.6
+      min-dash: 4.2.2
+      moddle: 6.2.3
+      moddle-xml: 10.1.0
     dev: true
 
   /brace-expansion@1.1.11:
@@ -4598,9 +4714,22 @@ packages:
     engines: {node: '>=6'}
     dev: false
 
+  /camunda-bpmn-js-behaviors@1.7.2(bpmn-js@17.11.1)(camunda-bpmn-moddle@7.0.1)(zeebe-bpmn-moddle@1.7.0):
+    resolution: {integrity: sha512-xjLJHc18T40tcYu4JCeYDo1wR5i9+ZqcVnXVP6c4ooAe2gKISbBvFc07gqGpqiwm7TpEBvUfDj3PrRr+ofaf4w==, tarball: https://registry.npmmirror.com/camunda-bpmn-js-behaviors/-/camunda-bpmn-js-behaviors-1.7.2.tgz}
+    peerDependencies:
+      bpmn-js: '>= 9'
+      camunda-bpmn-moddle: '>= 7'
+      zeebe-bpmn-moddle: '>= 0.18'
+    dependencies:
+      bpmn-js: 17.11.1
+      camunda-bpmn-moddle: 7.0.1
+      ids: 1.0.5
+      min-dash: 4.2.2
+      zeebe-bpmn-moddle: 1.7.0
+    dev: true
+
   /camunda-bpmn-moddle@7.0.1:
     resolution: {integrity: sha512-Br8Diu6roMpziHdpl66Dhnm0DTnCFMrSD9zwLV08LpD52QA0UsXxU87XfHf08HjuB7ly0Hd1bvajZRpf9hbmYQ==, tarball: https://registry.npmmirror.com/camunda-bpmn-moddle/-/camunda-bpmn-moddle-7.0.1.tgz}
-    dev: false
 
   /caniuse-lite@1.0.30001684:
     resolution: {integrity: sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==, tarball: https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz}
@@ -4686,6 +4815,10 @@ packages:
       static-extend: 0.1.2
     dev: true
 
+  /classnames@2.5.1:
+    resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==, tarball: https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz}
+    dev: true
+
   /cli-cursor@5.0.0:
     resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==, tarball: https://registry.npmmirror.com/cli-cursor/-/cli-cursor-5.0.0.tgz}
     engines: {node: '>=18'}
@@ -4726,7 +4859,6 @@ packages:
   /clsx@2.1.1:
     resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==, tarball: https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz}
     engines: {node: '>=6'}
-    dev: false
 
   /codemirror@6.65.7:
     resolution: {integrity: sha512-HcfnUFJwI2FvH73YWVbbMh7ObWxZiHIycEhv9ZEXy6e8ZKDjtZKbbYFUtsLN46HFXPvU5V2Uvc2d55Z//oFW5A==, tarball: https://registry.npmmirror.com/codemirror/-/codemirror-6.65.7.tgz}
@@ -4820,10 +4952,10 @@ packages:
 
   /component-event@0.1.4:
     resolution: {integrity: sha512-GMwOG8MnUHP1l8DZx1ztFO0SJTFnIzZnBDkXAj8RM2ntV2A6ALlDxgbMY1Fvxlg6WPQ+5IM/a6vg4PEYbjg/Rw==, tarball: https://registry.npmmirror.com/component-event/-/component-event-0.1.4.tgz}
+    dev: false
 
   /component-event@0.2.1:
     resolution: {integrity: sha512-wGA++isMqiDq1jPYeyv2as/Bt/u+3iLW0rEa+8NQ82jAv3TgqMiCM+B2SaBdn2DfLilLjjq736YcezihRYhfxw==, tarball: https://registry.npmmirror.com/component-event/-/component-event-0.2.1.tgz}
-    dev: false
 
   /component-indexof@0.0.3:
     resolution: {integrity: sha512-puDQKvx/64HZXb4hBwIcvQLaLgux8o1CbWl39s41hrIIZDl1lJiD5jc22gj3RBeGK0ovxALDYpIbyjqDUUl0rw==, tarball: https://registry.npmmirror.com/component-indexof/-/component-indexof-0.0.3.tgz}
@@ -4949,6 +5081,10 @@ packages:
       typescript: 5.3.3
     dev: true
 
+  /crelt@1.0.6:
+    resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==, tarball: https://registry.npmmirror.com/crelt/-/crelt-1.0.6.tgz}
+    dev: true
+
   /cropperjs@1.6.2:
     resolution: {integrity: sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==, tarball: https://registry.npmmirror.com/cropperjs/-/cropperjs-1.6.2.tgz}
     dev: false
@@ -5027,10 +5163,6 @@ packages:
     resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==, tarball: https://registry.npmmirror.com/css-what/-/css-what-6.1.0.tgz}
     engines: {node: '>= 6'}
 
-  /css.escape@1.5.1:
-    resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==, tarball: https://registry.npmmirror.com/css.escape/-/css.escape-1.5.1.tgz}
-    dev: true
-
   /cssesc@3.0.0:
     resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==, tarball: https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz}
     engines: {node: '>=4'}
@@ -5455,14 +5587,14 @@ packages:
     dev: true
     optional: true
 
-  /diagram-js-direct-editing@1.8.0(diagram-js@7.9.0):
-    resolution: {integrity: sha512-B4Xj+PJfgBjbPEzT3uZQEkZI5xHFB0Izc+7BhDFuHidzrEMzQKZrFGdA3PqfWhReHf3dp+iB6Tt11G9eGNjKMw==, tarball: https://registry.npmmirror.com/diagram-js-direct-editing/-/diagram-js-direct-editing-1.8.0.tgz}
+  /diagram-js-direct-editing@3.2.0(diagram-js@14.11.3):
+    resolution: {integrity: sha512-+pyxeQGBSdLiZX0/tmmsm2qZSvm9YtVzod5W3RMHSTR7VrkUMD6E7EX/W9JQv3ebxO7oIdqFmytmNDDpSHnYEw==, tarball: https://registry.npmmirror.com/diagram-js-direct-editing/-/diagram-js-direct-editing-3.2.0.tgz}
     peerDependencies:
       diagram-js: '*'
     dependencies:
-      diagram-js: 7.9.0
-      min-dash: 3.8.1
-      min-dom: 3.2.1
+      diagram-js: 14.11.3
+      min-dash: 4.2.2
+      min-dom: 4.2.1
     dev: true
 
   /diagram-js@12.8.1:
@@ -5478,29 +5610,28 @@ packages:
       object-refs: 0.3.0
       path-intersection: 2.2.1
       tiny-svg: 3.1.3
-    dev: false
 
-  /diagram-js@7.9.0:
-    resolution: {integrity: sha512-o1yUtX5TXV1pmpevP55gxU/AEG6nCidOXGs/HLuxNXG0zMZ3jQta7kMqRxTK93rNw/XuHmP1eMOwdvdJ2RP5qA==, tarball: https://registry.npmmirror.com/diagram-js/-/diagram-js-7.9.0.tgz}
+  /diagram-js@14.11.3:
+    resolution: {integrity: sha512-Seq9BHAXfzKS60L4v4Gvgvv72wOtvrfJQAyyPm9pntSZDMzjoodPSXnEUPud1G2zVCMGEUUW++s0reEdaWgkXA==, tarball: https://registry.npmmirror.com/diagram-js/-/diagram-js-14.11.3.tgz}
     dependencies:
-      css.escape: 1.5.1
-      didi: 5.2.1
-      hammerjs: 2.0.8
-      inherits: 2.0.4
-      min-dash: 3.8.1
-      min-dom: 3.2.1
-      object-refs: 0.3.0
-      path-intersection: 2.2.1
-      tiny-svg: 2.2.4
+      '@bpmn-io/diagram-js-ui': 0.2.3
+      clsx: 2.1.1
+      didi: 10.2.2
+      inherits-browser: 0.1.0
+      min-dash: 4.2.2
+      min-dom: 4.2.1
+      object-refs: 0.4.0
+      path-intersection: 3.1.0
+      tiny-svg: 3.1.3
     dev: true
 
-  /didi@5.2.1:
-    resolution: {integrity: sha512-IKNnajUlD4lWMy/Q9Emkk7H1qnzREgY4UyE3IhmOi/9IKua0JYtYldk928bOdt1yNxN8EiOy1sqtSozEYsmjCg==, tarball: https://registry.npmmirror.com/didi/-/didi-5.2.1.tgz}
+  /didi@10.2.2:
+    resolution: {integrity: sha512-l8NYkYFXV1izHI65EyT8EXOjUZtKmQkHLTT89cSP7HU5J/G7AOj0dXKtLc04EXYlga99PBY18IPjOeZ+c3DI4w==, tarball: https://registry.npmmirror.com/didi/-/didi-10.2.2.tgz}
+    engines: {node: '>= 16'}
     dev: true
 
   /didi@9.0.2:
     resolution: {integrity: sha512-q2+aj+lnJcUweV7A9pdUrwFr4LHVmRPwTmQLtHPFz4aT7IBoryN6Iy+jmFku+oIzr5ebBkvtBCOb87+dJhb7bg==, tarball: https://registry.npmmirror.com/didi/-/didi-9.0.2.tgz}
-    dev: false
 
   /dijkstrajs@1.0.3:
     resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==, tarball: https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz}
@@ -5585,6 +5716,11 @@ packages:
   /domify@1.4.2:
     resolution: {integrity: sha512-m4yreHcUWHBncGVV7U+yQzc12vIlq0jMrtHZ5mW6dQMiL/7skSYNVX9wqKwOtyO9SGCgevrAFEgOCAHmamHTUA==, tarball: https://registry.npmmirror.com/domify/-/domify-1.4.2.tgz}
 
+  /domify@2.0.0:
+    resolution: {integrity: sha512-rmvrrmWQPD/X1A/nPBfIVg4r05792QdG9Z4Prk6oQG0F9zBMDkr0GKAdds1wjb2dq1rTz/ywc4ZxpZbgz0tttg==, tarball: https://registry.npmmirror.com/domify/-/domify-2.0.0.tgz}
+    engines: {node: '>=18'}
+    dev: true
+
   /dompurify@3.2.1:
     resolution: {integrity: sha512-NBHEsc0/kzRYQd+AY6HR6B/IgsqzBABrqJbpCDQII/OK6h7B7LXzweZTDsqSW2LkTRpoxf18YUP+YjGySk6B3w==, tarball: https://registry.npmmirror.com/dompurify/-/dompurify-3.2.1.tgz}
     optionalDependencies:
@@ -5659,8 +5795,8 @@ packages:
     resolution: {integrity: sha512-nz88NNBsD7kQSAGGJyp8hS6xSPtWwqNogA0mjtc2nUYeEf3nURK9qpV18TuBdDmEDgVWotS8Wkzf+V52dSQ/LQ==, tarball: https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.67.tgz}
     dev: true
 
-  /element-plus@2.8.4(vue@3.5.12):
-    resolution: {integrity: sha512-ZlVAdUOoJliv4kW3ntWnnSHMT+u/Os7mXJjk2xzOlqNeHaI2/ozlF+R58ZCEak8ZnDi6+5A2viWEYRsq64IuiA==, tarball: https://registry.npmmirror.com/element-plus/-/element-plus-2.8.4.tgz}
+  /element-plus@2.9.1(vue@3.5.12):
+    resolution: {integrity: sha512-9Agqf/jt4Ugk7EZ6C5LME71sgkvauPCsnvJN12Xid2XVobjufxMGpRE4L7pS4luJMOmFAH3J0NgYEGZT5r+NDg==, tarball: https://registry.npmmirror.com/element-plus/-/element-plus-2.9.1.tgz}
     peerDependencies:
       vue: ^3.2.0
     dependencies:
@@ -6228,6 +6364,34 @@ packages:
       picomatch: 4.0.2
     dev: true
 
+  /feelers@1.4.0:
+    resolution: {integrity: sha512-CGa/7ILuqoqTaeYeoKsg/4tzu2es9sEEJTmSjdu0lousZBw4V9gcYhHYFNmbrSrKmbAVfOzj6/DsymGJWFIOeg==, tarball: https://registry.npmmirror.com/feelers/-/feelers-1.4.0.tgz}
+    dependencies:
+      '@bpmn-io/cm-theme': 0.1.0-alpha.2
+      '@bpmn-io/feel-lint': 1.3.1
+      '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3)
+      '@codemirror/commands': 6.7.1
+      '@codemirror/language': 6.10.6
+      '@codemirror/lint': 6.8.4
+      '@codemirror/state': 6.4.1
+      '@codemirror/view': 6.35.0
+      '@lezer/common': 1.2.3
+      '@lezer/highlight': 1.2.1
+      '@lezer/lr': 1.4.2
+      '@lezer/markdown': 1.3.2
+      feelin: 3.2.0
+      lezer-feel: 1.4.0
+      min-dom: 5.1.1
+    dev: true
+
+  /feelin@3.2.0:
+    resolution: {integrity: sha512-GFDbHsTYk7YXO1tyw1dOjb7IODeAZvNIosdGZThUwPx5XcD/XhO0hnPZXsIbAzSsIdrgGlTEEdby9fZ2gixysA==, tarball: https://registry.npmmirror.com/feelin/-/feelin-3.2.0.tgz}
+    dependencies:
+      '@lezer/lr': 1.4.2
+      lezer-feel: 1.4.0
+      luxon: 3.5.0
+    dev: true
+
   /file-entry-cache@6.0.1:
     resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==, tarball: https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz}
     engines: {node: ^10.12.0 || >=12.0.0}
@@ -6311,6 +6475,12 @@ packages:
     resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==, tarball: https://registry.npmmirror.com/flatted/-/flatted-3.3.2.tgz}
     dev: true
 
+  /focus-trap@7.6.2:
+    resolution: {integrity: sha512-9FhUxK1hVju2+AiQIDJ5Dd//9R2n2RAfJ0qfhF4IHGHgcoEUTMpbTeG/zbEuwaiYXfuAH6XE0/aCyxDdRM+W5w==, tarball: https://registry.npmmirror.com/focus-trap/-/focus-trap-7.6.2.tgz}
+    dependencies:
+      tabbable: 6.2.0
+    dev: true
+
   /follow-redirects@1.15.9(debug@4.3.7):
     resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==, tarball: https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz}
     engines: {node: '>=4.0'}
@@ -6674,7 +6844,6 @@ packages:
 
   /htm@3.1.1:
     resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==, tarball: https://registry.npmmirror.com/htm/-/htm-3.1.1.tgz}
-    dev: false
 
   /html-tags@3.3.1:
     resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==, tarball: https://registry.npmmirror.com/html-tags/-/html-tags-3.3.1.tgz}
@@ -6772,10 +6941,6 @@ packages:
     engines: {node: '>=8'}
     dev: true
 
-  /indexof@0.0.1:
-    resolution: {integrity: sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==, tarball: https://registry.npmmirror.com/indexof/-/indexof-0.0.1.tgz}
-    dev: true
-
   /individual@2.0.0:
     resolution: {integrity: sha512-pWt8hBCqJsUWI/HtcfWod7+N9SgAqyPEaF7JQjwzjn5vGrpg6aQ5qeAFQ7dx//UH4J1O+7xqew+gCeeFt6xN/g==, tarball: https://registry.npmmirror.com/individual/-/individual-2.0.0.tgz}
     dev: false
@@ -6790,7 +6955,6 @@ packages:
 
   /inherits-browser@0.1.0:
     resolution: {integrity: sha512-CJHHvW3jQ6q7lzsXPpapLdMx5hDpSF3FSh45pwsj6bKxJJ8Nl8v43i5yXnr3BdfOimGHKyniewQtnAIp3vyJJw==, tarball: https://registry.npmmirror.com/inherits-browser/-/inherits-browser-0.1.0.tgz}
-    dev: false
 
   /inherits@2.0.4:
     resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==, tarball: https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz}
@@ -7209,10 +7373,6 @@ packages:
     resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==, tarball: https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz}
     dev: true
 
-  /json-source-map@0.6.1:
-    resolution: {integrity: sha512-1QoztHPsMQqhDq0hlXY5ZqcEdUzxQEIxgFkKl4WUp2pgShObl+9ovi4kRh2TfvAfxAoHOJ9vIMEqk3k4iex7tg==, tarball: https://registry.npmmirror.com/json-source-map/-/json-source-map-0.6.1.tgz}
-    dev: true
-
   /json-stable-stringify-without-jsonify@1.0.1:
     resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==, tarball: https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz}
     dev: true
@@ -7302,6 +7462,17 @@ packages:
     resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==, tarball: https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz}
     dev: true
 
+  /lang-feel@2.2.0:
+    resolution: {integrity: sha512-Ebo5nftYsMfJzB3Ny8Oy4oaDXZXb5x61qtVVmKv6aImvAZUbT76mD60ZbEilizjZQzsR2CcU1iMK5sacIa1NVA==, tarball: https://registry.npmmirror.com/lang-feel/-/lang-feel-2.2.0.tgz}
+    dependencies:
+      '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3)
+      '@codemirror/language': 6.10.6
+      '@codemirror/state': 6.4.1
+      '@codemirror/view': 6.35.0
+      '@lezer/common': 1.2.3
+      lezer-feel: 1.4.0
+    dev: true
+
   /levn@0.4.1:
     resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==, tarball: https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz}
     engines: {node: '>= 0.8.0'}
@@ -7310,6 +7481,14 @@ packages:
       type-check: 0.4.0
     dev: true
 
+  /lezer-feel@1.4.0:
+    resolution: {integrity: sha512-kNxG7O38gwpuYy+C3JCRxQNTCE2qu9uTuH5dE3EGVnRhIQMe6rPDz0S8t3urLEOsMud6HI795m6zX2ujfUaqTw==, tarball: https://registry.npmmirror.com/lezer-feel/-/lezer-feel-1.4.0.tgz}
+    dependencies:
+      '@lezer/highlight': 1.2.1
+      '@lezer/lr': 1.4.2
+      min-dash: 4.2.2
+    dev: true
+
   /lilconfig@3.1.2:
     resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==, tarball: https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.2.tgz}
     engines: {node: '>=14'}
@@ -7513,6 +7692,11 @@ packages:
       yallist: 3.1.1
     dev: true
 
+  /luxon@3.5.0:
+    resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==, tarball: https://registry.npmmirror.com/luxon/-/luxon-3.5.0.tgz}
+    engines: {node: '>=12'}
+    dev: true
+
   /m3u8-parser@4.8.0:
     resolution: {integrity: sha512-UqA2a/Pw3liR6Df3gwxrqghCP17OpPlQj6RBPLYygf/ZSQ4MoSgvdvhvt35qV+3NaaA0FSZx93Ix+2brT1U7cA==, tarball: https://registry.npmmirror.com/m3u8-parser/-/m3u8-parser-4.8.0.tgz}
     dependencies:
@@ -7608,10 +7792,6 @@ packages:
       markmap-common: 0.16.0
     dev: false
 
-  /matches-selector@1.2.0:
-    resolution: {integrity: sha512-c4vLwYWyl+Ji+U43eU/G5FwxWd4ZH0ePUsFs5y0uwD9HUEFBXUQ1zUUan+78IpRD+y4pUfG0nAzNM292K7ItvA==, tarball: https://registry.npmmirror.com/matches-selector/-/matches-selector-1.2.0.tgz}
-    dev: true
-
   /mathml-tag-names@2.1.3:
     resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==, tarball: https://registry.npmmirror.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz}
     dev: true
@@ -7721,10 +7901,10 @@ packages:
 
   /min-dash@3.8.1:
     resolution: {integrity: sha512-evumdlmIlg9mbRVPbC4F5FuRhNmcMS5pvuBUbqb1G9v09Ro0ImPEgz5n3khir83lFok1inKqVDjnKEg3GpDxQg==, tarball: https://registry.npmmirror.com/min-dash/-/min-dash-3.8.1.tgz}
+    dev: false
 
   /min-dash@4.2.2:
     resolution: {integrity: sha512-qbhSYUxk6mBaF096B3JOQSumXbKWHenmT97cSpdNzgkWwGjhjhE/KZODCoDNhI2I4C9Cb6R/Q13S4BYkUSXoXQ==, tarball: https://registry.npmmirror.com/min-dash/-/min-dash-4.2.2.tgz}
-    dev: false
 
   /min-document@2.19.0:
     resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==, tarball: https://registry.npmmirror.com/min-document/-/min-document-2.19.0.tgz}
@@ -7744,23 +7924,19 @@ packages:
       domify: 1.4.2
     dev: false
 
-  /min-dom@3.2.1:
-    resolution: {integrity: sha512-v6YCmnDzxk4rRJntWTUiwggLupPw/8ZSRqUq0PDaBwVZEO/wYzCH4SKVBV+KkEvf3u0XaWHly5JEosPtqRATZA==, tarball: https://registry.npmmirror.com/min-dom/-/min-dom-3.2.1.tgz}
-    dependencies:
-      component-event: 0.1.4
-      domify: 1.4.2
-      indexof: 0.0.1
-      matches-selector: 1.2.0
-      min-dash: 3.8.1
-    dev: true
-
   /min-dom@4.2.1:
     resolution: {integrity: sha512-TMoL8SEEIhUWYgkj7XMSgxmwSyGI+4fP2KFFGnN3FbHfbGHVdsLYSz8LoIsgPhz4dWRmLvxWWSMgzZMJW5sZuA==, tarball: https://registry.npmmirror.com/min-dom/-/min-dom-4.2.1.tgz}
     dependencies:
       component-event: 0.2.1
       domify: 1.4.2
       min-dash: 4.2.2
-    dev: false
+
+  /min-dom@5.1.1:
+    resolution: {integrity: sha512-GaKUlguMAofd3OJsB0OkP17i5kucKqErgVCJxPawO9l5NwIPnr28SAr99zzlzMCWWljISBYrnZVWdE2Q92YGFQ==, tarball: https://registry.npmmirror.com/min-dom/-/min-dom-5.1.1.tgz}
+    dependencies:
+      domify: 2.0.0
+      min-dash: 4.2.2
+    dev: true
 
   /minimatch@3.1.2:
     resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==, tarball: https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz}
@@ -7798,10 +7974,6 @@ packages:
     engines: {node: '>=16 || 14 >=14.17'}
     dev: true
 
-  /mitt@1.2.0:
-    resolution: {integrity: sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==, tarball: https://registry.npmmirror.com/mitt/-/mitt-1.2.0.tgz}
-    dev: true
-
   /mitt@3.0.1:
     resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==, tarball: https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz}
     dev: false
@@ -7823,18 +7995,18 @@ packages:
       ufo: 1.5.4
     dev: true
 
-  /moddle-xml@9.0.6:
-    resolution: {integrity: sha512-tl0reHpsY/aKlLGhXeFlQWlYAQHFxTkFqC8tq8jXRYpQSnLVw13T6swMaourLd7EXqHdWsc+5ggsB+fEep6xZQ==, tarball: https://registry.npmmirror.com/moddle-xml/-/moddle-xml-9.0.6.tgz}
+  /moddle-xml@10.1.0:
+    resolution: {integrity: sha512-erWckwLt+dYskewKXJso9u+aAZ5172lOiYxSOqKCPTy7L/xmqH1PoeoA7eVC7oJTt3PqF5TkZzUmbjGH6soQBg==, tarball: https://registry.npmmirror.com/moddle-xml/-/moddle-xml-10.1.0.tgz}
     dependencies:
-      min-dash: 3.8.1
-      moddle: 5.0.4
+      min-dash: 4.2.2
+      moddle: 6.2.3
       saxen: 8.1.2
     dev: true
 
-  /moddle@5.0.4:
-    resolution: {integrity: sha512-Kjb+hjuzO+YlojNGxEUXvdhLYTHTtAABDlDcJTtTcn5MbJF9Zkv4I1Fyvp3Ypmfgg1EfHDZ3PsCQTuML9JD6wg==, tarball: https://registry.npmmirror.com/moddle/-/moddle-5.0.4.tgz}
+  /moddle@6.2.3:
+    resolution: {integrity: sha512-bLVN+ZHL3aKnhxc19XtjUfvdJsS3EsiEJC7bT6YPD11qYmTzvsxrGgyYz1Ouof7TZuGw0lDJ1OLmEnxcpQWk3Q==, tarball: https://registry.npmmirror.com/moddle/-/moddle-6.2.3.tgz}
     dependencies:
-      min-dash: 3.8.1
+      min-dash: 4.2.2
     dev: true
 
   /mpd-parser@0.22.1:
@@ -7994,6 +8166,10 @@ packages:
   /object-refs@0.3.0:
     resolution: {integrity: sha512-eP0ywuoWOaDoiake/6kTJlPJhs+k0qNm4nYRzXLNHj6vh+5M3i9R1epJTdxIPGlhWc4fNRQ7a6XJNCX+/L4FOQ==, tarball: https://registry.npmmirror.com/object-refs/-/object-refs-0.3.0.tgz}
 
+  /object-refs@0.4.0:
+    resolution: {integrity: sha512-6kJqKWryKZmtte6QYvouas0/EIJKPI1/MMIuRsiBlNuhIMfqYTggzX2F1AJ2+cDs288xyi9GL7FyasHINR98BQ==, tarball: https://registry.npmmirror.com/object-refs/-/object-refs-0.4.0.tgz}
+    dev: true
+
   /object-visit@1.0.1:
     resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==, tarball: https://registry.npmmirror.com/object-visit/-/object-visit-1.0.1.tgz}
     engines: {node: '>=0.10.0'}
@@ -8164,6 +8340,11 @@ packages:
   /path-intersection@2.2.1:
     resolution: {integrity: sha512-9u8xvMcSfuOiStv9bPdnRJQhGQXLKurew94n4GPQCdH1nj9QKC9ObbNoIpiRq8skiOBxKkt277PgOoFgAt3/rA==, tarball: https://registry.npmmirror.com/path-intersection/-/path-intersection-2.2.1.tgz}
 
+  /path-intersection@3.1.0:
+    resolution: {integrity: sha512-3xS3lvv/vuwm5aH2BVvNRvnvwR2Drde7jQClKpCXTYXIMMjcw/EnMhzCgeHwqbCpzi760PEfAkU53vSIlrNr9A==, tarball: https://registry.npmmirror.com/path-intersection/-/path-intersection-3.1.0.tgz}
+    engines: {node: '>= 14.20'}
+    dev: true
+
   /path-is-absolute@1.0.1:
     resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==, tarball: https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz}
     engines: {node: '>=0.10.0'}
@@ -8415,7 +8596,6 @@ packages:
 
   /preact@10.25.0:
     resolution: {integrity: sha512-6bYnzlLxXV3OSpUxLdaxBmE7PMOu0aR3pG6lryK/0jmvcDFPlcXGQAt5DpK3RITWiDrfYZRI0druyaK/S9kYLg==, tarball: https://registry.npmmirror.com/preact/-/preact-10.25.0.tgz}
-    dev: false
 
   /prelude-ls@1.2.1:
     resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==, tarball: https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz}
@@ -8872,22 +9052,10 @@ packages:
       compute-scroll-into-view: 1.0.20
     dev: false
 
-  /scroll-tabs@1.0.1:
-    resolution: {integrity: sha512-W4xjEwNS4QAyQnaJ450vQTcKpbnalBAfsTDV926WrxEMOqjyj2To8uv2d0Cp0oxMdk5TkygtzXmctPNc2zgBcg==, tarball: https://registry.npmmirror.com/scroll-tabs/-/scroll-tabs-1.0.1.tgz}
-    dependencies:
-      min-dash: 3.8.1
-      min-dom: 3.2.1
-      mitt: 1.2.0
-    dev: true
-
   /scule@1.3.0:
     resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==, tarball: https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz}
     dev: true
 
-  /selection-update@0.1.2:
-    resolution: {integrity: sha512-4jzoJNh7VT2s2tvm/kUSskSw7pD0BVcrrGccbfOMK+3AXLBPz6nIy1yo+pbXgvNoTNII96Pq92+sAY+rF0LUAA==, tarball: https://registry.npmmirror.com/selection-update/-/selection-update-0.1.2.tgz}
-    dev: true
-
   /semver@6.3.1:
     resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==, tarball: https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz}
     hasBin: true
@@ -9245,6 +9413,10 @@ packages:
     resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==, tarball: https://registry.npmmirror.com/strnum/-/strnum-1.0.5.tgz}
     dev: false
 
+  /style-mod@4.1.2:
+    resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==, tarball: https://registry.npmmirror.com/style-mod/-/style-mod-4.1.2.tgz}
+    dev: true
+
   /stylelint-config-html@1.1.0(postcss-html@1.7.0)(stylelint@16.11.0):
     resolution: {integrity: sha512-IZv4IVESjKLumUGi+HWeb7skgO6/g4VMuAYrJdlqQFndgbj6WJAXPhaysvBiXefX79upBdQVumgYcdd17gCpjQ==, tarball: https://registry.npmmirror.com/stylelint-config-html/-/stylelint-config-html-1.1.0.tgz}
     engines: {node: ^12 || >=14}
@@ -9427,6 +9599,10 @@ packages:
     resolution: {integrity: sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==, tarball: https://registry.npmmirror.com/systemjs/-/systemjs-6.15.1.tgz}
     dev: true
 
+  /tabbable@6.2.0:
+    resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==, tarball: https://registry.npmmirror.com/tabbable/-/tabbable-6.2.0.tgz}
+    dev: true
+
   /table@6.8.2:
     resolution: {integrity: sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==, tarball: https://registry.npmmirror.com/table/-/table-6.8.2.tgz}
     engines: {node: '>=10.0.0'}
@@ -9462,13 +9638,8 @@ packages:
     resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==, tarball: https://registry.npmmirror.com/through/-/through-2.3.8.tgz}
     dev: true
 
-  /tiny-svg@2.2.4:
-    resolution: {integrity: sha512-NOi39lBknf4UdDEahNkbEAJnzhu1ZcN2j75IS2vLRmIhsfxdZpTChfLKBcN1ShplVmPIXJAIafk6YY5/Aa80lQ==, tarball: https://registry.npmmirror.com/tiny-svg/-/tiny-svg-2.2.4.tgz}
-    dev: true
-
   /tiny-svg@3.1.3:
     resolution: {integrity: sha512-9mwnPqXInRsBmH/DO6NMxBE++9LsqpVXQSSTZGc5bomoKKvL5OX/Hlotw7XVXP6XLRcHWIzZpxfovGqWKgCypQ==, tarball: https://registry.npmmirror.com/tiny-svg/-/tiny-svg-3.1.3.tgz}
-    dev: false
 
   /tiny-warning@1.0.3:
     resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==, tarball: https://registry.npmmirror.com/tiny-warning/-/tiny-warning-1.0.3.tgz}
@@ -10177,6 +10348,10 @@ packages:
       vue: 3.5.12(typescript@5.3.3)
     dev: false
 
+  /w3c-keyname@2.2.8:
+    resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==, tarball: https://registry.npmmirror.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz}
+    dev: true
+
   /wangeditor@4.7.15:
     resolution: {integrity: sha512-aPTdREd8BxXVyJ5MI+LU83FQ7u1EPd341iXIorRNYSOvoimNoZ4nPg+yn3FGbB93/owEa6buLw8wdhYnMCJQLg==, tarball: https://registry.npmmirror.com/wangeditor/-/wangeditor-4.7.15.tgz}
     dependencies:
@@ -10429,6 +10604,10 @@ packages:
     engines: {node: '>=12.20'}
     dev: true
 
+  /zeebe-bpmn-moddle@1.7.0:
+    resolution: {integrity: sha512-eZ6OXSt0c4n9V/oN/46gTlwDIS3GhWQLt9jbM5uS/YryB4yN8wdrrKrtw+TpyNy0SSKWXNDHyC83nCA2blPO3Q==, tarball: https://registry.npmmirror.com/zeebe-bpmn-moddle/-/zeebe-bpmn-moddle-1.7.0.tgz}
+    dev: true
+
   /zrender@5.6.0:
     resolution: {integrity: sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==, tarball: https://registry.npmmirror.com/zrender/-/zrender-5.6.0.tgz}
     dependencies:

+ 8 - 7
src/api/login/index.ts

@@ -22,11 +22,6 @@ export const register = (data: RegisterVO) => {
   return request.post({ url: '/system/auth/register', data })
 }
 
-// 刷新访问令牌
-export const refreshToken = () => {
-  return request.post({ url: '/system/auth/refresh-token?refreshToken=' + getRefreshToken() })
-}
-
 // 使用租户名,获得租户编号
 export const getTenantIdByName = (name: string) => {
   return request.get({ url: '/system/tenant/get-id-by-name?name=' + name })
@@ -76,11 +71,17 @@ export const socialAuthRedirect = (type: number, redirectUri: string) => {
   })
 }
 // 获取验证图片以及 token
-export const getCode = (data) => {
+export const getCode = (data: any) => {
+  debugger
   return request.postOriginal({ url: 'system/captcha/get', data })
 }
 
 // 滑动或者点选验证
-export const reqCheck = (data) => {
+export const reqCheck = (data: any) => {
   return request.postOriginal({ url: 'system/captcha/check', data })
 }
+
+// 通过短信重置密码
+export const smsResetPassword = (data: any) => {
+  return request.post({ url: '/system/auth/sms-reset-password', data })
+}

+ 1 - 0
src/assets/svgs/bpm/delay.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1735905505218" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4277" width="200" height="200" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M561.778 454.929h198.117c0.549 0 0.994 0.444 0.994 1.001v97.553a0.998 0.998 0 0 1-0.994 1.001H463.224a1.005 1.005 0 0 1-1.002-1V207.04c0-0.552 0.444-1 1.002-1h97.552c0.553 0 1.002 0.455 1.002 1v247.89zM512 952.706c-247.424 0-448-200.576-448-448 0-247.423 200.576-448 448-448s448 200.577 448 448c0 247.424-200.576 448-448 448z m0-99.555c192.44 0 348.444-156.004 348.444-348.445 0-192.44-156.003-348.444-348.444-348.444-192.44 0-348.444 156.004-348.444 348.444 0 192.441 156.003 348.445 348.444 348.445z" fill="#3296FA" p-id="4278"></path></svg>

+ 4 - 0
src/components/Echart/src/Echart.vue

@@ -9,6 +9,10 @@ import { useAppStore } from '@/store/modules/app'
 import { isString } from '@/utils/is'
 import { useDesign } from '@/hooks/web/useDesign'
 
+import 'echarts/lib/component/markPoint'
+import 'echarts/lib/component/markLine'
+import 'echarts/lib/component/markArea'
+
 defineOptions({ name: 'EChart' })
 
 const { getPrefixCls, variables } = useDesign()

+ 3 - 3
src/components/Editor/src/Editor.vue

@@ -6,7 +6,7 @@ import { propTypes } from '@/utils/propTypes'
 import { isNumber } from '@/utils/is'
 import { ElMessage } from 'element-plus'
 import { useLocaleStore } from '@/store/modules/locale'
-import { getAccessToken, getTenantId } from '@/utils/auth'
+import { getRefreshToken, getTenantId } from '@/utils/auth'
 import { getUploadUrl } from '@/components/UploadFile/src/useUpload'
 
 defineOptions({ name: 'Editor' })
@@ -100,7 +100,7 @@ const editorConfig = computed((): IEditorConfig => {
           // 自定义增加 http  header
           headers: {
             Accept: '*',
-            Authorization: 'Bearer ' + getAccessToken(),
+            Authorization: 'Bearer ' + getRefreshToken(), // 使用 getRefreshToken() 方法,而不使用 getAccessToken() 方法的原因:Editor 无法方便的刷新访问令牌
             'tenant-id': getTenantId()
           },
 
@@ -148,7 +148,7 @@ const editorConfig = computed((): IEditorConfig => {
           // 自定义增加 http  header
           headers: {
             Accept: '*',
-            Authorization: 'Bearer ' + getAccessToken(),
+            Authorization: 'Bearer ' + getRefreshToken(), // 使用 getRefreshToken() 方法,而不使用 getAccessToken() 方法的原因:Editor 无法方便的刷新访问令牌
             'tenant-id': getTenantId()
           },
 

+ 17 - 0
src/components/SimpleProcessDesignerV2/src/NodeHandler.vue

@@ -39,6 +39,13 @@
             </div>
             <div class="handler-item-text">包容分支</div>
           </div>
+          <div class="handler-item" @click="addNode(NodeType.DELAY_TIMER_NODE)">
+            <!-- TODO @芋艿 需要更换一下iconfont的图标 -->
+            <div class="handler-item-icon copy">
+              <span class="iconfont icon-size icon-copy"></span>
+            </div>
+            <div class="handler-item-text">延迟器</div>
+          </div>
         </div>
         <template #reference>
           <div class="add-icon"><Icon icon="ep:plus" /></div>
@@ -208,6 +215,16 @@ const addNode = (type: number) => {
     }
     emits('update:childNode', data)
   }
+  if (type === NodeType.DELAY_TIMER_NODE) {
+    const data: SimpleFlowNode = {
+      id: 'Activity_' + generateUUID(),
+      name: NODE_DEFAULT_NAME.get(NodeType.DELAY_TIMER_NODE) as string,
+      showText: '',
+      type: NodeType.DELAY_TIMER_NODE,
+      childNode: props.childNode
+    }
+    emits('update:childNode', data)
+  }
 }
 </script>
 

+ 7 - 0
src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue

@@ -38,6 +38,12 @@
     @update:model-value="handleModelValueUpdate"
     @find:parent-node="findFromParentNode"
   />
+  <!-- 延迟器节点 -->
+  <DelayTimerNode
+    v-if="currentNode && currentNode.type === NodeType.DELAY_TIMER_NODE"
+    :flow-node="currentNode"
+    @update:flow-node="handleModelValueUpdate"
+  />
   <!-- 递归显示孩子节点  -->
   <ProcessNodeTree
     v-if="currentNode && currentNode.childNode"
@@ -60,6 +66,7 @@ import CopyTaskNode from './nodes/CopyTaskNode.vue'
 import ExclusiveNode from './nodes/ExclusiveNode.vue'
 import ParallelNode from './nodes/ParallelNode.vue'
 import InclusiveNode from './nodes/InclusiveNode.vue'
+import DelayTimerNode from './nodes/DelayTimerNode.vue'
 import { SimpleFlowNode, NodeType } from './consts'
 import { useWatchNode } from './node'
 defineOptions({

+ 165 - 38
src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue

@@ -1,6 +1,7 @@
 <template>
   <div v-loading="loading" class="overflow-auto">
     <SimpleProcessModel
+      ref="simpleProcessModelRef"
       v-if="processNodeTree"
       :flow-node="processNodeTree"
       :readonly="false"
@@ -38,12 +39,30 @@ import * as UserGroupApi from '@/api/bpm/userGroup'
 defineOptions({
   name: 'SimpleProcessDesigner'
 })
-const emits = defineEmits(['success']) // 保存成功事件
+
+const emits = defineEmits(['success', 'init-finished']) // 保存成功事件
 
 const props = defineProps({
   modelId: {
     type: String,
-    required: true
+    required: false
+  },
+  modelKey: {
+    type: String,
+    required: false
+  },
+  modelName: {
+    type: String,
+    required: false
+  },
+  // 可发起流程的人员编号
+  startUserIds : {
+    type: Array,
+    required: false
+  },
+  value: {
+    type: [String, Object],
+    required: false
   }
 })
 
@@ -56,6 +75,10 @@ const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
 const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表
 const deptTreeOptions = ref()
 const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
+
+// 添加当前值的引用
+const currentValue = ref<SimpleFlowNode | undefined>()
+
 provide('formFields', formFields)
 provide('formType', formType)
 provide('roleList', roleOptions)
@@ -64,33 +87,101 @@ provide('userList', userOptions)
 provide('deptList', deptOptions)
 provide('userGroupList', userGroupOptions)
 provide('deptTree', deptTreeOptions)
+provide('startUserIds', props.startUserIds)
 
 const message = useMessage() // 国际化
 const processNodeTree = ref<SimpleFlowNode | undefined>()
 const errorDialogVisible = ref(false)
 let errorNodes: SimpleFlowNode[] = []
+
+// 添加更新模型的方法
+const updateModel = () => {
+  if (!processNodeTree.value) {
+    processNodeTree.value = {
+      name: '发起人',
+      type: NodeType.START_USER_NODE,
+      id: NodeId.START_USER_NODE_ID,
+      childNode: {
+        id: NodeId.END_EVENT_NODE_ID,
+        name: '结束',
+        type: NodeType.END_EVENT_NODE
+      }
+    }
+    // 初始化时也触发一次保存
+    saveSimpleFlowModel(processNodeTree.value)
+  }
+}
+
+// 加载流程数据
+const loadProcessData = async (data: any) => {
+  try {
+    if (data) {
+      const parsedData = typeof data === 'string' ? JSON.parse(data) : data
+      processNodeTree.value = parsedData
+      currentValue.value = parsedData
+      // 确保数据加载后刷新视图
+      await nextTick()
+      if (simpleProcessModelRef.value?.refresh) {
+        await simpleProcessModelRef.value.refresh()
+      }
+    }
+  } catch (error) {
+    console.error('加载流程数据失败:', error)
+  }
+}
+
+// 监听属性变化
+watch(
+  () => props.value,
+  async (newValue, oldValue) => {
+    if (newValue && newValue !== oldValue) {
+      await loadProcessData(newValue)
+    }
+  },
+  { immediate: true, deep: true }
+)
+
+// 监听流程节点树变化,自动保存
+watch(
+  () => processNodeTree.value,
+  async (newValue, oldValue) => {
+    if (newValue && oldValue && JSON.stringify(newValue) !== JSON.stringify(oldValue)) {
+      await saveSimpleFlowModel(newValue)
+    }
+  },
+  { deep: true }
+)
+
 const saveSimpleFlowModel = async (simpleModelNode: SimpleFlowNode) => {
   if (!simpleModelNode) {
-    message.error('模型数据为空')
     return
   }
+
+  // 校验节点
+  errorNodes = []
+  validateNode(simpleModelNode, errorNodes)
+  if (errorNodes.length > 0) {
+    errorDialogVisible.value = true
+    return
+  }
+
   try {
-    loading.value = true
-    const data = {
-      id: props.modelId,
-      simpleModel: simpleModelNode
-    }
-    const result = await updateBpmSimpleModel(data)
-    if (result) {
-      message.success('修改成功')
-      emits('success')
-    } else {
-      message.alert('修改失败')
+    if (props.modelId) {
+      // 编辑模式
+      const data = {
+        id: props.modelId,
+        simpleModel: simpleModelNode
+      }
+      await updateBpmSimpleModel(data)
     }
-  } finally {
-    loading.value = false
+    // 无论是编辑还是新建模式,都更新当前值并触发事件
+    currentValue.value = simpleModelNode
+    emits('success', simpleModelNode)
+  } catch (error) {
+    console.error('保存失败:', error)
   }
 }
+
 // 校验节点设置。 暂时以 showText 为空 未节点错误配置
 const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
   if (node) {
@@ -134,12 +225,14 @@ onMounted(async () => {
   try {
     loading.value = true
     // 获取表单字段
-    const bpmnModel = await getModel(props.modelId)
-    if (bpmnModel) {
-      formType.value = bpmnModel.formType
-      if (formType.value === 10) {
-        const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO
-        formFields.value = bpmnForm?.fields
+    if (props.modelId) {
+      const bpmnModel = await getModel(props.modelId)
+      if (bpmnModel) {
+        formType.value = bpmnModel.formType
+        if (formType.value === 10) {
+          const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO
+          formFields.value = bpmnForm?.fields
+        }
       }
     }
     // 获得角色列表
@@ -150,30 +243,64 @@ onMounted(async () => {
     userOptions.value = await UserApi.getSimpleUserList()
     // 获得部门列表
     deptOptions.value = await DeptApi.getSimpleDeptList()
-
     deptTreeOptions.value = handleTree(deptOptions.value as DeptApi.DeptVO[], 'id')
     // 获取用户组列表
     userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
 
-    //获取 SIMPLE 设计器模型
-    const result = await getBpmSimpleModel(props.modelId)
-    if (result) {
-      processNodeTree.value = result
-    } else {
-      // 初始值
-      processNodeTree.value = {
-        name: '发起人',
-        type: NodeType.START_USER_NODE,
-        id: NodeId.START_USER_NODE_ID,
-        childNode: {
-          id: NodeId.END_EVENT_NODE_ID,
-          name: '结束',
-          type: NodeType.END_EVENT_NODE
-        }
+    // 加载流程数据
+    if (props.modelId) {
+      // 获取 SIMPLE 设计器模型
+      const result = await getBpmSimpleModel(props.modelId)
+      if (result) {
+        await loadProcessData(result)
+      } else {
+        updateModel()
       }
+    } else if (props.value) {
+      await loadProcessData(props.value)
+    } else {
+      updateModel()
     }
   } finally {
     loading.value = false
+    emits('init-finished')
   }
 })
+
+const simpleProcessModelRef = ref()
+
+/** 获取当前流程数据 */
+const getCurrentFlowData = async () => {
+  try {
+    if (simpleProcessModelRef.value) {
+      const data = await simpleProcessModelRef.value.getCurrentFlowData()
+      if (data) {
+        currentValue.value = data
+        return data
+      }
+    }
+    return currentValue.value
+  } catch (error) {
+    console.error('获取流程数据失败:', error)
+    return currentValue.value
+  }
+}
+
+// 刷新方法
+const refresh = async () => {
+  try {
+    if (currentValue.value) {
+      await loadProcessData(currentValue.value)
+    }
+  } catch (error) {
+    console.error('刷新失败:', error)
+  }
+}
+
+defineExpose({
+  getCurrentFlowData,
+  updateModel,
+  loadProcessData,
+  refresh
+})
 </script>

+ 27 - 19
src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue

@@ -8,15 +8,6 @@
           <el-button size="default" class="w-80px"> {{ scaleValue }}% </el-button>
           <el-button size="default" :plain="true" :icon="ZoomIn" @click="zoomIn()" />
         </el-button-group>
-        <el-button
-          v-if="!readonly"
-          size="default"
-          class="ml-4px"
-          type="primary"
-          :icon="Select"
-          @click="saveSimpleFlowModel"
-          >保存模型</el-button
-        >
       </el-row>
     </div>
     <div class="simple-process-model" :style="`transform: scale(${scaleValue / 100});`">
@@ -42,7 +33,8 @@
 import ProcessNodeTree from './ProcessNodeTree.vue'
 import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from './consts'
 import { useWatchNode } from './node'
-import { Select, ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue'
+import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue'
+
 defineOptions({
   name: 'SimpleProcessModel'
 })
@@ -58,6 +50,7 @@ const props = defineProps({
     default: true
   }
 })
+
 const emits = defineEmits<{
   'save': [node: SimpleFlowNode | undefined]
 }>()
@@ -68,6 +61,7 @@ provide('readonly', props.readonly)
 let scaleValue = ref(100)
 const MAX_SCALE_VALUE = 200
 const MIN_SCALE_VALUE = 50
+
 // 放大
 const zoomIn = () => {
   if (scaleValue.value == MAX_SCALE_VALUE) {
@@ -75,6 +69,7 @@ const zoomIn = () => {
   }
   scaleValue.value += 10
 }
+
 // 缩小
 const zoomOut = () => {
   if (scaleValue.value == MIN_SCALE_VALUE) {
@@ -82,21 +77,14 @@ const zoomOut = () => {
   }
   scaleValue.value -= 10
 }
+
 const processReZoom = () => {
   scaleValue.value = 100
 }
 
 const errorDialogVisible = ref(false)
 let errorNodes: SimpleFlowNode[] = []
-const saveSimpleFlowModel = async () => {
-  errorNodes = []
-  validateNode(processNodeTree.value, errorNodes)
-  if (errorNodes.length > 0) {
-    errorDialogVisible.value = true
-    return
-  }
-  emits('save', processNodeTree.value)
-}
+
 // 校验节点设置。 暂时以 showText 为空 未节点错误配置
 const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
   if (node) {
@@ -135,6 +123,26 @@ const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNo
     }
   }
 }
+
+/** 获取当前流程数据 */
+const getCurrentFlowData = async () => {
+  try {
+    errorNodes = []
+    validateNode(processNodeTree.value, errorNodes)
+    if (errorNodes.length > 0) {
+      errorDialogVisible.value = true
+      return undefined
+    }
+    return processNodeTree.value
+  } catch (error) {
+    console.error('获取流程数据失败:', error)
+    return undefined
+  }
+}
+
+defineExpose({
+  getCurrentFlowData
+})
 </script>
 
 <style lang="scss" scoped></style>

+ 36 - 0
src/components/SimpleProcessDesignerV2/src/consts.ts

@@ -23,6 +23,11 @@ export enum NodeType {
    */
   COPY_TASK_NODE = 12,
 
+  /**
+   * 延迟器节点
+   */
+  DELAY_TIMER_NODE = 14,
+
   /**
    * 条件节点
    */
@@ -98,6 +103,8 @@ export interface SimpleFlowNode {
   defaultFlow?: boolean
   // 活动的状态,用于前端节点状态展示
   activityStatus?: TaskStatusEnum
+  // 延迟设置
+  delaySetting?: DelaySetting
 }
 // 候选人策略枚举 ( 用于审批节点。抄送节点 )
 export enum CandidateStrategy {
@@ -413,12 +420,14 @@ NODE_DEFAULT_TEXT.set(NodeType.USER_TASK_NODE, '请配置审批人')
 NODE_DEFAULT_TEXT.set(NodeType.COPY_TASK_NODE, '请配置抄送人')
 NODE_DEFAULT_TEXT.set(NodeType.CONDITION_NODE, '请设置条件')
 NODE_DEFAULT_TEXT.set(NodeType.START_USER_NODE, '请设置发起人')
+NODE_DEFAULT_TEXT.set(NodeType.DELAY_TIMER_NODE, '请设置延迟器')
 
 export const NODE_DEFAULT_NAME = new Map<number, string>()
 NODE_DEFAULT_NAME.set(NodeType.USER_TASK_NODE, '审批人')
 NODE_DEFAULT_NAME.set(NodeType.COPY_TASK_NODE, '抄送人')
 NODE_DEFAULT_NAME.set(NodeType.CONDITION_NODE, '条件')
 NODE_DEFAULT_NAME.set(NodeType.START_USER_NODE, '发起人')
+NODE_DEFAULT_NAME.set(NodeType.DELAY_TIMER_NODE, '延迟器')
 
 // 候选人策略。暂时不从字典中取。 后续可能调整。控制显示顺序
 export const CANDIDATE_STRATEGY: DictDataVO[] = [
@@ -568,3 +577,30 @@ export enum ProcessVariableEnum {
    */
   START_USER_ID = 'PROCESS_START_USER_ID'
 }
+
+/**
+ * 延迟设置
+ */
+export type DelaySetting = {
+  // 延迟类型
+  delayType: number
+  // 延迟时间表达式
+  delayTime: string
+}
+/**
+ * 延迟类型
+ */
+export enum DelayTypeEnum {
+  /**
+   * 固定时长
+   */
+  FIXED_TIME_DURATION = 1,
+  /**
+   * 固定日期时间
+   */
+  FIXED_DATE_TIME = 2
+}
+export const DELAY_TYPE = [
+  { label: '固定时长', value: DelayTypeEnum.FIXED_TIME_DURATION },
+  { label: '固定日期', value: DelayTypeEnum.FIXED_DATE_TIME }
+]

+ 27 - 11
src/components/SimpleProcessDesignerV2/src/node.ts

@@ -14,8 +14,7 @@ import {
   NODE_DEFAULT_NAME,
   AssignStartUserHandlerType,
   AssignEmptyHandlerType,
-  FieldPermissionType,
-  ProcessVariableEnum
+  FieldPermissionType
 } from './consts'
 import { parseFormFields } from '@/components/FormCreate/src/utils/index'
 export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> {
@@ -37,13 +36,6 @@ const parseFormCreateFields = (formFields?: string[]) => {
       parseFormFields(JSON.parse(fieldStr), result)
     })
   }
-  // 固定添加发起人 ID 字段
-  result.unshift({
-    field: ProcessVariableEnum.START_USER_ID,
-    title: '发起人',
-    type: 'UserSelect',
-    required: true
-  })
   return result
 }
 
@@ -60,9 +52,33 @@ export function useFormFieldsPermission(defaultPermission: FieldPermissionType)
 
   const getNodeConfigFormFields = (nodeFormFields?: Array<Record<string, string>>) => {
     nodeFormFields = toRaw(nodeFormFields)
-    fieldsPermissionConfig.value =
-      cloneDeep(nodeFormFields) || getDefaultFieldsPermission(unref(formFields))
+    if (!nodeFormFields || nodeFormFields.length === 0) {
+      fieldsPermissionConfig.value = getDefaultFieldsPermission(unref(formFields))
+    } else {
+      fieldsPermissionConfig.value = mergeFieldsPermission(nodeFormFields, unref(formFields))
+    }
   }
+  // 合并已经设置的表单字段权限,当前流程表单字段 (可能新增,或删除了字段)
+  const mergeFieldsPermission = (
+    formFieldsPermisson: Array<Record<string, string>>,
+    formFields?: string[]
+  ) => {
+    let mergedFieldsPermission: Array<Record<string, any>> = []
+    if (formFields) {
+      mergedFieldsPermission = parseFormCreateFields(formFields).map((item) => {
+        const found = formFieldsPermisson.find(
+          (fieldPermission) => fieldPermission.field == item.field
+        )
+        return {
+          field: item.field,
+          title: item.title,
+          permission: found ? found.permission : defaultPermission
+        }
+      })
+    }
+    return mergedFieldsPermission
+  }
+
   // 默认的表单权限: 获取表单的所有字段,设置字段默认权限为只读
   const getDefaultFieldsPermission = (formFields?: string[]) => {
     let defaultFieldsPermission: Array<Record<string, any>> = []

+ 28 - 18
src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue

@@ -26,19 +26,13 @@
       </div>
     </template>
     <div>
-      <div class="mb-3 font-size-16px" v-if="currentNode.defaultFlow">未满足其它条件时,将进入此分支(该分支不可编辑和删除)</div>
+      <div class="mb-3 font-size-16px" v-if="currentNode.defaultFlow"
+        >未满足其它条件时,将进入此分支(该分支不可编辑和删除)</div
+      >
       <div v-else>
-        <el-form
-          ref="formRef"
-          :model="currentNode"
-          :rules="formRules"
-          label-position="top"
-        >
+        <el-form ref="formRef" :model="currentNode" :rules="formRules" label-position="top">
           <el-form-item label="配置方式" prop="conditionType">
-            <el-radio-group
-              v-model="currentNode.conditionType"
-              @change="changeConditionType"
-            >
+            <el-radio-group v-model="currentNode.conditionType" @change="changeConditionType">
               <el-radio
                 v-for="(dict, index) in conditionConfigTypes"
                 :key="index"
@@ -108,10 +102,11 @@
                   <div class="mr-2">
                     <el-select style="width: 160px" v-model="rule.leftSide">
                       <el-option
-                        v-for="(item, index) in fieldsInfo"
+                        v-for="(item, index) in fieldOptions"
                         :key="index"
                         :label="item.title"
                         :value="item.field"
+                        :disabled="!item.required"
                       />
                     </el-select>
                   </div>
@@ -165,10 +160,12 @@ import {
   COMPARISON_OPERATORS,
   ConditionGroup,
   Condition,
-  ConditionRule
+  ConditionRule,
+  ProcessVariableEnum
 } from '../consts'
 import { getDefaultConditionNodeName } from '../utils'
 import { useFormFields } from '../node'
+import { BpmModelFormType } from '@/utils/constants'
 const message = useMessage() // 消息弹窗
 defineOptions({
   name: 'ConditionNodeConfig'
@@ -177,8 +174,8 @@ const formType = inject<Ref<number>>('formType') // 表单类型
 const conditionConfigTypes = computed(() => {
   return CONDITION_CONFIG_TYPES.filter((item) => {
     // 业务表单暂时去掉条件规则选项
-    if (formType?.value !== 10) {
-      return item.value === ConditionType.RULE
+    if (formType?.value === BpmModelFormType.CUSTOM && item.value === ConditionType.RULE) {
+      return false
     } else {
       return true
     }
@@ -368,16 +365,29 @@ const addConditionRule = (condition: Condition, idx: number) => {
 const deleteConditionRule = (condition: Condition, idx: number) => {
   condition.rules.splice(idx, 1)
 }
-
 const fieldsInfo = useFormFields()
 
+/** 条件规则可选择的表单字段 */
+const fieldOptions = computed(() => {
+  const fieldsCopy = fieldsInfo.slice()
+  // 固定添加发起人 ID 字段
+  fieldsCopy.unshift({
+    field: ProcessVariableEnum.START_USER_ID,
+    title: '发起人',
+    required: true
+  })
+  return fieldsCopy
+})
+
+/** 获取字段名称 */
 const getFieldTitle = (field: string) => {
-  const item = fieldsInfo.find((item) => item.field === field)
+  const item = fieldOptions.value.find((item) => item.field === field)
   return item?.title
 }
 
+/** 获取操作符名称 */
 const getOpName = (opCode: string): string => {
-  const opName = COMPARISON_OPERATORS.find((item) => item.value === opCode)
+  const opName = COMPARISON_OPERATORS.find((item: any) => item.value === opCode)
   return opName?.label
 }
 </script>

+ 189 - 0
src/components/SimpleProcessDesignerV2/src/nodes-config/DelayTimerNodeConfig.vue

@@ -0,0 +1,189 @@
+<template>
+  <el-drawer
+    :append-to-body="true"
+    v-model="settingVisible"
+    :show-close="false"
+    :size="550"
+    :before-close="saveConfig"
+  >
+    <template #header>
+      <div class="config-header">
+        <input
+          v-if="showInput"
+          type="text"
+          class="config-editable-input"
+          @blur="blurEvent()"
+          v-mountedFocus
+          v-model="nodeName"
+          :placeholder="nodeName"
+        />
+        <div v-else class="node-name">
+          {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
+        </div>
+        <div class="divide-line"></div>
+      </div>
+    </template>
+    <div>
+      <el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
+        <el-form-item label="延迟时间" prop="delayType">
+          <el-radio-group v-model="configForm.delayType">
+            <el-radio-button
+              v-for="item in DELAY_TYPE"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item v-if="configForm.delayType === DelayTypeEnum.FIXED_TIME_DURATION">
+          <el-form-item prop="timeDuration">
+            <el-input-number
+              class="mr-2"
+              :style="{ width: '100px' }"
+              v-model="configForm.timeDuration"
+              :min="1"
+              controls-position="right"
+            />
+          </el-form-item>
+          <el-select v-model="configForm.timeUnit" class="mr-2" :style="{ width: '100px' }">
+            <el-option
+              v-for="item in TIME_UNIT_TYPES"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+          <el-text>后进入下一节点</el-text>
+        </el-form-item>
+        <el-form-item v-if="configForm.delayType === DelayTypeEnum.FIXED_DATE_TIME" prop="dateTime">
+          <el-date-picker
+            class="mr-2"
+            v-model="configForm.dateTime"
+            type="datetime"
+            placeholder="请选择日期和时间"
+            value-format="YYYY-MM-DDTHH:mm:ss"
+          />
+          <el-text>后进入下一节点</el-text>
+        </el-form-item>
+      </el-form>
+    </div>
+    <template #footer>
+      <el-divider />
+      <div>
+        <el-button type="primary" @click="saveConfig">确 定</el-button>
+        <el-button @click="closeDrawer">取 消</el-button>
+      </div>
+    </template>
+  </el-drawer>
+</template>
+<script setup lang="ts">
+import {
+  SimpleFlowNode,
+  NodeType,
+  TIME_UNIT_TYPES,
+  TimeUnitType,
+  DelayTypeEnum,
+  DELAY_TYPE
+} from '../consts'
+import { useWatchNode, useDrawer, useNodeName } from '../node'
+import { convertTimeUnit } from '../utils'
+defineOptions({
+  name: 'DelayTimerNodeConfig'
+})
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  }
+})
+// 抽屉配置
+const { settingVisible, closeDrawer, openDrawer } = useDrawer()
+// 当前节点
+const currentNode = useWatchNode(props)
+// 节点名称
+const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.DELAY_TIMER_NODE)
+// 抄送人表单配置
+const formRef = ref() // 表单 Ref
+// 表单校验规则
+const formRules = reactive({
+  delayType: [{ required: true, message: '延迟时间不能为空', trigger: 'change' }],
+  timeDuration: [{ required: true, message: '延迟时间不能为空', trigger: 'change' }],
+  dateTime: [{ required: true, message: '延迟时间不能为空', trigger: 'change' }]
+})
+// 配置表单数据
+const configForm = ref({
+  delayType: DelayTypeEnum.FIXED_TIME_DURATION,
+  timeDuration: 1,
+  timeUnit: TimeUnitType.HOUR,
+  dateTime: ''
+})
+// 保存配置
+const saveConfig = async () => {
+  if (!formRef) return false
+  const valid = await formRef.value.validate()
+  if (!valid) return false
+  const showText = getShowText()
+  if (!showText) return false
+  currentNode.value.showText = showText
+  if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
+    currentNode.value.delaySetting = {
+      delayType: configForm.value.delayType,
+      delayTime: getIsoTimeDuration()
+    }
+  }
+  if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) {
+    currentNode.value.delaySetting = {
+      delayType: configForm.value.delayType,
+      delayTime: configForm.value.dateTime
+    }
+  }
+  settingVisible.value = false
+  return true
+}
+const getShowText = (): string => {
+  let showText = ''
+  if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
+    showText = `延迟${configForm.value.timeDuration}${TIME_UNIT_TYPES.find((item) => item.value === configForm.value.timeUnit).label}`
+  }
+  if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) {
+    showText = `延迟至${configForm.value.dateTime.replace('T', ' ')}`
+  }
+  return showText
+}
+const getIsoTimeDuration = () => {
+  let strTimeDuration = 'PT'
+  if (configForm.value.timeUnit === TimeUnitType.MINUTE) {
+    strTimeDuration += configForm.value.timeDuration + 'M'
+  }
+  if (configForm.value.timeUnit === TimeUnitType.HOUR) {
+    strTimeDuration += configForm.value.timeDuration + 'H'
+  }
+  if (configForm.value.timeUnit === TimeUnitType.DAY) {
+    strTimeDuration += configForm.value.timeDuration + 'D'
+  }
+  return strTimeDuration
+}
+// 显示延迟器节点配置, 由父组件传过来
+const showDelayTimerNodeConfig = (node: SimpleFlowNode) => {
+  nodeName.value = node.name
+  if (node.delaySetting) {
+    configForm.value.delayType = node.delaySetting.delayType
+    // 固定时长
+    if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
+      const strTimeDuration = node.delaySetting.delayTime
+      let parseTime = strTimeDuration.slice(2, strTimeDuration.length - 1)
+      let parseTimeUnit = strTimeDuration.slice(strTimeDuration.length - 1)
+      configForm.value.timeDuration = parseInt(parseTime)
+      configForm.value.timeUnit = convertTimeUnit(parseTimeUnit)
+    }
+    // 固定日期时间
+    if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) {
+      configForm.value.dateTime = node.delaySetting.delayTime
+    }
+  }
+}
+
+defineExpose({ openDrawer, showDelayTimerNodeConfig }) // 暴露方法给父组件
+</script>
+
+<style lang="scss" scoped></style>

+ 32 - 4
src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue

@@ -25,7 +25,20 @@
     </template>
     <el-tabs type="border-card" v-model="activeTabName">
       <el-tab-pane label="权限" name="user">
-        <div> 待实现 </div>
+        <el-text v-if="!startUserIds || startUserIds.length === 0"> 全部成员可以发起流程 </el-text>
+        <el-text v-else-if="startUserIds.length == 1">
+          {{ getUserNicknames(startUserIds) }} 可发起流程
+        </el-text>
+        <el-text v-else>
+          <el-tooltip
+            class="box-item"
+            effect="dark"
+            placement="top"
+            :content="getUserNicknames(startUserIds)"
+          >
+            {{ getUserNicknames(startUserIds.slice(0,2)) }} 等 {{ startUserIds.length }} 人可发起流程
+          </el-tooltip>
+        </el-text>
       </el-tab-pane>
       <el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10">
         <div class="field-setting-pane">
@@ -86,7 +99,7 @@
 <script setup lang="ts">
 import { SimpleFlowNode, NodeType, FieldPermissionType, START_USER_BUTTON_SETTING } from '../consts'
 import { useWatchNode, useDrawer, useNodeName, useFormFieldsPermission } from '../node'
-
+import * as UserApi from '@/api/system/user'
 defineOptions({
   name: 'StartUserNodeConfig'
 })
@@ -96,6 +109,10 @@ const props = defineProps({
     required: true
   }
 })
+// 可发起流程的用户编号
+const startUserIds = inject<Ref<any[]>>('startUserIds')
+// 用户列表
+const userOptions = inject<Ref<UserApi.UserVO[]>>('userList')
 // 抽屉配置
 const { settingVisible, closeDrawer, openDrawer } = useDrawer()
 // 当前节点
@@ -108,12 +125,23 @@ const activeTabName = ref('user')
 const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
   FieldPermissionType.WRITE
 )
-
+const getUserNicknames = (userIds: number[]): string => {
+  if (!userIds || userIds.length === 0) {
+    return ''
+  }
+  const nicknames: string[] = []
+  userIds.forEach((userId) => {
+    const found = userOptions?.value.find((item) => item.id === userId)
+    if (found && found.nickname) {
+      nicknames.push(found.nickname)
+    }
+  })
+  return nicknames.join(',')
+}
 // 保存配置
 const saveConfig = async () => {
   activeTabName.value = 'user'
   currentNode.value.name = nodeName.value!
-  // TODO 暂时写死。后续可以显示谁有权限可以发起
   currentNode.value.showText = '已设置'
   // 设置表单权限
   currentNode.value.fieldsPermission = fieldsPermissionConfig.value

+ 9 - 1
src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue

@@ -469,7 +469,8 @@ import {
   TimeoutHandlerType,
   ASSIGN_EMPTY_HANDLER_TYPES,
   AssignEmptyHandlerType,
-  FieldPermissionType
+  FieldPermissionType,
+  ProcessVariableEnum
 } from '../consts'
 
 import {
@@ -519,6 +520,13 @@ const { formType, fieldsPermissionConfig, formFieldOptions, getNodeConfigFormFie
   useFormFieldsPermission(FieldPermissionType.READ)
 // 表单内用户字段选项, 必须是必填和用户选择器
 const userFieldOnFormOptions = computed(() => {
+  // 固定添加发起人 ID 字段
+  formFieldOptions.unshift({
+    field: ProcessVariableEnum.START_USER_ID,
+    title: '发起人',
+    type: 'UserSelect',
+    required: true
+  })
   return formFieldOptions.filter((item) => item.type === 'UserSelect')
 })
 // 表单内部门字段选项, 必须是必填和部门选择器

+ 98 - 0
src/components/SimpleProcessDesignerV2/src/nodes/DelayTimerNode.vue

@@ -0,0 +1,98 @@
+<template>
+  <div class="node-wrapper">
+    <div class="node-container">
+      <div
+        class="node-box"
+        :class="[
+          { 'node-config-error': !currentNode.showText },
+          `${useTaskStatusClass(currentNode?.activityStatus)}`
+        ]"
+      >
+        <div class="node-title-container">
+          <!-- TODO @芋艿 需要更换图标 -->
+          <div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div>
+          <input
+            v-if="!readonly && showInput"
+            type="text"
+            class="editable-title-input"
+            @blur="blurEvent()"
+            v-mountedFocus
+            v-model="currentNode.name"
+            :placeholder="currentNode.name"
+          />
+          <div v-else class="node-title" @click="clickTitle">
+            {{ currentNode.name }}
+          </div>
+        </div>
+        <div class="node-content" @click="openNodeConfig">
+          <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
+            {{ currentNode.showText }}
+          </div>
+          <div class="node-text" v-else>
+            {{ NODE_DEFAULT_TEXT.get(NodeType.DELAY_TIMER_NODE) }}
+          </div>
+          <Icon v-if="!readonly" icon="ep:arrow-right-bold" />
+        </div>
+        <div v-if="!readonly" class="node-toolbar">
+          <div class="toolbar-icon"
+            ><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
+          /></div>
+        </div>
+      </div>
+
+      <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
+      <NodeHandler
+        v-if="currentNode"
+        v-model:child-node="currentNode.childNode"
+        :current-node="currentNode"
+      />
+    </div>
+    <DelayTimerNodeConfig
+      v-if="!readonly && currentNode"
+      ref="nodeSetting"
+      :flow-node="currentNode"
+    />
+  </div>
+</template>
+<script setup lang="ts">
+import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
+import NodeHandler from '../NodeHandler.vue'
+import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node'
+import DelayTimerNodeConfig from '../nodes-config/DelayTimerNodeConfig.vue'
+defineOptions({
+  name: 'DelayTimerNode'
+})
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  }
+})
+// 定义事件,更新父组件。
+const emits = defineEmits<{
+  'update:flowNode': [node: SimpleFlowNode | undefined]
+}>()
+// 是否只读
+const readonly = inject<Boolean>('readonly')
+// 监控节点的变化
+const currentNode = useWatchNode(props)
+// 节点名称编辑
+const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.DELAY_TIMER_NODE)
+
+const nodeSetting = ref()
+// 打开节点配置
+const openNodeConfig = () => {
+  if (readonly) {
+    return
+  }
+  nodeSetting.value.showDelayTimerNodeConfig(currentNode.value)
+  nodeSetting.value.openDrawer()
+}
+
+// 删除节点。更新当前节点为孩子节点
+const deleteNode = () => {
+  emits('update:flowNode', currentNode.value.childNode)
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 6 - 1
src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss

@@ -173,13 +173,16 @@
   height: 100%;
   padding-top: 32px;
   background-color: #fafafa;
+  overflow-x: auto;
+  width: 100%;
+
   .simple-process-model {
     display: flex;
     flex-direction: column;
     justify-content: center;
     align-items: center;
     transform-origin: 50% 0 0;
-    overflow: auto;
+    min-width: fit-content;
     transform: scale(1);
     transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
     background: url(@/assets/svgs/bpm/simple-process-bg.svg) 0 0 repeat;
@@ -473,6 +476,7 @@
       .branch-node-container {
         position: relative;
         display: flex;
+        min-width: fit-content;
 
         &::before {
           position: absolute;
@@ -548,6 +552,7 @@
           background: transparent;
           border-top: 2px solid #dedede;
           border-bottom: 2px solid #dedede;
+          flex-shrink: 0;
 
           &::before {
             position: absolute;

+ 29 - 10
src/components/UserSelectForm/index.vue

@@ -39,7 +39,7 @@
   </Dialog>
 </template>
 <script lang="ts" setup>
-import { defaultProps, findTreeNode, handleTree } from '@/utils/tree'
+import { defaultProps, handleTree } from '@/utils/tree'
 import * as DeptApi from '@/api/system/dept'
 import * as UserApi from '@/api/system/user'
 
@@ -50,6 +50,7 @@ const emit = defineEmits<{
 const { t } = useI18n() // 国际
 const message = useMessage() // 消息弹窗
 const deptTree = ref<Tree[]>([]) // 部门树形结构化
+const deptList = ref<any[]>([]) // 保存扁平化的部门列表数据
 const userList = ref<UserApi.UserVO[]>([]) // 所有用户列表
 const filteredUserList = ref<UserApi.UserVO[]>([]) // 当前部门过滤后的用户列表
 const selectedUserIdList: any = ref([]) // 选中的用户列表
@@ -79,7 +80,9 @@ const open = async (id: number, selectedList?: any[]) => {
   resetForm()
 
   // 加载部门、用户列表
-  deptTree.value = handleTree(await DeptApi.getSimpleDeptList())
+  const deptData = await DeptApi.getSimpleDeptList()
+  deptList.value = deptData // 保存扁平结构的部门数据
+  deptTree.value = handleTree(deptData) // 转换成树形结构
   userList.value = await UserApi.getSimpleUserList()
 
   // 初始状态下,过滤列表等于所有用户列表
@@ -88,16 +91,31 @@ const open = async (id: number, selectedList?: any[]) => {
   dialogVisible.value = true
 }
 
+/** 获取指定部门及其所有子部门的ID列表 */
+const getChildDeptIds = (deptId: number, deptList: any[]): number[] => {
+  const ids = [deptId]
+  const children = deptList.filter((dept) => dept.parentId === deptId)
+  children.forEach((child) => {
+    ids.push(...getChildDeptIds(child.id, deptList))
+  })
+  return ids
+}
+
 /** 获取部门过滤后的用户列表 */
-const getUserList = async (deptId?: number) => {
+const filterUserList = async (deptId?: number) => {
   formLoading.value = true
   try {
-    // @ts-ignore
-    // TODO @芋艿:替换到 simple List 暂不支持 deptId 过滤
-    // TODO @Zqqq:这个,可以使用前端过滤么?通过 deptList 获取到 deptId 子节点,然后去 userList
-    const data = await UserApi.getUserPage({ pageSize: 100, pageNo: 1, deptId })
-    // 更新过滤后的用户列表
-    filteredUserList.value = data.list
+    if (!deptId) {
+      // 如果没有选择部门,显示所有用户
+      filteredUserList.value = [...userList.value]
+      return
+    }
+
+    // 直接使用已保存的部门列表数据进行过滤
+    const deptIds = getChildDeptIds(deptId, deptList.value)
+
+    // 过滤出这些部门下的用户
+    filteredUserList.value = userList.value.filter((user) => deptIds.includes(user.deptId))
   } finally {
     formLoading.value = false
   }
@@ -121,6 +139,7 @@ const submitForm = async () => {
 /** 重置表单 */
 const resetForm = () => {
   deptTree.value = []
+  deptList.value = []
   userList.value = []
   filteredUserList.value = []
   selectedUserIdList.value = []
@@ -128,7 +147,7 @@ const resetForm = () => {
 
 /** 处理部门被点击 */
 const handleNodeClick = (row: { [key: string]: any }) => {
-  getUserList(row.id)
+  filterUserList(row.id)
 }
 
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗

+ 23 - 62
src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue

@@ -160,13 +160,6 @@
             <XButton preIcon="ep:refresh" @click="processRestart()" />
           </el-tooltip>
         </ElButtonGroup>
-        <XButton
-          preIcon="ep:plus"
-          title="保存模型"
-          @click="processSave"
-          :type="props.headerButtonType"
-          :disabled="simulationStatus"
-        />
       </template>
       <!-- 用于打开本地文件-->
       <input
@@ -315,6 +308,28 @@ const props = defineProps({
   }
 })
 
+// 监听value变化,重新加载流程图
+watch(
+  () => props.value,
+  (newValue) => {
+    if (newValue && bpmnModeler) {
+      createNewDiagram(newValue)
+    }
+  },
+  { immediate: true }
+)
+
+// 监听processId和processName变化
+watch(
+  [() => props.processId, () => props.processName],
+  ([newId, newName]) => {
+    if (newId && newName && !props.value) {
+      createNewDiagram(null)
+    }
+  },
+  { immediate: true }
+)
+
 provide('configGlobal', props)
 let bpmnModeler: any = null
 const defaultZoom = ref(1)
@@ -592,16 +607,6 @@ const processZoomOut = (zoomStep = 0.1) => {
   defaultZoom.value = newZoom
   bpmnModeler.get('canvas').zoom(defaultZoom.value)
 }
-// const processZoomTo = (newZoom = 1) => {
-//   if (newZoom < 0.2) {
-//     throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2')
-//   }
-//   if (newZoom > 4) {
-//     throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4')
-//   }
-//   defaultZoom = newZoom
-//   bpmnModeler.get('canvas').zoom(newZoom)
-// }
 const processReZoom = () => {
   defaultZoom.value = 1
   bpmnModeler.get('canvas').zoom('fit-viewport', 'auto')
@@ -640,63 +645,19 @@ const previewProcessXML = () => {
 }
 const previewProcessJson = () => {
   bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
-    // console.log(xml, 'xml')
-
-    // const rootNode = parseXmlString(xml)
-    // console.log(rootNode, 'rootNoderootNode')
     const rootNodes = new XmlNode(XmlNodeType.Root, parseXmlString(xml))
-    // console.log(rootNodes, 'rootNodesrootNodesrootNodes')
-    // console.log(rootNodes.parent.toJsObject(), 'rootNodes.toJSON()')
-    // console.log(JSON.stringify(rootNodes.parent.toJsObject()), 'rootNodes.toJSON()')
-    // console.log(JSON.stringify(rootNodes.parent.toJSON()), 'rootNodes.toJSON()')
-
-    // const parser = new xml2js.XMLParser()
-    // let jObj = parser.parse(xml)
-    // console.log(jObj, 'jObjjObjjObjjObjjObj')
-    // const builder = new xml2js.XMLBuilder(xml)
-    // const xmlContent = builder
-    // console.log(xmlContent, 'xmlContent')
-    // console.log(xml2js, 'convertconvertconvert')
     previewResult.value = rootNodes.parent?.toJSON() as unknown as string
-    // previewResult.value = jObj
-    // previewResult.value = convert.xml2json(xml,  {explicitArray : false},{ spaces: 2 })
     previewType.value = 'json'
     previewModelVisible.value = true
   })
 }
+
 /* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */
-const processSave = async () => {
-  // console.log(bpmnModeler, 'bpmnModelerbpmnModelerbpmnModelerbpmnModeler')
-  const { err, xml } = await bpmnModeler.saveXML()
-  // console.log(err, 'errerrerrerrerr')
-  // console.log(xml, 'xmlxmlxmlxmlxml')
-  // 读取异常时抛出异常
-  if (err) {
-    // this.$modal.msgError('保存模型失败,请重试!')
-    alert('保存模型失败,请重试!')
-    return
-  }
-  // 触发 save 事件
-  emit('save', xml)
-}
-/** 高亮显示 */
-// const highlightedCode = (previewType, previewResult) => {
-//   console.log(previewType, 'previewType, previewResult')
-//   console.log(previewResult, 'previewType, previewResult')
-//   console.log(hljs.highlight, 'hljs.highlight')
-//   const result = hljs.highlight(previewType, previewResult.value || '', true)
-//   return result.value || '&nbsp;'
-// }
-onBeforeMount(() => {
-  console.log(props, 'propspropspropsprops')
-})
 onMounted(() => {
   initBpmnModeler()
   createNewDiagram(props.value)
 })
 onBeforeUnmount(() => {
-  // this.$once('hook:beforeDestroy', () => {
-  // })
   if (bpmnModeler) bpmnModeler.destroy()
   emit('destroy', bpmnModeler)
   bpmnModeler = null

+ 157 - 0
src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json

@@ -406,6 +406,31 @@
           "name": "variableMappingDelegateExpression",
           "isAttr": true,
           "type": "String"
+        },
+        {
+          "name": "calledElementType",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "processInstanceName",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "inheritBusinessKey",
+          "isAttr": true,
+          "type": "Boolean"
+        },
+        {
+          "name": "businessKey",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "inheritVariables",
+          "isAttr": true,
+          "type": "Boolean"
         }
       ]
     },
@@ -1281,6 +1306,138 @@
           "isBody": true
         }
       ]
+    },
+    {
+      "name": "ButtonsSetting",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:UserTask"]
+      },
+      "properties": [
+        {
+          "name": "flowable:id",
+          "type": "Integer",
+          "isAttr": true
+        },
+        {
+          "name": "flowable:enable",
+          "type": "Boolean",
+          "isAttr": true
+        },
+        {
+          "name": "flowable:displayName",
+          "type": "String",
+          "isAttr": true
+        }
+      ]
+    },
+    {
+      "name": "FieldsPermission",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:UserTask"]
+      },
+      "properties": [
+        {
+          "name": "flowable:field",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "flowable:title",
+          "type": "String",
+          "isAttr": true
+        },
+        {
+          "name": "flowable:permission",
+          "type": "String",
+          "isAttr": true
+        }
+      ]
+    },
+    {
+      "name": "BoundaryEventType",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:BoundaryEvent"]
+      },
+      "properties": [
+        {
+          "name": "value",
+          "type": "Integer",
+          "isBody": true
+        }
+      ]
+    },
+    {
+      "name": "TimeoutHandlerType",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:BoundaryEvent"]
+      },
+      "properties": [
+        {
+          "name": "value",
+          "type": "Integer",
+          "isBody": true
+        }
+      ]
+    },
+    {
+      "name": "ApproveType",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:UserTask"]
+      },
+      "properties": [
+        {
+          "name": "value",
+          "type": "Integer",
+          "isBody": true
+        }
+      ]
+    },
+    {
+      "name": "ApproveMethod",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:UserTask"]
+      },
+      "properties": [
+        {
+          "name": "value",
+          "type": "Integer",
+          "isBody": true
+        }
+      ]
+    },
+    {
+      "name": "CandidateStrategy",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:UserTask"]
+      },
+      "properties": [
+        {
+          "name": "value",
+          "type": "Integer",
+          "isBody": true
+        }
+      ]
+    },
+    {
+      "name": "CandidateParam",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:UserTask"]
+      },
+      "properties": [
+        {
+          "name": "value",
+          "type": "String",
+          "isBody": true
+        }
+      ]
     }
   ],
   "emumerations": []

+ 12 - 0
src/components/bpmnProcessDesigner/package/designer/plugins/palette/CustomPalette.js

@@ -165,6 +165,18 @@ F.prototype.getPaletteEntries = function () {
       'bpmn-icon-user-task',
       translate('Create User Task')
     ),
+    'create.call-activity': createAction(
+      'bpmn:CallActivity',
+      'activity',
+      'bpmn-icon-call-activity',
+      translate('Create Call Activity')
+    ),
+    'create.service-task': createAction(
+      'bpmn:ServiceTask',
+      'activity',
+      'bpmn-icon-service',
+      translate('Create Service Task')
+    ),
     'create.data-object': createAction(
       'bpmn:DataObjectReference',
       'data-object',

+ 6 - 0
src/components/bpmnProcessDesigner/package/designer/plugins/palette/paletteProvider.js

@@ -171,6 +171,12 @@ PaletteProvider.prototype.getPaletteEntries = function () {
       'bpmn-icon-user-task',
       translate('Create User Task')
     ),
+    'create.service-task': createAction(
+      'bpmn:ServiceTask',
+      'activity',
+      'bpmn-icon-service',
+      translate('Create Service Task')
+    ),
     'create.data-object': createAction(
       'bpmn:DataObjectReference',
       'data-object',

+ 2 - 0
src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js

@@ -56,6 +56,8 @@ export default {
   'Create EndEvent': '创建结束事件',
   'Create Task': '创建任务',
   'Create User Task': '创建用户任务',
+  'Create Call Activity': '创建调用活动',
+  'Create Service Task': '创建服务任务',
   'Create Gateway': '创建网关',
   'Create DataObjectReference': '创建数据对象',
   'Create DataStoreReference': '创建数据存储',

+ 95 - 47
src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue

@@ -1,6 +1,6 @@
 <template>
-  <div class="process-panel__container" :style="{ width: `${width}px`,maxHeight: '700px' }">
-    <el-collapse v-model="activeTab">
+  <div class="process-panel__container" :style="{ width: `${width}px` }">
+    <el-collapse v-model="activeTab" v-if="isReady">
       <el-collapse-item name="base">
         <!-- class="panel-tab__title" -->
         <template #title>
@@ -26,8 +26,10 @@
         <template #title><Icon icon="ep:list" />表单</template>
         <element-form :id="elementId" :type="elementType" />
       </el-collapse-item>
-      <el-collapse-item name="task" v-if="elementType.indexOf('Task') !== -1" key="task">
-        <template #title><Icon icon="ep:checked" />任务(审批人)</template>
+      <el-collapse-item name="task" v-if="isTaskCollapseItemShow(elementType)" key="task">
+        <template #title
+          ><Icon icon="ep:checked" />{{ getTaskCollapseItemName(elementType) }}</template
+        >
         <element-task :id="elementId" :type="elementType" />
       </el-collapse-item>
       <el-collapse-item
@@ -35,8 +37,12 @@
         v-if="elementType.indexOf('Task') !== -1"
         key="multiInstance"
       >
-        <template #title><Icon icon="ep:help-filled" />多实例(会签配置)</template>
-        <element-multi-instance :business-object="elementBusinessObject" :type="elementType" />
+        <template #title><Icon icon="ep:help-filled" />多人审批方式</template>
+        <element-multi-instance
+          :id="elementId"
+          :business-object="elementBusinessObject"
+          :type="elementType"
+        />
       </el-collapse-item>
       <el-collapse-item name="listeners" key="listeners">
         <template #title><Icon icon="ep:bell-filled" />执行监听器</template>
@@ -54,9 +60,13 @@
         <template #title><Icon icon="ep:promotion" />其他</template>
         <element-other-config :id="elementId" />
       </el-collapse-item>
-      <el-collapse-item name="customConfig" v-if="elementType.indexOf('Task') !== -1" key="customConfig">
-        <template #title><Icon icon="ep:circle-plus-filled" />自定义配置</template>
-        <element-custom-config :id="elementId" :type="elementType" />
+      <el-collapse-item name="customConfig" key="customConfig">
+        <template #title><Icon icon="ep:tools" />自定义配置</template>
+        <element-custom-config
+          :id="elementId"
+          :type="elementType"
+          :business-object="elementBusinessObject"
+        />
       </el-collapse-item>
     </el-collapse>
   </div>
@@ -72,6 +82,7 @@ import ElementListeners from './listeners/ElementListeners.vue'
 import ElementProperties from './properties/ElementProperties.vue'
 // import ElementForm from './form/ElementForm.vue'
 import UserTaskListeners from './listeners/UserTaskListeners.vue'
+import { getTaskCollapseItemName, isTaskCollapseItemShow } from './task/data'
 
 defineOptions({ name: 'MyPropertiesPanel' })
 
@@ -108,24 +119,16 @@ const elementBusinessObject = ref<any>({}) // 元素 businessObject 镜像,提
 const conditionFormVisible = ref(false) // 流转条件设置
 const formVisible = ref(false) // 表单配置
 const bpmnElement = ref()
+const isReady = ref(false)
 
 provide('prefix', props.prefix)
 provide('width', props.width)
-const bpmnInstances = () => (window as any)?.bpmnInstances
-
-// 监听 props.bpmnModeler 然后 initModels
-const unwatchBpmn = watch(
-  () => props.bpmnModeler,
-  () => {
-    // 避免加载时 流程图 并未加载完成
-    if (!props.bpmnModeler) {
-      console.log('缺少props.bpmnModeler')
-      return
-    }
 
-    console.log('props.bpmnModeler 有值了!!!')
-    const w = window as any
-    w.bpmnInstances = {
+// 初始化 bpmnInstances
+const initBpmnInstances = () => {
+  if (!props.bpmnModeler) return false
+  try {
+    const instances = {
       modeler: props.bpmnModeler,
       modeling: props.bpmnModeler.get('modeling'),
       moddle: props.bpmnModeler.get('moddle'),
@@ -137,9 +140,45 @@ const unwatchBpmn = watch(
       selection: props.bpmnModeler.get('selection')
     }
 
-    console.log(bpmnInstances(), 'window.bpmnInstances')
-    getActiveElement()
-    unwatchBpmn()
+    // 检查所有实例是否都存在
+    const allInstancesExist = Object.values(instances).every(instance => instance)
+    if (allInstancesExist) {
+      const w = window as any
+      w.bpmnInstances = instances
+      return true
+    }
+    return false
+  } catch (error) {
+    console.error('初始化 bpmnInstances 失败:', error)
+    return false
+  }
+}
+
+const bpmnInstances = () => (window as any)?.bpmnInstances
+
+// 监听 props.bpmnModeler 然后 initModels
+const unwatchBpmn = watch(
+  () => props.bpmnModeler,
+  async () => {
+    // 避免加载时 流程图 并未加载完成
+    if (!props.bpmnModeler) {
+      console.log('缺少props.bpmnModeler')
+      return
+    }
+
+    try {
+      // 等待 modeler 初始化完成
+      await nextTick()
+      if (initBpmnInstances()) {
+        isReady.value = true
+        await nextTick()
+        getActiveElement()
+      } else {
+        console.error('modeler 实例未完全初始化')
+      }
+    } catch (error) {
+      console.error('初始化失败:', error)
+    }
   },
   {
     immediate: true
@@ -147,6 +186,8 @@ const unwatchBpmn = watch(
 )
 
 const getActiveElement = () => {
+  if (!isReady.value || !props.bpmnModeler) return
+
   // 初始第一个选中元素 bpmn:Process
   initFormOnChanged(null)
   props.bpmnModeler.on('import.done', (e) => {
@@ -164,8 +205,11 @@ const getActiveElement = () => {
     }
   })
 }
+
 // 初始化数据
 const initFormOnChanged = (element) => {
+  if (!isReady.value || !bpmnInstances()) return
+
   let activatedElement = element
   if (!activatedElement) {
     activatedElement =
@@ -173,32 +217,36 @@ const initFormOnChanged = (element) => {
       bpmnInstances().elementRegistry.find((el) => el.type === 'bpmn:Collaboration')
   }
   if (!activatedElement) return
-  console.log(`
-              ----------
-      select element changed:
-                id:  ${activatedElement.id}
-              type:  ${activatedElement.businessObject.$type}
-              ----------
-              `)
-  console.log('businessObject: ', activatedElement.businessObject)
-  bpmnInstances().bpmnElement = activatedElement
-  bpmnElement.value = activatedElement
-  elementId.value = activatedElement.id
-  elementType.value = activatedElement.type.split(':')[1] || ''
-  elementBusinessObject.value = JSON.parse(JSON.stringify(activatedElement.businessObject))
-  conditionFormVisible.value = !!(
-    elementType.value === 'SequenceFlow' &&
-    activatedElement.source &&
-    activatedElement.source.type.indexOf('StartEvent') === -1
-  )
-  formVisible.value = elementType.value === 'UserTask' || elementType.value === 'StartEvent'
+
+  try {
+    console.log(`
+                ----------
+        select element changed:
+                  id:  ${activatedElement.id}
+                type:  ${activatedElement.businessObject.$type}
+                ----------
+                `)
+    console.log('businessObject: ', activatedElement.businessObject)
+    bpmnInstances().bpmnElement = activatedElement
+    bpmnElement.value = activatedElement
+    elementId.value = activatedElement.id
+    elementType.value = activatedElement.type.split(':')[1] || ''
+    elementBusinessObject.value = JSON.parse(JSON.stringify(activatedElement.businessObject))
+    conditionFormVisible.value = !!(
+      elementType.value === 'SequenceFlow' &&
+      activatedElement.source &&
+      activatedElement.source.type.indexOf('StartEvent') === -1
+    )
+    formVisible.value = elementType.value === 'UserTask' || elementType.value === 'StartEvent'
+  } catch (error) {
+    console.error('初始化表单数据失败:', error)
+  }
 }
 
 onBeforeUnmount(() => {
   const w = window as any
   w.bpmnInstances = null
-  console.log(props, 'props1')
-  console.log(props.bpmnModeler, 'props.bpmnModeler1')
+  isReady.value = false
 })
 
 watch(

+ 20 - 264
src/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue

@@ -1,283 +1,39 @@
-<!-- UserTask 自定义配置:
-     1. 审批人与提交人为同一人时
-     2. 审批人拒绝时
-     3. 审批人为空时
--->
 <template>
   <div class="panel-tab__content">
-    <el-divider content-position="left">审批人拒绝时</el-divider>
-    <el-form-item prop="rejectHandlerType">
-      <el-radio-group
-        v-model="rejectHandlerType"
-        :disabled="returnTaskList.length === 0"
-        @change="updateRejectHandlerType"
-      >
-        <div class="flex-col">
-          <div v-for="(item, index) in REJECT_HANDLER_TYPES" :key="index">
-            <el-radio :key="item.value" :value="item.value" :label="item.label" />
-          </div>
-        </div>
-      </el-radio-group>
-    </el-form-item>
-    <el-form-item
-      v-if="rejectHandlerType == RejectHandlerType.RETURN_USER_TASK"
-      label="驳回节点"
-      prop="returnNodeId"
-    >
-      <el-select v-model="returnNodeId" clearable style="width: 100%" @change="updateReturnNodeId">
-        <el-option
-          v-for="item in returnTaskList"
-          :key="item.id"
-          :label="item.name"
-          :value="item.id"
-        />
-      </el-select>
-    </el-form-item>
-
-    <el-divider content-position="left">审批人为空时</el-divider>
-    <el-form-item prop="assignEmptyHandlerType">
-      <el-radio-group v-model="assignEmptyHandlerType" @change="updateAssignEmptyHandlerType">
-        <div class="flex-col">
-          <div v-for="(item, index) in ASSIGN_EMPTY_HANDLER_TYPES" :key="index">
-            <el-radio :key="item.value" :value="item.value" :label="item.label" />
-          </div>
-        </div>
-      </el-radio-group>
-    </el-form-item>
-    <el-form-item
-      v-if="assignEmptyHandlerType == AssignEmptyHandlerType.ASSIGN_USER"
-      label="指定用户"
-      prop="assignEmptyHandlerUserIds"
-      span="24"
-    >
-      <el-select
-        v-model="assignEmptyUserIds"
-        clearable
-        multiple
-        style="width: 100%"
-        @change="updateAssignEmptyUserIds"
-      >
-        <el-option
-          v-for="item in userOptions"
-          :key="item.id"
-          :label="item.nickname"
-          :value="item.id"
-        />
-      </el-select>
-    </el-form-item>
-
-    <el-divider content-position="left">审批人与提交人为同一人时</el-divider>
-    <el-radio-group v-model="assignStartUserHandlerType" @change="updateAssignStartUserHandlerType">
-      <div class="flex-col">
-        <div v-for="(item, index) in ASSIGN_START_USER_HANDLER_TYPES" :key="index">
-          <el-radio :key="item.value" :value="item.value" :label="item.label" />
-        </div>
-      </div>
-    </el-radio-group>
+    <component :is="customConfigComponent" v-bind="$props" />
   </div>
 </template>
 
 <script lang="ts" setup>
-import {
-  ASSIGN_START_USER_HANDLER_TYPES,
-  RejectHandlerType,
-  REJECT_HANDLER_TYPES,
-  ASSIGN_EMPTY_HANDLER_TYPES,
-  AssignEmptyHandlerType
-} from '@/components/SimpleProcessDesignerV2/src/consts'
-import * as UserApi from '@/api/system/user'
+import { CustomConfigMap } from './data'
 
 defineOptions({ name: 'ElementCustomConfig' })
+
 const props = defineProps({
   id: String,
-  type: String
+  type: String,
+  businessObject: {
+    type: Object,
+    default: () => {}
+  }
 })
-const prefix = inject('prefix')
-
-// 审批人与提交人为同一人时
-const assignStartUserHandlerTypeEl = ref()
-const assignStartUserHandlerType = ref()
-
-// 审批人拒绝时
-const rejectHandlerTypeEl = ref()
-const rejectHandlerType = ref()
-const returnNodeIdEl = ref()
-const returnNodeId = ref()
-const returnTaskList = ref([])
 
-// 审批人为空时
-const assignEmptyHandlerTypeEl = ref()
-const assignEmptyHandlerType = ref()
-const assignEmptyUserIdsEl = ref()
-const assignEmptyUserIds = ref()
-
-const elExtensionElements = ref()
-const otherExtensions = ref()
-const bpmnElement = ref()
 const bpmnInstances = () => (window as any)?.bpmnInstances
-
-const resetCustomConfigList = () => {
-  bpmnElement.value = bpmnInstances().bpmnElement
-
-  // 获取可回退的列表
-  returnTaskList.value = findAllPredecessorsExcludingStart(
-    bpmnElement.value.id,
-    bpmnInstances().modeler
-  )
-
-  // 获取元素扩展属性 或者 创建扩展属性
-  elExtensionElements.value =
-    bpmnElement.value.businessObject?.extensionElements ??
-    bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
-
-  // 审批人与提交人为同一人时
-  assignStartUserHandlerTypeEl.value =
-    elExtensionElements.value.values?.filter(
-      (ex) => ex.$type === `${prefix}:AssignStartUserHandlerType`
-    )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignStartUserHandlerType`, { value: 1 })
-  assignStartUserHandlerType.value = assignStartUserHandlerTypeEl.value.value
-
-  // 审批人拒绝时
-  rejectHandlerTypeEl.value =
-    elExtensionElements.value.values?.filter(
-      (ex) => ex.$type === `${prefix}:RejectHandlerType`
-    )?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectHandlerType`, { value: 1 })
-  rejectHandlerType.value = rejectHandlerTypeEl.value.value
-  returnNodeIdEl.value =
-    elExtensionElements.value.values?.filter(
-      (ex) => ex.$type === `${prefix}:RejectReturnTaskId`
-    )?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectReturnTaskId`, { value: '' })
-  returnNodeId.value = returnNodeIdEl.value.value
-
-  // 审批人为空时
-  assignEmptyHandlerTypeEl.value =
-    elExtensionElements.value.values?.filter(
-      (ex) => ex.$type === `${prefix}:AssignEmptyHandlerType`
-    )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyHandlerType`, { value: 1 })
-  assignEmptyHandlerType.value = assignEmptyHandlerTypeEl.value.value
-  assignEmptyUserIdsEl.value =
-    elExtensionElements.value.values?.filter(
-      (ex) => ex.$type === `${prefix}:AssignEmptyUserIds`
-    )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyUserIds`, { value: '' })
-  assignEmptyUserIds.value = assignEmptyUserIdsEl.value.value.split(',').map((item) => {
-    // 如果数字超出了最大安全整数范围,则将其作为字符串处理
-    let num = Number(item)
-    return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER ? item : num
-  })
-
-  // 保留剩余扩展元素,便于后面更新该元素对应属性
-  otherExtensions.value =
-    elExtensionElements.value.values?.filter(
-      (ex) =>
-        ex.$type !== `${prefix}:AssignStartUserHandlerType` &&
-        ex.$type !== `${prefix}:RejectHandlerType` &&
-        ex.$type !== `${prefix}:RejectReturnTaskId` &&
-        ex.$type !== `${prefix}:AssignEmptyHandlerType` &&
-        ex.$type !== `${prefix}:AssignEmptyUserIds`
-    ) ?? []
-
-  // 更新元素扩展属性,避免后续报错
-  updateElementExtensions()
-}
-
-const updateAssignStartUserHandlerType = () => {
-  assignStartUserHandlerTypeEl.value.value = assignStartUserHandlerType.value
-
-  updateElementExtensions()
-}
-
-const updateRejectHandlerType = () => {
-  rejectHandlerTypeEl.value.value = rejectHandlerType.value
-
-  returnNodeId.value = returnTaskList.value[0].id
-  returnNodeIdEl.value.value = returnNodeId.value
-
-  updateElementExtensions()
-}
-
-const updateReturnNodeId = () => {
-  returnNodeIdEl.value.value = returnNodeId.value
-
-  updateElementExtensions()
-}
-
-const updateAssignEmptyHandlerType = () => {
-  assignEmptyHandlerTypeEl.value.value = assignEmptyHandlerType.value
-
-  updateElementExtensions()
-}
-
-const updateAssignEmptyUserIds = () => {
-  assignEmptyUserIdsEl.value.value = assignEmptyUserIds.value.toString()
-
-  updateElementExtensions()
-}
-
-const updateElementExtensions = () => {
-  const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
-    values: [
-      ...otherExtensions.value,
-      assignStartUserHandlerTypeEl.value,
-      rejectHandlerTypeEl.value,
-      returnNodeIdEl.value,
-      assignEmptyHandlerTypeEl.value,
-      assignEmptyUserIdsEl.value
-    ]
-  })
-  bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
-    extensionElements: extensions
-  })
-}
+const customConfigComponent = ref<any>(null)
 
 watch(
-  () => props.id,
-  (val) => {
-    val &&
-      val.length &&
-      nextTick(() => {
-        resetCustomConfigList()
-      })
+  () => props.businessObject,
+  () => {
+    if (props.type && props.businessObject) {
+      let val = props.type
+      if (props.businessObject.eventDefinitions) {
+        val += props.businessObject.eventDefinitions[0]?.$type.split(':')[1] || ''
+      }
+      customConfigComponent.value = CustomConfigMap[val]?.componet
+    }
   },
   { immediate: true }
 )
-
-function findAllPredecessorsExcludingStart(elementId, modeler) {
-  const elementRegistry = modeler.get('elementRegistry')
-  const allConnections = elementRegistry.filter((element) => element.type === 'bpmn:SequenceFlow')
-  const predecessors = new Set() // 使用 Set 来避免重复节点
-
-  // 检查是否是开始事件节点
-  function isStartEvent(element) {
-    return element.type === 'bpmn:StartEvent'
-  }
-
-  function findPredecessorsRecursively(element) {
-    // 获取与当前节点相连的所有连接
-    const incomingConnections = allConnections.filter((connection) => connection.target === element)
-
-    incomingConnections.forEach((connection) => {
-      const source = connection.source // 获取前置节点
-
-      // 只添加不是开始事件的前置节点
-      if (!isStartEvent(source)) {
-        predecessors.add(source.businessObject)
-        // 递归查找前置节点
-        findPredecessorsRecursively(source)
-      }
-    })
-  }
-
-  const targetElement = elementRegistry.get(elementId)
-  if (targetElement) {
-    findPredecessorsRecursively(targetElement)
-  }
-
-  return Array.from(predecessors) // 返回前置节点数组
-}
-
-const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
-onMounted(async () => {
-  // 获得用户列表
-  userOptions.value = await UserApi.getSimpleUserList()
-})
 </script>
+
+<style lang="scss" scoped></style>

+ 252 - 0
src/components/bpmnProcessDesigner/package/penal/custom-config/components/BoundaryEventTimer.vue

@@ -0,0 +1,252 @@
+<template>
+  <div>
+    <el-divider content-position="left">审批人超时未处理时</el-divider>
+    <el-form-item label="启用开关" prop="timeoutHandlerEnable">
+      <el-switch
+        v-model="timeoutHandlerEnable"
+        active-text="开启"
+        inactive-text="关闭"
+        @change="timeoutHandlerChange"
+      />
+    </el-form-item>
+    <el-form-item label="执行动作" prop="timeoutHandlerType" v-if="timeoutHandlerEnable">
+      <el-radio-group v-model="timeoutHandlerType.value" @change="onTimeoutHandlerTypeChanged">
+        <el-radio-button
+          v-for="item in TIMEOUT_HANDLER_TYPES"
+          :key="item.value"
+          :value="item.value"
+          :label="item.label"
+        />
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item label="超时时间设置" v-if="timeoutHandlerEnable">
+      <span class="mr-2">当超过</span>
+      <el-form-item prop="timeDuration">
+        <el-input-number
+          class="mr-2"
+          :style="{ width: '100px' }"
+          v-model="timeDuration"
+          :min="1"
+          controls-position="right"
+          @change="() => updateTimeModdle()"
+        />
+      </el-form-item>
+      <el-select
+        v-model="timeUnit"
+        class="mr-2"
+        :style="{ width: '100px' }"
+        @change="onTimeUnitChange"
+      >
+        <el-option
+          v-for="item in TIME_UNIT_TYPES"
+          :key="item.value"
+          :label="item.label"
+          :value="item.value"
+        />
+      </el-select>
+      未处理
+    </el-form-item>
+    <el-form-item
+      label="最大提醒次数"
+      prop="maxRemindCount"
+      v-if="timeoutHandlerEnable && timeoutHandlerType.value === 1"
+    >
+      <el-input-number
+        v-model="maxRemindCount"
+        :min="1"
+        :max="10"
+        @change="() => updateTimeModdle()"
+      />
+    </el-form-item>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import {
+  TimeUnitType,
+  TIME_UNIT_TYPES,
+  TIMEOUT_HANDLER_TYPES,
+} from '@/components/SimpleProcessDesignerV2/src/consts'
+import { convertTimeUnit } from '@/components/SimpleProcessDesignerV2/src/utils'
+
+defineOptions({ name: 'ElementCustomConfig4BoundaryEventTimer' })
+const props = defineProps({
+  id: String,
+  type: String
+})
+const prefix = inject('prefix')
+
+const bpmnElement = ref()
+const bpmnInstances = () => (window as any)?.bpmnInstances
+
+const timeoutHandlerEnable = ref(false)
+const boundaryEventType = ref()
+const timeoutHandlerType = ref({
+  value: undefined
+})
+const timeModdle = ref()
+const timeDuration = ref(6)
+const timeUnit = ref(TimeUnitType.HOUR)
+const maxRemindCount = ref(1)
+
+const elExtensionElements = ref()
+const otherExtensions = ref()
+const configExtensions = ref([])
+const eventDefinition = ref()
+
+const resetElement = () => {
+  bpmnElement.value = bpmnInstances().bpmnElement
+  eventDefinition.value = bpmnElement.value.businessObject.eventDefinitions[0]
+
+  // 获取元素扩展属性 或者 创建扩展属性
+  elExtensionElements.value =
+    bpmnElement.value.businessObject?.extensionElements ??
+    bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
+
+  // 是否开启自定义用户任务超时处理
+  boundaryEventType.value = elExtensionElements.value.values?.filter(
+    (ex) => ex.$type === `${prefix}:BoundaryEventType`
+  )?.[0]
+  if (boundaryEventType.value && boundaryEventType.value.value === 1) {
+    timeoutHandlerEnable.value = true
+    configExtensions.value.push(boundaryEventType.value)
+  }
+
+  // 执行动作
+  timeoutHandlerType.value = elExtensionElements.value.values?.filter(
+    (ex) => ex.$type === `${prefix}:TimeoutHandlerType`
+  )?.[0]
+  if (timeoutHandlerType.value) {
+    configExtensions.value.push(timeoutHandlerType.value)
+    if (eventDefinition.value.timeCycle) {
+      const timeStr = eventDefinition.value.timeCycle.body
+      const maxRemindCountStr = timeStr.split('/')[0]
+      const timeDurationStr = timeStr.split('/')[1]
+      console.log(maxRemindCountStr)
+      maxRemindCount.value = parseInt(maxRemindCountStr.slice(1))
+      timeDuration.value = parseInt(timeDurationStr.slice(2, timeDurationStr.length - 1))
+      timeUnit.value = convertTimeUnit(timeDurationStr.slice(timeDurationStr.length - 1))
+      timeModdle.value = eventDefinition.value.timeCycle
+    }
+    if (eventDefinition.value.timeDuration) {
+      const timeDurationStr = eventDefinition.value.timeDuration.body
+      timeDuration.value = parseInt(timeDurationStr.slice(2, timeDurationStr.length - 1))
+      timeUnit.value = convertTimeUnit(timeDurationStr.slice(timeDurationStr.length - 1))
+      timeModdle.value = eventDefinition.value.timeDuration
+    }
+  }
+
+  // 保留剩余扩展元素,便于后面更新该元素对应属性
+  otherExtensions.value =
+    elExtensionElements.value.values?.filter(
+      (ex) =>
+        ex.$type !== `${prefix}:BoundaryEventType` && ex.$type !== `${prefix}:TimeoutHandlerType`
+    ) ?? []
+}
+
+const timeoutHandlerChange = (val) => {
+  timeoutHandlerEnable.value = val
+  if (val) {
+    // 启用自定义用户任务超时处理
+    // 边界事件类型 --- 超时
+    boundaryEventType.value = bpmnInstances().moddle.create(`${prefix}:BoundaryEventType`, {
+      value: 1
+    })
+    configExtensions.value.push(boundaryEventType.value)
+    // 超时处理类型
+    timeoutHandlerType.value = bpmnInstances().moddle.create(`${prefix}:TimeoutHandlerType`, {
+      value: 1
+    })
+    configExtensions.value.push(timeoutHandlerType.value)
+    // 超时时间表达式
+    timeDuration.value = 6
+    timeUnit.value = 2
+    maxRemindCount.value = 1
+    timeModdle.value = bpmnInstances().moddle.create(`bpmn:Expression`, {
+      body: 'PT6H'
+    })
+    eventDefinition.value.timeDuration = timeModdle.value
+  } else {
+    // 关闭自定义用户任务超时处理
+    configExtensions.value = []
+    delete eventDefinition.value.timeDuration
+    delete eventDefinition.value.timeCycle
+  }
+  updateElementExtensions()
+}
+
+const onTimeoutHandlerTypeChanged = () => {
+  maxRemindCount.value = 1
+  updateElementExtensions()
+  updateTimeModdle()
+}
+
+const onTimeUnitChange = () => {
+  // 分钟,默认是 60 分钟
+  if (timeUnit.value === TimeUnitType.MINUTE) {
+    timeDuration.value = 60
+  }
+  // 小时,默认是 6 个小时
+  if (timeUnit.value === TimeUnitType.HOUR) {
+    timeDuration.value = 6
+  }
+  // 天, 默认 1天
+  if (timeUnit.value === TimeUnitType.DAY) {
+    timeDuration.value = 1
+  }
+  updateTimeModdle()
+}
+
+const updateTimeModdle = () => {
+  if (maxRemindCount.value > 1) {
+    timeModdle.value.body = 'R' + maxRemindCount.value + '/' + isoTimeDuration()
+    if (!eventDefinition.value.timeCycle) {
+      delete eventDefinition.value.timeDuration
+      eventDefinition.value.timeCycle = timeModdle.value
+    }
+  } else {
+    timeModdle.value.body = isoTimeDuration()
+    if (!eventDefinition.value.timeDuration) {
+      delete eventDefinition.value.timeCycle
+      eventDefinition.value.timeDuration = timeModdle.value
+    }
+  }
+}
+
+const isoTimeDuration = () => {
+  let strTimeDuration = 'PT'
+  if (timeUnit.value === TimeUnitType.MINUTE) {
+    strTimeDuration += timeDuration.value + 'M'
+  }
+  if (timeUnit.value === TimeUnitType.HOUR) {
+    strTimeDuration += timeDuration.value + 'H'
+  }
+  if (timeUnit.value === TimeUnitType.DAY) {
+    strTimeDuration += timeDuration.value + 'D'
+  }
+  return strTimeDuration
+}
+
+const updateElementExtensions = () => {
+  const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
+    values: [...otherExtensions.value, ...configExtensions.value]
+  })
+  bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+    extensionElements: extensions
+  })
+}
+
+watch(
+  () => props.id,
+  (val) => {
+    val &&
+      val.length &&
+      nextTick(() => {
+        resetElement()
+      })
+  },
+  { immediate: true }
+)
+</script>
+
+<style lang="scss" scoped></style>

+ 623 - 0
src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue

@@ -0,0 +1,623 @@
+<!-- UserTask 自定义配置:
+     1. 审批人与提交人为同一人时
+     2. 审批人拒绝时
+     3. 审批人为空时
+     4. 操作按钮
+     5. 字段权限
+     6. 审批类型
+-->
+<template>
+  <div>
+    <el-divider content-position="left">审批类型</el-divider>
+    <el-form-item prop="approveType">
+      <el-radio-group v-model="approveType.value">
+        <el-radio
+          v-for="(item, index) in APPROVE_TYPE"
+          :key="index"
+          :value="item.value"
+          :label="item.value"
+        >
+          {{ item.label }}
+        </el-radio>
+      </el-radio-group>
+    </el-form-item>
+
+    <el-divider content-position="left">审批人拒绝时</el-divider>
+    <el-form-item prop="rejectHandlerType">
+      <el-radio-group
+        v-model="rejectHandlerType"
+        :disabled="returnTaskList.length === 0"
+        @change="updateRejectHandlerType"
+      >
+        <div class="flex-col">
+          <div v-for="(item, index) in REJECT_HANDLER_TYPES" :key="index">
+            <el-radio :key="item.value" :value="item.value" :label="item.label" />
+          </div>
+        </div>
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item
+      v-if="rejectHandlerType == RejectHandlerType.RETURN_USER_TASK"
+      label="驳回节点"
+      prop="returnNodeId"
+    >
+      <el-select v-model="returnNodeId" clearable style="width: 100%" @change="updateReturnNodeId">
+        <el-option
+          v-for="item in returnTaskList"
+          :key="item.id"
+          :label="item.name"
+          :value="item.id"
+        />
+      </el-select>
+    </el-form-item>
+
+    <el-divider content-position="left">审批人为空时</el-divider>
+    <el-form-item prop="assignEmptyHandlerType">
+      <el-radio-group v-model="assignEmptyHandlerType" @change="updateAssignEmptyHandlerType">
+        <div class="flex-col">
+          <div v-for="(item, index) in ASSIGN_EMPTY_HANDLER_TYPES" :key="index">
+            <el-radio :key="item.value" :value="item.value" :label="item.label" />
+          </div>
+        </div>
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item
+      v-if="assignEmptyHandlerType == AssignEmptyHandlerType.ASSIGN_USER"
+      label="指定用户"
+      prop="assignEmptyHandlerUserIds"
+      span="24"
+    >
+      <el-select
+        v-model="assignEmptyUserIds"
+        clearable
+        multiple
+        style="width: 100%"
+        @change="updateAssignEmptyUserIds"
+      >
+        <el-option
+          v-for="item in userOptions"
+          :key="item.id"
+          :label="item.nickname"
+          :value="item.id"
+        />
+      </el-select>
+    </el-form-item>
+
+    <el-divider content-position="left">审批人与提交人为同一人时</el-divider>
+    <el-radio-group v-model="assignStartUserHandlerType" @change="updateAssignStartUserHandlerType">
+      <div class="flex-col">
+        <div v-for="(item, index) in ASSIGN_START_USER_HANDLER_TYPES" :key="index">
+          <el-radio :key="item.value" :value="item.value" :label="item.label" />
+        </div>
+      </div>
+    </el-radio-group>
+
+    <el-divider content-position="left">操作按钮</el-divider>
+    <div class="button-setting-pane">
+      <div class="button-setting-title">
+        <div class="button-title-label">操作按钮</div>
+        <div class="pl-4 button-title-label">显示名称</div>
+        <div class="button-title-label">启用</div>
+      </div>
+      <div class="button-setting-item" v-for="(item, index) in buttonsSettingEl" :key="index">
+        <div class="button-setting-item-label"> {{ OPERATION_BUTTON_NAME.get(item.id) }} </div>
+        <div class="button-setting-item-label">
+          <input
+            type="text"
+            class="editable-title-input"
+            @blur="btnDisplayNameBlurEvent(index)"
+            v-mountedFocus
+            v-model="item.displayName"
+            :placeholder="item.displayName"
+            v-if="btnDisplayNameEdit[index]"
+          />
+          <el-button v-else text @click="changeBtnDisplayName(index)"
+            >{{ item.displayName }} &nbsp;<Icon icon="ep:edit"
+          /></el-button>
+        </div>
+        <div class="button-setting-item-label">
+          <el-switch v-model="item.enable" />
+        </div>
+      </div>
+    </div>
+
+    <el-divider content-position="left">字段权限</el-divider>
+    <div class="field-setting-pane" v-if="formType === 10">
+      <div class="field-permit-title">
+        <div class="setting-title-label first-title"> 字段名称 </div>
+        <div class="other-titles">
+          <span class="setting-title-label">只读</span>
+          <span class="setting-title-label">可编辑</span>
+          <span class="setting-title-label">隐藏</span>
+        </div>
+      </div>
+      <div class="field-setting-item" v-for="(item, index) in fieldsPermissionEl" :key="index">
+        <div class="field-setting-item-label"> {{ item.title }} </div>
+        <el-radio-group class="field-setting-item-group" v-model="item.permission">
+          <div class="item-radio-wrap">
+            <el-radio
+              :value="FieldPermissionType.READ"
+              size="large"
+              :label="FieldPermissionType.READ"
+              ><span></span
+            ></el-radio>
+          </div>
+          <div class="item-radio-wrap">
+            <el-radio
+              :value="FieldPermissionType.WRITE"
+              size="large"
+              :label="FieldPermissionType.WRITE"
+              ><span></span
+            ></el-radio>
+          </div>
+          <div class="item-radio-wrap">
+            <el-radio
+              :value="FieldPermissionType.NONE"
+              size="large"
+              :label="FieldPermissionType.NONE"
+              ><span></span
+            ></el-radio>
+          </div>
+        </el-radio-group>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import {
+  ASSIGN_START_USER_HANDLER_TYPES,
+  RejectHandlerType,
+  REJECT_HANDLER_TYPES,
+  ASSIGN_EMPTY_HANDLER_TYPES,
+  AssignEmptyHandlerType,
+  OPERATION_BUTTON_NAME,
+  DEFAULT_BUTTON_SETTING,
+  FieldPermissionType,
+  APPROVE_TYPE,
+  ApproveType,
+  ButtonSetting
+} from '@/components/SimpleProcessDesignerV2/src/consts'
+import * as UserApi from '@/api/system/user'
+import { useFormFieldsPermission } from '@/components/SimpleProcessDesignerV2/src/node'
+
+defineOptions({ name: 'ElementCustomConfig4UserTask' })
+const props = defineProps({
+  id: String,
+  type: String
+})
+const prefix = inject('prefix')
+
+// 审批人与提交人为同一人时
+const assignStartUserHandlerTypeEl = ref()
+const assignStartUserHandlerType = ref()
+
+// 审批人拒绝时
+const rejectHandlerTypeEl = ref()
+const rejectHandlerType = ref()
+const returnNodeIdEl = ref()
+const returnNodeId = ref()
+const returnTaskList = ref([])
+
+// 审批人为空时
+const assignEmptyHandlerTypeEl = ref()
+const assignEmptyHandlerType = ref()
+const assignEmptyUserIdsEl = ref()
+const assignEmptyUserIds = ref()
+
+// 操作按钮
+const buttonsSettingEl = ref()
+const { btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } = useButtonsSetting()
+
+// 字段权限
+const fieldsPermissionEl = ref([])
+const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
+  FieldPermissionType.READ
+)
+
+// 审批类型
+const approveType = ref({ value: ApproveType.USER })
+
+const elExtensionElements = ref()
+const otherExtensions = ref()
+const bpmnElement = ref()
+const bpmnInstances = () => (window as any)?.bpmnInstances
+
+const resetCustomConfigList = () => {
+  bpmnElement.value = bpmnInstances().bpmnElement
+
+  // 获取可回退的列表
+  returnTaskList.value = findAllPredecessorsExcludingStart(
+    bpmnElement.value.id,
+    bpmnInstances().modeler
+  )
+
+  // 获取元素扩展属性 或者 创建扩展属性
+  elExtensionElements.value =
+    bpmnElement.value.businessObject?.extensionElements ??
+    bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
+
+  // 审批类型
+  approveType.value =
+    elExtensionElements.value.values?.filter((ex) => ex.$type === `${prefix}:ApproveType`)?.[0] ||
+    bpmnInstances().moddle.create(`${prefix}:ApproveType`, { value: ApproveType.USER })
+
+  // 审批人与提交人为同一人时
+  assignStartUserHandlerTypeEl.value =
+    elExtensionElements.value.values?.filter(
+      (ex) => ex.$type === `${prefix}:AssignStartUserHandlerType`
+    )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignStartUserHandlerType`, { value: 1 })
+  assignStartUserHandlerType.value = assignStartUserHandlerTypeEl.value.value
+
+  // 审批人拒绝时
+  rejectHandlerTypeEl.value =
+    elExtensionElements.value.values?.filter(
+      (ex) => ex.$type === `${prefix}:RejectHandlerType`
+    )?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectHandlerType`, { value: 1 })
+  rejectHandlerType.value = rejectHandlerTypeEl.value.value
+  returnNodeIdEl.value =
+    elExtensionElements.value.values?.filter(
+      (ex) => ex.$type === `${prefix}:RejectReturnTaskId`
+    )?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectReturnTaskId`, { value: '' })
+  returnNodeId.value = returnNodeIdEl.value.value
+
+  // 审批人为空时
+  assignEmptyHandlerTypeEl.value =
+    elExtensionElements.value.values?.filter(
+      (ex) => ex.$type === `${prefix}:AssignEmptyHandlerType`
+    )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyHandlerType`, { value: 1 })
+  assignEmptyHandlerType.value = assignEmptyHandlerTypeEl.value.value
+  assignEmptyUserIdsEl.value =
+    elExtensionElements.value.values?.filter(
+      (ex) => ex.$type === `${prefix}:AssignEmptyUserIds`
+    )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyUserIds`, { value: '' })
+  assignEmptyUserIds.value = assignEmptyUserIdsEl.value.value?.split(',').map((item) => {
+    // 如果数字超出了最大安全整数范围,则将其作为字符串处理
+    let num = Number(item)
+    return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER ? item : num
+  })
+
+  // 操作按钮
+  buttonsSettingEl.value = elExtensionElements.value.values?.filter(
+    (ex) => ex.$type === `${prefix}:ButtonsSetting`
+  )
+  if (buttonsSettingEl.value.length === 0) {
+    DEFAULT_BUTTON_SETTING.forEach((item) => {
+      buttonsSettingEl.value.push(
+        bpmnInstances().moddle.create(`${prefix}:ButtonsSetting`, {
+          'flowable:id': item.id,
+          'flowable:displayName': item.displayName,
+          'flowable:enable': item.enable
+        })
+      )
+    })
+  }
+
+  // 字段权限
+  if (formType.value === 10) {
+    const fieldsPermissionList = elExtensionElements.value.values?.filter(
+      (ex) => ex.$type === `${prefix}:FieldsPermission`
+    )
+    fieldsPermissionEl.value = []
+    getNodeConfigFormFields()
+    // 由于默认添加了发起人元素,这里需要删掉
+    fieldsPermissionConfig.value = fieldsPermissionConfig.value.slice(1)
+    fieldsPermissionConfig.value.forEach((element) => {
+      element.permission =
+        fieldsPermissionList?.find((obj) => obj.field === element.field)?.permission ?? '1'
+      fieldsPermissionEl.value.push(
+        bpmnInstances().moddle.create(`${prefix}:FieldsPermission`, element)
+      )
+    })
+  }
+
+  // 保留剩余扩展元素,便于后面更新该元素对应属性
+  otherExtensions.value =
+    elExtensionElements.value.values?.filter(
+      (ex) =>
+        ex.$type !== `${prefix}:AssignStartUserHandlerType` &&
+        ex.$type !== `${prefix}:RejectHandlerType` &&
+        ex.$type !== `${prefix}:RejectReturnTaskId` &&
+        ex.$type !== `${prefix}:AssignEmptyHandlerType` &&
+        ex.$type !== `${prefix}:AssignEmptyUserIds` &&
+        ex.$type !== `${prefix}:ButtonsSetting` &&
+        ex.$type !== `${prefix}:FieldsPermission` &&
+        ex.$type !== `${prefix}:ApproveType`
+    ) ?? []
+
+  // 更新元素扩展属性,避免后续报错
+  updateElementExtensions()
+}
+
+const updateAssignStartUserHandlerType = () => {
+  assignStartUserHandlerTypeEl.value.value = assignStartUserHandlerType.value
+
+  updateElementExtensions()
+}
+
+const updateRejectHandlerType = () => {
+  rejectHandlerTypeEl.value.value = rejectHandlerType.value
+
+  returnNodeId.value = returnTaskList.value[0].id
+  returnNodeIdEl.value.value = returnNodeId.value
+
+  updateElementExtensions()
+}
+
+const updateReturnNodeId = () => {
+  returnNodeIdEl.value.value = returnNodeId.value
+
+  updateElementExtensions()
+}
+
+const updateAssignEmptyHandlerType = () => {
+  assignEmptyHandlerTypeEl.value.value = assignEmptyHandlerType.value
+
+  updateElementExtensions()
+}
+
+const updateAssignEmptyUserIds = () => {
+  assignEmptyUserIdsEl.value.value = assignEmptyUserIds.value.toString()
+
+  updateElementExtensions()
+}
+
+const updateElementExtensions = () => {
+  const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
+    values: [
+      ...otherExtensions.value,
+      assignStartUserHandlerTypeEl.value,
+      rejectHandlerTypeEl.value,
+      returnNodeIdEl.value,
+      assignEmptyHandlerTypeEl.value,
+      assignEmptyUserIdsEl.value,
+      approveType.value,
+      ...buttonsSettingEl.value,
+      ...fieldsPermissionEl.value
+    ]
+  })
+  bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+    extensionElements: extensions
+  })
+}
+
+watch(
+  () => props.id,
+  (val) => {
+    val &&
+      val.length &&
+      nextTick(() => {
+        resetCustomConfigList()
+      })
+  },
+  { immediate: true }
+)
+
+function findAllPredecessorsExcludingStart(elementId, modeler) {
+  const elementRegistry = modeler.get('elementRegistry')
+  const allConnections = elementRegistry.filter((element) => element.type === 'bpmn:SequenceFlow')
+  const predecessors = new Set() // 使用 Set 来避免重复节点
+  const visited = new Set() // 用于记录已访问的节点
+
+  // 检查是否是开始事件节点
+  function isStartEvent(element) {
+    return element.type === 'bpmn:StartEvent'
+  }
+
+  function findPredecessorsRecursively(element) {
+    // 如果该节点已经访问过,直接返回,避免循环
+    if (visited.has(element)) {
+      return
+    }
+
+    // 标记当前节点为已访问
+    visited.add(element)
+
+    // 获取与当前节点相连的所有连接
+    const incomingConnections = allConnections.filter((connection) => connection.target === element)
+
+    incomingConnections.forEach((connection) => {
+      const source = connection.source // 获取前置节点
+
+      // 只添加不是开始事件的前置节点
+      if (!isStartEvent(source)) {
+        predecessors.add(source.businessObject)
+        // 递归查找前置节点
+        findPredecessorsRecursively(source)
+      }
+    })
+  }
+
+  const targetElement = elementRegistry.get(elementId)
+  if (targetElement) {
+    findPredecessorsRecursively(targetElement)
+  }
+
+  return Array.from(predecessors) // 返回前置节点数组
+}
+
+function useButtonsSetting() {
+  const buttonsSetting = ref<ButtonSetting[]>()
+  // 操作按钮显示名称可编辑
+  const btnDisplayNameEdit = ref<boolean[]>([])
+  const changeBtnDisplayName = (index: number) => {
+    btnDisplayNameEdit.value[index] = true
+  }
+  const btnDisplayNameBlurEvent = (index: number) => {
+    btnDisplayNameEdit.value[index] = false
+    const buttonItem = buttonsSetting.value![index]
+    buttonItem.displayName = buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)!
+  }
+  return {
+    buttonsSetting,
+    btnDisplayNameEdit,
+    changeBtnDisplayName,
+    btnDisplayNameBlurEvent
+  }
+}
+
+const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
+onMounted(async () => {
+  // 获得用户列表
+  userOptions.value = await UserApi.getSimpleUserList()
+})
+</script>
+
+<style lang="scss" scoped>
+.button-setting-pane {
+  display: flex;
+  flex-direction: column;
+  font-size: 14px;
+  margin-top: 8px;
+
+  .button-setting-desc {
+    padding-right: 8px;
+    margin-bottom: 16px;
+    font-size: 16px;
+    font-weight: 700;
+  }
+
+  .button-setting-title {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    height: 45px;
+    padding-left: 12px;
+    background-color: #f8fafc0a;
+    border: 1px solid #1f38581a;
+
+    & > :first-child {
+      width: 100px !important;
+      text-align: left !important;
+    }
+
+    & > :last-child {
+      text-align: center !important;
+    }
+
+    .button-title-label {
+      width: 150px;
+      font-size: 13px;
+      font-weight: 700;
+      color: #000;
+      text-align: left;
+    }
+  }
+
+  .button-setting-item {
+    align-items: center;
+    display: flex;
+    justify-content: space-between;
+    height: 38px;
+    padding-left: 12px;
+    border: 1px solid #1f38581a;
+    border-top: 0;
+
+    & > :first-child {
+      width: 100px !important;
+    }
+
+    & > :last-child {
+      text-align: center !important;
+    }
+
+    .button-setting-item-label {
+      width: 150px;
+      overflow: hidden;
+      text-align: left;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+
+    .editable-title-input {
+      height: 24px;
+      max-width: 130px;
+      margin-left: 4px;
+      line-height: 24px;
+      border: 1px solid #d9d9d9;
+      border-radius: 4px;
+      transition: all 0.3s;
+
+      &:focus {
+        border-color: #40a9ff;
+        outline: 0;
+        box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
+      }
+    }
+  }
+}
+
+.field-setting-pane {
+  display: flex;
+  flex-direction: column;
+  font-size: 14px;
+
+  .field-setting-desc {
+    padding-right: 8px;
+    margin-bottom: 16px;
+    font-size: 16px;
+    font-weight: 700;
+  }
+
+  .field-permit-title {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    height: 45px;
+    padding-left: 12px;
+    line-height: 45px;
+    background-color: #f8fafc0a;
+    border: 1px solid #1f38581a;
+
+    .first-title {
+      text-align: left !important;
+    }
+
+    .other-titles {
+      display: flex;
+      justify-content: space-between;
+    }
+
+    .setting-title-label {
+      display: inline-block;
+      width: 100px;
+      padding: 5px 0;
+      font-size: 13px;
+      font-weight: 700;
+      color: #000;
+      text-align: center;
+    }
+  }
+
+  .field-setting-item {
+    align-items: center;
+    display: flex;
+    justify-content: space-between;
+    height: 38px;
+    padding-left: 12px;
+    border: 1px solid #1f38581a;
+    border-top: 0;
+
+    .field-setting-item-label {
+      display: inline-block;
+      width: 100px;
+      min-height: 16px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      cursor: text;
+    }
+
+    .field-setting-item-group {
+      display: flex;
+      justify-content: space-between;
+
+      .item-radio-wrap {
+        display: inline-block;
+        width: 100px;
+        text-align: center;
+      }
+    }
+  }
+}
+</style>

+ 13 - 0
src/components/bpmnProcessDesigner/package/penal/custom-config/data.ts

@@ -0,0 +1,13 @@
+import UserTaskCustomConfig from './components/UserTaskCustomConfig.vue'
+import BoundaryEventTimer from './components/BoundaryEventTimer.vue'
+
+export const CustomConfigMap = {
+  UserTask: {
+    name: '用户任务',
+    componet: UserTaskCustomConfig
+  },
+  BoundaryEventTimerEventDefinition: {
+    name: '定时边界事件(非中断)',
+    componet: BoundaryEventTimer
+  }
+}

+ 134 - 5
src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue

@@ -1,6 +1,30 @@
 <template>
   <div class="panel-tab__content">
-    <el-form label-width="90px">
+    <el-radio-group v-model="approveMethod" @change="onApproveMethodChange">
+      <div class="flex-col">
+        <div v-for="(item, index) in APPROVE_METHODS" :key="index">
+          <el-radio :value="item.value" :label="item.value">
+            {{ item.label }}
+          </el-radio>
+          <el-form-item prop="approveRatio">
+            <el-input-number
+              v-model="approveRatio"
+              :min="10"
+              :max="100"
+              :step="10"
+              size="small"
+              v-if="
+                item.value === ApproveMethodType.APPROVE_BY_RATIO &&
+                approveMethod === ApproveMethodType.APPROVE_BY_RATIO
+              "
+              @change="onApproveRatioChange"
+            />
+          </el-form-item>
+        </div>
+      </div>
+    </el-radio-group>
+    <!-- 与Simple设计器配置合并,保留以前的代码 -->
+    <el-form label-width="90px" style="display: none">
       <el-form-item label="快捷配置">
         <el-button size="small" @click="changeConfig('依次审批')">依次审批</el-button>
         <el-button size="small" @click="changeConfig('会签')">会签</el-button>
@@ -76,11 +100,14 @@
 </template>
 
 <script lang="ts" setup>
+import { ApproveMethodType, APPROVE_METHODS } from '@/components/SimpleProcessDesignerV2/src/consts'
+
 defineOptions({ name: 'ElementMultiInstance' })
 
 const props = defineProps({
   businessObject: Object,
-  type: String
+  type: String,
+  id: String
 })
 const prefix = inject('prefix')
 const loopCharacteristics = ref('')
@@ -267,16 +294,118 @@ const changeConfig = (config) => {
   }
 }
 
+/**
+ * -----新版本多实例-----
+ */
+const approveMethod = ref()
+const approveRatio = ref(100)
+const otherExtensions = ref()
+const getElementLoopNew = () => {
+  const extensionElements =
+    bpmnElement.value.businessObject?.extensionElements ??
+    bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
+  approveMethod.value = extensionElements.values.filter(
+    (ex) => ex.$type === `${prefix}:ApproveMethod`
+  )?.[0]?.value
+
+  otherExtensions.value =
+    extensionElements.values.filter((ex) => ex.$type !== `${prefix}:ApproveMethod`) ?? []
+
+  if (!approveMethod.value) {
+    approveMethod.value = ApproveMethodType.SEQUENTIAL_APPROVE
+    updateLoopCharacteristics()
+  }
+}
+const onApproveMethodChange = () => {
+  approveRatio.value = 100
+  updateLoopCharacteristics()
+}
+const onApproveRatioChange = () => {
+  updateLoopCharacteristics()
+}
+const updateLoopCharacteristics = () => {
+  // 根据ApproveMethod生成multiInstanceLoopCharacteristics节点
+  if (approveMethod.value === ApproveMethodType.RANDOM_SELECT_ONE_APPROVE) {
+    bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+      loopCharacteristics: null
+    })
+  } else {
+    if (approveMethod.value === ApproveMethodType.APPROVE_BY_RATIO) {
+      multiLoopInstance.value = bpmnInstances().moddle.create(
+        'bpmn:MultiInstanceLoopCharacteristics',
+        { isSequential: false, collection: '${coll_userList}' }
+      )
+      multiLoopInstance.value.completionCondition = bpmnInstances().moddle.create(
+        'bpmn:FormalExpression',
+        {
+          body: '${ nrOfCompletedInstances/nrOfInstances >= ' + approveRatio.value / 100 + '}'
+        }
+      )
+    }
+    if (approveMethod.value === ApproveMethodType.ANY_APPROVE) {
+      multiLoopInstance.value = bpmnInstances().moddle.create(
+        'bpmn:MultiInstanceLoopCharacteristics',
+        { isSequential: false, collection: '${coll_userList}' }
+      )
+      multiLoopInstance.value.completionCondition = bpmnInstances().moddle.create(
+        'bpmn:FormalExpression',
+        {
+          body: '${ nrOfCompletedInstances > 0 }'
+        }
+      )
+    }
+    if (approveMethod.value === ApproveMethodType.SEQUENTIAL_APPROVE) {
+      multiLoopInstance.value = bpmnInstances().moddle.create(
+        'bpmn:MultiInstanceLoopCharacteristics',
+        { isSequential: true, collection: '${coll_userList}' }
+      )
+      multiLoopInstance.value.loopCardinality = bpmnInstances().moddle.create(
+        'bpmn:FormalExpression',
+        {
+          body: '1'
+        }
+      )
+      multiLoopInstance.value.completionCondition = bpmnInstances().moddle.create(
+        'bpmn:FormalExpression',
+        {
+          body: '${ nrOfCompletedInstances >= nrOfInstances }'
+        }
+      )
+    }
+    bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+      loopCharacteristics: toRaw(multiLoopInstance.value)
+    })
+  }
+
+  // 添加ApproveMethod到ExtensionElements
+  const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
+    values: [
+      ...otherExtensions.value,
+      bpmnInstances().moddle.create(`${prefix}:ApproveMethod`, {
+        value: approveMethod.value
+      })
+    ]
+  })
+  bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+    extensionElements: extensions
+  })
+}
+
 onBeforeUnmount(() => {
   multiLoopInstance.value = null
   bpmnElement.value = null
 })
 
 watch(
-  () => props.businessObject,
+  () => props.id,
   (val) => {
-    bpmnElement.value = bpmnInstances().bpmnElement
-    getElementLoop(val)
+    if (val) {
+      nextTick(() => {
+        bpmnElement.value = bpmnInstances().bpmnElement
+        // getElementLoop(val)
+        getElementLoopNew()
+      })
+    }
   },
   { immediate: true }
 )

+ 1 - 2
src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue

@@ -75,7 +75,6 @@ const attributeFormRef = ref()
 const bpmnInstances = () => (window as any)?.bpmnInstances
 
 const resetAttributesList = () => {
-  console.log(window, 'windowwindowwindowwindowwindowwindowwindow')
   bpmnElement.value = bpmnInstances().bpmnElement
   otherExtensionList.value = [] // 其他扩展配置
   bpmnElementProperties.value =
@@ -85,7 +84,7 @@ const resetAttributesList = () => {
         otherExtensionList.value.push(ex)
       }
       return ex.$type === `${prefix}:Properties`
-    }) ?? []
+    }) ?? [];
 
   // 保存所有的 扩展属性字段
   bpmnElementPropertyList.value = bpmnElementProperties.value.reduce(

+ 4 - 20
src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue

@@ -29,9 +29,7 @@
 </template>
 
 <script lang="ts" setup>
-import UserTask from './task-components/UserTask.vue'
-import ScriptTask from './task-components/ScriptTask.vue'
-import ReceiveTask from './task-components/ReceiveTask.vue'
+import { installedComponent } from './data'
 
 defineOptions({ name: 'ElementTaskConfig' })
 
@@ -45,14 +43,7 @@ const taskConfigForm = ref({
   exclusive: false
 })
 const witchTaskComponent = ref()
-const installedComponent = ref({
-  // 手工任务与普通任务一致,不需要其他配置
-  // 接收消息任务,需要在全局下插入新的消息实例,并在该节点下的 messageRef 属性绑定该实例
-  // 发送任务、服务任务、业务规则任务共用一个相同配置
-  UserTask: 'UserTask', // 用户任务配置
-  ScriptTask: 'ScriptTask', // 脚本任务配置
-  ReceiveTask: 'ReceiveTask' // 消息接收任务
-})
+
 const bpmnElement = ref()
 
 const bpmnInstances = () => (window as any).bpmnInstances
@@ -78,15 +69,8 @@ watch(
 watch(
   () => props.type,
   () => {
-    // witchTaskComponent.value = installedComponent.value[props.type]
-    if (props.type == installedComponent.value.UserTask) {
-      witchTaskComponent.value = UserTask
-    }
-    if (props.type == installedComponent.value.ScriptTask) {
-      witchTaskComponent.value = ScriptTask
-    }
-    if (props.type == installedComponent.value.ReceiveTask) {
-      witchTaskComponent.value = ReceiveTask
+    if (props.type) {
+      witchTaskComponent.value = installedComponent[props.type].component
     }
   },
   { immediate: true }

+ 36 - 0
src/components/bpmnProcessDesigner/package/penal/task/data.ts

@@ -0,0 +1,36 @@
+import UserTask from './task-components/UserTask.vue'
+import ServiceTask from './task-components/ServiceTask.vue'
+import ScriptTask from './task-components/ScriptTask.vue'
+import ReceiveTask from './task-components/ReceiveTask.vue'
+import CallActivity from './task-components/CallActivity.vue'
+
+export const installedComponent = {
+  UserTask: {
+    name: '用户任务',
+    component: UserTask
+  },
+  ServiceTask: {
+    name: '服务任务',
+    component: ServiceTask
+  },
+  ScriptTask: {
+    name: '脚本任务',
+    component: ScriptTask
+  },
+  ReceiveTask: {
+    name: '接收任务',
+    component: ReceiveTask
+  },
+  CallActivity: {
+    name: '调用活动',
+    component: CallActivity
+  }
+}
+
+export const getTaskCollapseItemName = (elementType) => {
+  return installedComponent[elementType].name
+}
+
+export const isTaskCollapseItemShow = (elementType) => {
+  return installedComponent[elementType]
+}

+ 280 - 0
src/components/bpmnProcessDesigner/package/penal/task/task-components/CallActivity.vue

@@ -0,0 +1,280 @@
+<template>
+  <div>
+    <el-form label-width="100px">
+      <el-form-item label="实例名称" prop="processInstanceName">
+        <el-input
+          v-model="formData.processInstanceName"
+          clearable
+          placeholder="请输入实例名称"
+          @change="updateCallActivityAttr('processInstanceName')"
+        />
+      </el-form-item>
+
+      <!-- TODO 需要可选择已存在的流程 -->
+      <el-form-item label="被调用流程" prop="calledElement">
+        <el-input
+          v-model="formData.calledElement"
+          clearable
+          placeholder="请输入被调用流程"
+          @change="updateCallActivityAttr('calledElement')"
+        />
+      </el-form-item>
+
+      <el-form-item label="继承变量" prop="inheritVariables">
+        <el-switch
+          v-model="formData.inheritVariables"
+          @change="updateCallActivityAttr('inheritVariables')"
+        />
+      </el-form-item>
+
+      <el-form-item label="继承业务键" prop="inheritBusinessKey">
+        <el-switch
+          v-model="formData.inheritBusinessKey"
+          @change="updateCallActivityAttr('inheritBusinessKey')"
+        />
+      </el-form-item>
+
+      <el-form-item v-if="!formData.inheritBusinessKey" label="业务键表达式" prop="businessKey">
+        <el-input
+          v-model="formData.businessKey"
+          clearable
+          placeholder="请输入业务键表达式"
+          @change="updateCallActivityAttr('businessKey')"
+        />
+      </el-form-item>
+
+      <el-divider />
+      <div>
+        <div class="flex mb-10px">
+          <el-text>输入参数</el-text>
+          <XButton
+            class="ml-auto"
+            type="primary"
+            preIcon="ep:plus"
+            title="添加参数"
+            size="small"
+            @click="openVariableForm('in', null, -1)"
+          />
+        </div>
+        <el-table :data="inVariableList" max-height="240" fit border>
+          <el-table-column label="源" prop="source" min-width="100px" show-overflow-tooltip />
+          <el-table-column label="目标" prop="target" min-width="100px" show-overflow-tooltip />
+          <el-table-column label="操作" width="110px">
+            <template #default="scope">
+              <el-button link @click="openVariableForm('in', scope.row, scope.$index)" size="small">
+                编辑
+              </el-button>
+              <el-divider direction="vertical" />
+              <el-button
+                link
+                size="small"
+                style="color: #ff4d4f"
+                @click="removeVariable('in', scope.$index)"
+              >
+                移除
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+
+      <el-divider />
+      <div>
+        <div class="flex mb-10px">
+          <el-text>输出参数</el-text>
+          <XButton
+            class="ml-auto"
+            type="primary"
+            preIcon="ep:plus"
+            title="添加参数"
+            size="small"
+            @click="openVariableForm('out', null, -1)"
+          />
+        </div>
+        <el-table :data="outVariableList" max-height="240" fit border>
+          <el-table-column label="源" prop="source" min-width="100px" show-overflow-tooltip />
+          <el-table-column label="目标" prop="target" min-width="100px" show-overflow-tooltip />
+          <el-table-column label="操作" width="110px">
+            <template #default="scope">
+              <el-button
+                link
+                @click="openVariableForm('out', scope.row, scope.$index)"
+                size="small"
+              >
+                编辑
+              </el-button>
+              <el-divider direction="vertical" />
+              <el-button
+                link
+                size="small"
+                style="color: #ff4d4f"
+                @click="removeVariable('out', scope.$index)"
+              >
+                移除
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </el-form>
+
+    <!-- 添加或修改参数 -->
+    <el-dialog
+      v-model="variableDialogVisible"
+      title="参数配置"
+      width="600px"
+      append-to-body
+      destroy-on-close
+    >
+      <el-form :model="varialbeFormData" label-width="80px" ref="varialbeFormRef">
+        <el-form-item label="源:" prop="source">
+          <el-input v-model="varialbeFormData.source" clearable />
+        </el-form-item>
+        <el-form-item label="目标:" prop="target">
+          <el-input v-model="varialbeFormData.target" clearable />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="variableDialogVisible = false">取 消</el-button>
+        <el-button type="primary" @click="saveVariable">确 定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+defineOptions({ name: 'CallActivity' })
+const props = defineProps({
+  id: String,
+  type: String
+})
+const prefix = inject('prefix')
+const message = useMessage()
+
+const formData = ref({
+  processInstanceName: '',
+  calledElement: '',
+  inheritVariables: false,
+  businessKey: '',
+  inheritBusinessKey: false,
+  calledElementType: 'key'
+})
+const inVariableList = ref()
+const outVariableList = ref()
+const variableType = ref() // 参数类型
+const editingVariableIndex = ref(-1) // 编辑参数下标
+const variableDialogVisible = ref(false)
+const varialbeFormRef = ref()
+const varialbeFormData = ref({
+  source: '',
+  target: ''
+})
+
+const bpmnInstances = () => (window as any)?.bpmnInstances
+const bpmnElement = ref()
+const otherExtensionList = ref()
+
+const initCallActivity = () => {
+  bpmnElement.value = bpmnInstances().bpmnElement
+  console.log(bpmnElement.value.businessObject, 'callActivity')
+
+  // 初始化所有配置项
+  Object.keys(formData.value).forEach((key) => {
+    formData.value[key] = bpmnElement.value.businessObject[key] ?? formData.value[key]
+  })
+
+  otherExtensionList.value = [] // 其他扩展配置
+  inVariableList.value = []
+  outVariableList.value = []
+  // 初始化输入参数
+  bpmnElement.value.businessObject?.extensionElements?.values?.forEach((ex) => {
+    if (ex.$type === `${prefix}:In`) {
+      inVariableList.value.push(ex)
+    } else if (ex.$type === `${prefix}:Out`) {
+      outVariableList.value.push(ex)
+    } else {
+      otherExtensionList.value.push(ex)
+    }
+  })
+
+  // 默认添加
+  // bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+  //   calledElementType: 'key'
+  // })
+}
+
+const updateCallActivityAttr = (attr) => {
+  bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+    [attr]: formData.value[attr]
+  })
+}
+
+const openVariableForm = (type, data, index) => {
+  editingVariableIndex.value = index
+  variableType.value = type
+  varialbeFormData.value = index === -1 ? {} : { ...data }
+  variableDialogVisible.value = true
+}
+
+const removeVariable = async (type, index) => {
+  try {
+    await message.delConfirm()
+    if (type === 'in') {
+      inVariableList.value.splice(index, 1)
+    }
+    if (type === 'out') {
+      outVariableList.value.splice(index, 1)
+    }
+    updateElementExtensions()
+  } catch {}
+}
+
+const saveVariable = () => {
+  if (editingVariableIndex.value === -1) {
+    if (variableType.value === 'in') {
+      inVariableList.value.push(
+        bpmnInstances().moddle.create(`${prefix}:In`, { ...varialbeFormData.value })
+      )
+    }
+    if (variableType.value === 'out') {
+      outVariableList.value.push(
+        bpmnInstances().moddle.create(`${prefix}:Out`, { ...varialbeFormData.value })
+      )
+    }
+    updateElementExtensions()
+  } else {
+    if (variableType.value === 'in') {
+      inVariableList.value[editingVariableIndex.value].source = varialbeFormData.value.source
+      inVariableList.value[editingVariableIndex.value].target = varialbeFormData.value.target
+    }
+    if (variableType.value === 'out') {
+      outVariableList.value[editingVariableIndex.value].source = varialbeFormData.value.source
+      outVariableList.value[editingVariableIndex.value].target = varialbeFormData.value.target
+    }
+  }
+  variableDialogVisible.value = false
+}
+
+const updateElementExtensions = () => {
+  const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
+    values: [...inVariableList.value, ...outVariableList.value, ...otherExtensionList.value]
+  })
+  bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+    extensionElements: extensions
+  })
+}
+
+watch(
+  () => props.id,
+  (val) => {
+    val &&
+      val.length &&
+      nextTick(() => {
+        initCallActivity()
+      })
+  },
+  { immediate: true }
+)
+</script>
+
+<style lang="scss" scoped></style>

+ 91 - 0
src/components/bpmnProcessDesigner/package/penal/task/task-components/ServiceTask.vue

@@ -0,0 +1,91 @@
+<template>
+  <div>
+    <el-form-item label="执行类型" key="executeType">
+      <el-select v-model="serviceTaskForm.executeType">
+        <el-option label="Java类" value="class" />
+        <el-option label="表达式" value="expression" />
+        <el-option label="代理表达式" value="delegateExpression" />
+      </el-select>
+    </el-form-item>
+    <el-form-item
+      v-if="serviceTaskForm.executeType === 'class'"
+      label="Java类"
+      prop="class"
+      key="execute-class"
+    >
+      <el-input v-model="serviceTaskForm.class" clearable @change="updateElementTask" />
+    </el-form-item>
+    <el-form-item
+      v-if="serviceTaskForm.executeType === 'expression'"
+      label="表达式"
+      prop="expression"
+      key="execute-expression"
+    >
+      <el-input v-model="serviceTaskForm.expression" clearable @change="updateElementTask" />
+    </el-form-item>
+    <el-form-item
+      v-if="serviceTaskForm.executeType === 'delegateExpression'"
+      label="代理表达式"
+      prop="delegateExpression"
+      key="execute-delegate"
+    >
+      <el-input v-model="serviceTaskForm.delegateExpression" clearable @change="updateElementTask" />
+    </el-form-item>
+  </div>
+</template>
+
+<script lang="ts" setup>
+defineOptions({ name: 'ServiceTask' })
+const props = defineProps({
+  id: String,
+  type: String
+})
+
+const defaultTaskForm = ref({
+  executeType: '',
+  class: '',
+  expression: '',
+  delegateExpression: ''
+})
+
+const serviceTaskForm = ref<any>({})
+const bpmnElement = ref()
+
+const bpmnInstances = () => (window as any)?.bpmnInstances
+
+const resetTaskForm = () => {
+  for (let key in defaultTaskForm.value) {
+    let value = bpmnElement.value?.businessObject[key] || defaultTaskForm.value[key]
+    serviceTaskForm.value[key] = value
+    if (value) {
+      serviceTaskForm.value.executeType = key
+    }
+  }
+}
+
+const updateElementTask = () => {
+  let taskAttr = Object.create(null);
+  const type = serviceTaskForm.value.executeType;
+  for (let key in serviceTaskForm.value) {
+    if (key !== 'executeType' && key !== type) taskAttr[key] = null;
+  }
+  taskAttr[type] = serviceTaskForm.value[type] || "";
+  bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr)
+}
+
+onBeforeUnmount(() => {
+  bpmnElement.value = null
+})
+
+watch(
+  () => props.id,
+  () => {
+    bpmnElement.value = bpmnInstances().bpmnElement
+    nextTick(() => {
+      resetTaskForm()
+    })
+  },
+  { immediate: true }
+)
+
+</script>

+ 220 - 15
src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-form label-width="100px">
+  <el-form label-width="120px">
     <el-form-item label="规则类型" prop="candidateStrategy">
       <el-select
         v-model="userTaskForm.candidateStrategy"
@@ -8,15 +8,15 @@
         @change="changeCandidateStrategy"
       >
         <el-option
-          v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_CANDIDATE_STRATEGY)"
-          :key="dict.value"
+          v-for="(dict, index) in CANDIDATE_STRATEGY"
+          :key="index"
           :label="dict.label"
           :value="dict.value"
         />
       </el-select>
     </el-form-item>
     <el-form-item
-      v-if="userTaskForm.candidateStrategy == 10"
+      v-if="userTaskForm.candidateStrategy == CandidateStrategy.ROLE"
       label="指定角色"
       prop="candidateParam"
     >
@@ -31,7 +31,11 @@
       </el-select>
     </el-form-item>
     <el-form-item
-      v-if="userTaskForm.candidateStrategy == 20 || userTaskForm.candidateStrategy == 21"
+      v-if="
+        userTaskForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER ||
+        userTaskForm.candidateStrategy == CandidateStrategy.DEPT_LEADER ||
+        userTaskForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
+      "
       label="指定部门"
       prop="candidateParam"
       span="24"
@@ -49,7 +53,7 @@
       />
     </el-form-item>
     <el-form-item
-      v-if="userTaskForm.candidateStrategy == 22"
+      v-if="userTaskForm.candidateStrategy == CandidateStrategy.POST"
       label="指定岗位"
       prop="candidateParam"
       span="24"
@@ -65,7 +69,7 @@
       </el-select>
     </el-form-item>
     <el-form-item
-      v-if="userTaskForm.candidateStrategy == 30"
+      v-if="userTaskForm.candidateStrategy == CandidateStrategy.USER"
       label="指定用户"
       prop="candidateParam"
       span="24"
@@ -86,7 +90,7 @@
       </el-select>
     </el-form-item>
     <el-form-item
-      v-if="userTaskForm.candidateStrategy === 40"
+      v-if="userTaskForm.candidateStrategy === CandidateStrategy.USER_GROUP"
       label="指定用户组"
       prop="candidateParam"
     >
@@ -106,7 +110,67 @@
       </el-select>
     </el-form-item>
     <el-form-item
-      v-if="userTaskForm.candidateStrategy === 60"
+      v-if="userTaskForm.candidateStrategy === CandidateStrategy.FORM_USER"
+      label="表单内用户字段"
+      prop="formUser"
+    >
+      <el-select
+        v-model="userTaskForm.candidateParam"
+        clearable
+        style="width: 100%"
+        @change="handleFormUserChange"
+      >
+        <el-option
+          v-for="(item, idx) in userFieldOnFormOptions"
+          :key="idx"
+          :label="item.title"
+          :value="item.field"
+          :disabled="!item.required"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item
+      v-if="userTaskForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER"
+      label="表单内部门字段"
+      prop="formDept"
+    >
+      <el-select
+        v-model="userTaskForm.candidateParam"
+        clearable
+        style="width: 100%"
+        @change="updateElementTask"
+      >
+        <el-option
+          v-for="(item, idx) in deptFieldOnFormOptions"
+          :key="idx"
+          :label="item.title"
+          :value="item.field"
+          :disabled="!item.required"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item
+      v-if="
+        userTaskForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
+        userTaskForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
+        userTaskForm.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER ||
+        userTaskForm.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER
+      "
+      :label="deptLevelLabel!"
+      prop="deptLevel"
+      span="24"
+    >
+      <el-select v-model="deptLevel" clearable @change="updateElementTask">
+        <el-option
+          v-for="(item, index) in MULTI_LEVEL_DEPT"
+          :key="index"
+          :label="item.label"
+          :value="item.value"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item
+      v-if="userTaskForm.candidateStrategy === CandidateStrategy.EXPRESSION"
       label="流程表达式"
       prop="candidateParam"
     >
@@ -114,12 +178,17 @@
         type="textarea"
         v-model="userTaskForm.candidateParam[0]"
         clearable
-        style="width: 72%"
+        style="width: 100%"
         @change="updateElementTask"
       />
-      <el-button class="ml-5px" size="small" type="success" @click="openProcessExpressionDialog"
-        >选择表达式</el-button
-      >
+      <XButton
+        class="!w-1/1 mt-5px"
+        type="success"
+        preIcon="ep:select"
+        title="选择表达式"
+        size="small"
+        @click="openProcessExpressionDialog"
+      />
       <!-- 选择弹窗 -->
       <ProcessExpressionDialog ref="processExpressionDialogRef" @select="selectProcessExpression" />
     </el-form-item>
@@ -127,7 +196,12 @@
 </template>
 
 <script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import {
+  CANDIDATE_STRATEGY,
+  CandidateStrategy,
+  FieldPermissionType,
+  MULTI_LEVEL_DEPT
+} from '@/components/SimpleProcessDesignerV2/src/consts'
 import { defaultProps, handleTree } from '@/utils/tree'
 import * as RoleApi from '@/api/system/role'
 import * as DeptApi from '@/api/system/dept'
@@ -136,12 +210,14 @@ import * as UserApi from '@/api/system/user'
 import * as UserGroupApi from '@/api/bpm/userGroup'
 import ProcessExpressionDialog from './ProcessExpressionDialog.vue'
 import { ProcessExpressionVO } from '@/api/bpm/processExpression'
+import { useFormFieldsPermission } from '@/components/SimpleProcessDesignerV2/src/node'
 
 defineOptions({ name: 'UserTask' })
 const props = defineProps({
   id: String,
   type: String
 })
+const prefix = inject('prefix')
 const userTaskForm = ref({
   candidateStrategy: undefined, // 分配规则
   candidateParam: [] // 分配选项
@@ -155,11 +231,88 @@ const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表
 const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
 const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
 
+const { formFieldOptions } = useFormFieldsPermission(FieldPermissionType.READ)
+// 表单内用户字段选项, 必须是必填和用户选择器
+const userFieldOnFormOptions = computed(() => {
+  return formFieldOptions.filter((item) => item.type === 'UserSelect')
+})
+// 表单内部门字段选项, 必须是必填和部门选择器
+const deptFieldOnFormOptions = computed(() => {
+  return formFieldOptions.filter((item) => item.type === 'DeptSelect')
+})
+
+const deptLevel = ref(1)
+const deptLevelLabel = computed(() => {
+  let label = '部门负责人来源'
+  if (userTaskForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) {
+    label = label + '(指定部门向上)'
+  } else if (userTaskForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER) {
+    label = label + '(表单内部门向上)'
+  } else {
+    label = label + '(发起人部门向上)'
+  }
+  return label
+})
+
+const otherExtensions = ref()
+
 const resetTaskForm = () => {
   const businessObject = bpmnElement.value.businessObject
   if (!businessObject) {
     return
   }
+
+  const extensionElements =
+    businessObject?.extensionElements ??
+    bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
+  userTaskForm.value.candidateStrategy = extensionElements.values?.filter(
+    (ex) => ex.$type === `${prefix}:CandidateStrategy`
+  )?.[0]?.value
+  const candidateParamStr = extensionElements.values?.filter(
+    (ex) => ex.$type === `${prefix}:CandidateParam`
+  )?.[0]?.value
+  if (candidateParamStr && candidateParamStr.length > 0) {
+    if (userTaskForm.value.candidateStrategy === CandidateStrategy.EXPRESSION) {
+      // 特殊:流程表达式,只有一个 input 输入框
+      userTaskForm.value.candidateParam = [candidateParamStr]
+    } else if (userTaskForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) {
+      // 特殊:多级不部门负责人,需要通过'|'分割
+      userTaskForm.value.candidateParam = candidateParamStr
+        .split('|')[0]
+        .split(',')
+        .map((item) => {
+          // 如果数字超出了最大安全整数范围,则将其作为字符串处理
+          let num = Number(item)
+          return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER ? item : num
+        })
+      deptLevel.value = +candidateParamStr.split('|')[1]
+    } else if (
+      userTaskForm.value.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
+      userTaskForm.value.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
+    ) {
+      userTaskForm.value.candidateParam = +candidateParamStr
+      deptLevel.value = +candidateParamStr
+    } else if (userTaskForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER) {
+      userTaskForm.value.candidateParam = candidateParamStr.split('|')[0]
+      deptLevel.value = +candidateParamStr.split('|')[1]
+    } else {
+      userTaskForm.value.candidateParam = candidateParamStr.split(',').map((item) => {
+        // 如果数字超出了最大安全整数范围,则将其作为字符串处理
+        let num = Number(item)
+        return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER ? item : num
+      })
+    }
+  } else {
+    userTaskForm.value.candidateParam = []
+  }
+
+  otherExtensions.value =
+    extensionElements.values?.filter(
+      (ex) => ex.$type !== `${prefix}:CandidateStrategy` && ex.$type !== `${prefix}:CandidateParam`
+    ) ?? []
+
+  // 改用通过extensionElements来存储数据
+  return
   if (businessObject.candidateStrategy != undefined) {
     userTaskForm.value.candidateStrategy = parseInt(businessObject.candidateStrategy) as any
   } else {
@@ -172,7 +325,7 @@ const resetTaskForm = () => {
     } else {
       userTaskForm.value.candidateParam = businessObject.candidateParam
         .split(',')
-        .map((item) => +item)
+        .map((item) => item)
     }
   } else {
     userTaskForm.value.candidateParam = []
@@ -182,11 +335,55 @@ const resetTaskForm = () => {
 /** 更新 candidateStrategy 字段时,需要清空 candidateParam,并触发 bpmn 图更新 */
 const changeCandidateStrategy = () => {
   userTaskForm.value.candidateParam = []
+  deptLevel.value = 1
+  if (userTaskForm.value.candidateStrategy === CandidateStrategy.FORM_USER) {
+    // 特殊处理表单内用户字段,当只有发起人选项时应选中发起人
+    if (!userFieldOnFormOptions.value || userFieldOnFormOptions.value.length <= 1) {
+      userTaskForm.value.candidateStrategy = CandidateStrategy.START_USER
+    }
+  }
   updateElementTask()
 }
 
 /** 选中某个 options 时候,更新 bpmn 图  */
 const updateElementTask = () => {
+  let candidateParam =
+    userTaskForm.value.candidateParam instanceof Array
+      ? userTaskForm.value.candidateParam.join(',')
+      : userTaskForm.value.candidateParam
+
+  // 特殊处理多级部门情况
+  if (
+    userTaskForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
+    userTaskForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER
+  ) {
+    candidateParam += '|' + deptLevel.value
+  }
+  // 特殊处理发起人部门负责人、发起人连续部门负责人
+  if (
+    userTaskForm.value.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
+    userTaskForm.value.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
+  ) {
+    candidateParam = deptLevel.value + ''
+  }
+
+  const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
+    values: [
+      ...otherExtensions.value,
+      bpmnInstances().moddle.create(`${prefix}:CandidateStrategy`, {
+        value: userTaskForm.value.candidateStrategy
+      }),
+      bpmnInstances().moddle.create(`${prefix}:CandidateParam`, {
+        value: candidateParam
+      })
+    ]
+  })
+  bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+    extensionElements: extensions
+  })
+
+  // 改用通过extensionElements来存储数据
+  return
   bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
     candidateStrategy: userTaskForm.value.candidateStrategy,
     candidateParam: userTaskForm.value.candidateParam.join(',')
@@ -203,6 +400,14 @@ const selectProcessExpression = (expression: ProcessExpressionVO) => {
   updateElementTask()
 }
 
+const handleFormUserChange = (e) => {
+  if (e === 'PROCESS_START_USER_ID') {
+    userTaskForm.value.candidateParam = []
+    userTaskForm.value.candidateStrategy = CandidateStrategy.START_USER
+  }
+  updateElementTask()
+}
+
 watch(
   () => props.id,
   () => {

+ 6 - 8
src/components/bpmnProcessDesigner/package/theme/process-designer.scss

@@ -1,6 +1,4 @@
 @use 'bpmn-js-token-simulation/assets/css/bpmn-js-token-simulation.css';
-@use 'bpmn-js-token-simulation/assets/css/font-awesome.min.css';
-@use 'bpmn-js-token-simulation/assets/css/normalize.css';
 
 // 边框被 token-simulation 样式覆盖了
 .djs-palette {
@@ -97,12 +95,12 @@
         box-sizing: border-box;
       }
     }
-    svg {
-      width: 100%;
-      height: 100%;
-      min-height: 100%;
-      overflow: hidden;
-    }
+    // svg {
+    //   width: 100%;
+    //   height: 100%;
+    //   min-height: 100%;
+    //   overflow: hidden;
+    // }
   }
 }
 

+ 1 - 2
src/config/axios/service.ts

@@ -44,8 +44,7 @@ service.interceptors.request.use(
     // 是否需要设置 token
     let isToken = (config!.headers || {}).isToken === false
     whiteList.some((v) => {
-      if (config.url) {
-        config.url.indexOf(v) > -1
+      if (config.url && config.url.indexOf(v) > -1) {
         return (isToken = false)
       }
     })

+ 12 - 9
src/directives/permission/hasPermi.ts

@@ -5,18 +5,10 @@ const { t } = useI18n() // 国际化
 
 export function hasPermi(app: App<Element>) {
   app.directive('hasPermi', (el, binding) => {
-    const { wsCache } = useCache()
     const { value } = binding
-    const all_permission = '*:*:*'
-    const userInfo = wsCache.get(CACHE_KEY.USER)
-    const permissions = userInfo?.permissions || []
 
     if (value && value instanceof Array && value.length > 0) {
-      const permissionFlag = value
-
-      const hasPermissions = permissions.some((permission: string) => {
-        return all_permission === permission || permissionFlag.includes(permission)
-      })
+      const hasPermissions = hasPermission(value)
 
       if (!hasPermissions) {
         el.parentNode && el.parentNode.removeChild(el)
@@ -26,3 +18,14 @@ export function hasPermi(app: App<Element>) {
     }
   })
 }
+
+export const hasPermission = (permission: string[]) => {
+  const { wsCache } = useCache()
+  const all_permission = '*:*:*'
+  const userInfo = wsCache.get(CACHE_KEY.USER)
+  const permissions = userInfo?.permissions || []
+
+  return permissions.some((p: string) => {
+    return all_permission === p || permission.includes(p)
+  })
+}

+ 1 - 1
src/layout/components/useRenderLayout.tsx

@@ -197,7 +197,7 @@ export const useRenderLayout = () => {
               `${prefixCls}-content-scrollbar`,
               {
                 '!h-[calc(100%-var(--tags-view-height))] mt-[calc(var(--tags-view-height))]':
-                  fixedHeader.value
+                  fixedHeader.value && tagsView.value
               }
             ]}
           >

+ 4 - 1
src/locales/en.ts

@@ -140,7 +140,10 @@ export default {
     btnQRCode: 'QR code sign in',
     qrcode: 'Scan the QR code to log in',
     btnRegister: 'Sign up',
-    SmsSendMsg: 'code has been sent'
+    SmsSendMsg: 'code has been sent',
+    resetPassword: "Reset Password",
+    resetPasswordSuccess: "Reset Password Success",
+    invalidTenantName:"Invalid Tenant Name"
   },
   captcha: {
     verification: 'Please complete security verification',

+ 4 - 1
src/locales/zh-CN.ts

@@ -141,7 +141,10 @@ export default {
     btnQRCode: '二维码登录',
     qrcode: '扫描二维码登录',
     btnRegister: '注册',
-    SmsSendMsg: '验证码已发送'
+    SmsSendMsg: '验证码已发送',
+    resetPassword: "重置密码",
+    resetPasswordSuccess: "重置密码成功",
+    invalidTenantName: "无效的租户名称"
   },
   captcha: {
     verification: '请完成安全验证',

+ 24 - 0
src/router/modules/remaining.ts

@@ -330,6 +330,30 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: '查看 OA 请假',
           activeMenu: '/bpm/oa/leave'
         }
+      },
+      {
+        path: 'manager/model/create',
+        component: () => import('@/views/bpm/model/form/index.vue'),
+        name: 'BpmModelCreate',
+        meta: {
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          title: '创建流程',
+          activeMenu: '/bpm/manager/model'
+        }
+      },
+      {
+        path: 'manager/model/update/:id',
+        component: () => import('@/views/bpm/model/form/index.vue'),
+        name: 'BpmModelUpdate',
+        meta: {
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          title: '修改流程',
+          activeMenu: '/bpm/manager/model'
+        }
       }
     ]
   },

+ 1 - 2
src/utils/routerHelper.ts

@@ -73,7 +73,7 @@ export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecord
       noCache: !route.keepAlive,
       alwaysShow:
         route.children &&
-        route.children.length === 1 &&
+        route.children.length > 0 &&
         (route.alwaysShow !== undefined ? route.alwaysShow : true)
     } as any
     // 特殊逻辑:如果后端配置的 MenuDO.component 包含 ?,则表示需要传递参数
@@ -100,7 +100,6 @@ export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecord
     //处理顶级非目录路由
     if (!route.children && route.parentId == 0 && route.component) {
       data.component = Layout
-      data.meta = {}
       data.name = toCamelCase(route.path, true) + 'Parent'
       data.redirect = ''
       meta.alwaysShow = true

+ 3 - 0
src/utils/tree.ts

@@ -376,6 +376,9 @@ export const treeToString = (tree: any[], nodeId) => {
   let str = ''
 
   function performAThoroughValidation(arr) {
+    if (typeof arr === 'undefined' || !Array.isArray(arr) || arr.length === 0) {
+      return false
+    }
     for (const item of arr) {
       if (item.id === nodeId) {
         str += ` / ${item.name}`

+ 3 - 1
src/views/Login/Login.vue

@@ -59,6 +59,8 @@
             <RegisterForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
             <!-- 三方登录 -->
             <SSOLoginVue class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
+            <!-- 忘记密码 -->
+            <ForgetPasswordForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
           </div>
         </Transition>
       </div>
@@ -73,7 +75,7 @@ import { useAppStore } from '@/store/modules/app'
 import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
 import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
 
-import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue } from './components'
+import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue, ForgetPasswordForm } from './components'
 
 defineOptions({ name: 'Login' })
 

+ 1 - 0
src/views/Login/SocialLogin.vue

@@ -133,6 +133,7 @@
                   </el-form-item>
                 </el-col>
                 <Verify
+                  v-if="loginData.captchaEnable === 'true'"
                   ref="verify"
                   :captchaType="captchaType"
                   :imgSize="{ width: '400px', height: '200px' }"

+ 278 - 0
src/views/Login/components/ForgetPasswordForm.vue

@@ -0,0 +1,278 @@
+<template>
+  <el-form
+    v-show="getShow"
+    ref="formSmsResetPassword"
+    :model="resetPasswordData"
+    :rules="rules"
+    class="login-form"
+    label-position="top"
+    label-width="120px"
+    size="large"
+  >
+    <el-row style="margin-right: -10px; margin-left: -10px">
+      <!-- 租户名 -->
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item>
+          <LoginFormTitle style="width: 100%" />
+        </el-form-item>
+      </el-col>
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item v-if="resetPasswordData.tenantEnable === 'true'" prop="tenantName">
+          <el-input
+            v-model="resetPasswordData.tenantName"
+            :placeholder="t('login.tenantNamePlaceholder')"
+            :prefix-icon="iconHouse"
+            type="primary"
+            link
+          />
+        </el-form-item>
+      </el-col>
+      <!-- 手机号 -->
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item prop="mobile">
+          <el-input
+            v-model="resetPasswordData.mobile"
+            :placeholder="t('login.mobileNumberPlaceholder')"
+            :prefix-icon="iconCellphone"
+          />
+        </el-form-item>
+      </el-col>
+      <Verify
+        ref="verify"
+        :captchaType="captchaType"
+        :imgSize="{ width: '400px', height: '200px' }"
+        mode="pop"
+        @success="getSmsCode"
+      />
+      <!-- 验证码 -->
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item prop="code">
+          <el-row :gutter="5" justify="space-between" style="width: 100%">
+            <el-col :span="24">
+              <el-input
+                v-model="resetPasswordData.code"
+                :placeholder="t('login.codePlaceholder')"
+                :prefix-icon="iconCircleCheck"
+              >
+                <template #append>
+                  <span
+                    v-if="mobileCodeTimer <= 0"
+                    class="getMobileCode"
+                    style="cursor: pointer"
+                    @click="getCode"
+                  >
+                    {{ t('login.getSmsCode') }}
+                  </span>
+                  <span v-if="mobileCodeTimer > 0" class="getMobileCode" style="cursor: pointer">
+                    {{ mobileCodeTimer }}秒后可重新获取
+                  </span>
+                </template>
+              </el-input>
+              <!-- </el-button> -->
+            </el-col>
+          </el-row>
+        </el-form-item>
+      </el-col>
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item prop="password">
+          <InputPassword
+            v-model="resetPasswordData.password"
+            :placeholder="t('login.passwordPlaceholder')"
+            style="width: 100%"
+            strength="true"
+          />
+        </el-form-item>
+      </el-col>
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item prop="check_password">
+          <InputPassword
+            v-model="resetPasswordData.check_password"
+            :placeholder="t('login.checkPassword')"
+            style="width: 100%"
+            strength="true"
+          />
+        </el-form-item>
+      </el-col>
+      <!-- 登录按钮 / 返回按钮 -->
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item>
+          <XButton
+            :loading="loginLoading"
+            :title="t('login.resetPassword')"
+            class="w-[100%]"
+            type="primary"
+            @click="resetPassword()"
+          />
+        </el-form-item>
+      </el-col>
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item>
+          <XButton
+            :loading="loginLoading"
+            :title="t('login.backLogin')"
+            class="w-[100%]"
+            @click="handleBackLogin()"
+          />
+        </el-form-item>
+      </el-col>
+    </el-row>
+  </el-form>
+</template>
+<script lang="ts" setup>
+import type { RouteLocationNormalizedLoaded } from 'vue-router'
+
+import { useIcon } from '@/hooks/web/useIcon'
+
+import { sendSmsCode, smsResetPassword } from '@/api/login'
+import LoginFormTitle from './LoginFormTitle.vue'
+import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
+import { ElLoading } from 'element-plus'
+import * as authUtil from '@/utils/auth'
+import * as LoginApi from '@/api/login'
+defineOptions({ name: 'ForgetPasswordForm' })
+const verify = ref()
+
+const { t } = useI18n()
+const message = useMessage()
+const { currentRoute, push } = useRouter()
+const formSmsResetPassword = ref()
+const loginLoading = ref(false)
+const iconHouse = useIcon({ icon: 'ep:house' })
+const iconCellphone = useIcon({ icon: 'ep:cellphone' })
+const iconCircleCheck = useIcon({ icon: 'ep:circle-check' })
+const { validForm } = useFormValid(formSmsResetPassword)
+const { handleBackLogin, getLoginState, setLoginState } = useLoginState()
+const getShow = computed(() => unref(getLoginState) === LoginStateEnum.RESET_PASSWORD)
+const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字
+
+const validatePass2 = (rule, value, callback) => {
+  if (value === '') {
+    callback(new Error('请再次输入密码'))
+  } else if (value !== resetPasswordData.password) {
+    callback(new Error('两次输入密码不一致!'))
+  } else {
+    callback()
+  }
+}
+
+const rules = {
+  tenantName: [{ required: true, min: 2, max: 20, trigger: 'blur', message: '长度为4到16位' }],
+  mobile: [{ required: true, min: 11, max: 11, trigger: 'blur', message: '手机号长度为11位' }],
+  password: [
+    {
+      required: true,
+      min: 4,
+      max: 16,
+      validator: validatePass2,
+      trigger: 'blur',
+      message: '密码长度为4到16位'
+    }
+  ],
+  check_password: [{ required: true, validator: validatePass2, trigger: 'blur' }],
+  code: [required]
+}
+
+const resetPasswordData = reactive({
+  captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
+  tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
+  tenantName: '',
+  username: '',
+  password: '',
+  check_password: '',
+  mobile: '',
+  code: ''
+})
+
+const smsVO = reactive({
+  tenantName: '',
+  mobile: '',
+  captchaVerification: '',
+  scene: 23
+})
+const mobileCodeTimer = ref(0)
+const redirect = ref<string>('')
+
+// 获取验证码
+const getCode = async () => {
+  // 情况一,未开启:则直接发送验证码
+  if (resetPasswordData.captchaEnable === 'false') {
+    await getSmsCode({})
+  } else {
+    // 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行发送验证码
+    // 弹出验证码
+    verify.value.show()
+  }
+}
+
+const getSmsCode = async (params) => {
+  if (resetPasswordData.tenantEnable === 'true') {
+    await getTenantId()
+  }
+  smsVO.captchaVerification = params.captchaVerification
+  smsVO.mobile = resetPasswordData.mobile
+  await sendSmsCode(smsVO).then(async () => {
+    message.success(t('login.SmsSendMsg'))
+    // 设置倒计时
+    mobileCodeTimer.value = 60
+    let msgTimer = setInterval(() => {
+      mobileCodeTimer.value = mobileCodeTimer.value - 1
+      if (mobileCodeTimer.value <= 0) {
+        clearInterval(msgTimer)
+      }
+    }, 1000)
+  })
+}
+watch(
+  () => currentRoute.value,
+  (route: RouteLocationNormalizedLoaded) => {
+    redirect.value = route?.query?.redirect as string
+  },
+  {
+    immediate: true
+  }
+)
+
+const getTenantId = async () => {
+  if (resetPasswordData.tenantEnable === 'true') {
+    const res = await LoginApi.getTenantIdByName(resetPasswordData.tenantName)
+    if (res == null) {
+      message.error(t('login.invalidTenantName'))
+      throw t('login.invalidTenantName')
+    }
+    authUtil.setTenantId(res)
+  }
+}
+
+// 重置密码
+const resetPassword = async () => {
+  const data = await validForm()
+  if (!data) return
+  await getTenantId()
+  loginLoading.value = true
+  await smsResetPassword(resetPasswordData)
+    .then(async () => {
+      message.success(t('login.resetPasswordSuccess'))
+      setLoginState(LoginStateEnum.LOGIN)
+    })
+    .catch(() => {})
+    .finally(() => {
+      loginLoading.value = false
+      setTimeout(() => {
+        const loadingInstance = ElLoading.service()
+        loadingInstance.close()
+      }, 400)
+    })
+}
+</script>
+
+<style lang="scss" scoped>
+:deep(.anticon) {
+  &:hover {
+    color: var(--el-color-primary) !important;
+  }
+}
+
+.smsbtn {
+  margin-top: 33px;
+}
+</style>

+ 11 - 5
src/views/Login/components/LoginForm.vue

@@ -59,7 +59,13 @@
               </el-checkbox>
             </el-col>
             <el-col :offset="6" :span="12">
-              <el-link style="float: right" type="primary">{{ t('login.forgetPassword') }}</el-link>
+              <el-link
+                style="float: right"
+                type="primary"
+                @click="setLoginState(LoginStateEnum.RESET_PASSWORD)"
+              >
+                {{ t('login.forgetPassword') }}
+              </el-link>
             </el-col>
           </el-row>
         </el-form-item>
@@ -76,6 +82,7 @@
         </el-form-item>
       </el-col>
       <Verify
+        v-if="loginData.captchaEnable === 'true'"
         ref="verify"
         :captchaType="captchaType"
         :imgSize="{ width: '400px', height: '200px' }"
@@ -241,7 +248,7 @@ const getTenantByWebsite = async () => {
 }
 const loading = ref() // ElLoading.service 返回的实例
 // 登录
-const handleLogin = async (params) => {
+const handleLogin = async (params: any) => {
   loginLoading.value = true
   try {
     await getTenantId()
@@ -273,7 +280,7 @@ const handleLogin = async (params) => {
     if (redirect.value.indexOf('sso') !== -1) {
       window.location.href = window.location.href.replace('/login?redirect=', '')
     } else {
-      push({ path: redirect.value || permissionStore.addRouters[0].path })
+      await push({ path: redirect.value || permissionStore.addRouters[0].path })
     }
   } finally {
     loginLoading.value = false
@@ -313,8 +320,7 @@ const doSocialLogin = async (type: number) => {
       encodeURIComponent(`type=${type}&redirect=${redirect.value || '/'}`)
 
     // 进行跳转
-    const res = await LoginApi.socialAuthRedirect(type, encodeURIComponent(redirectUri))
-    window.location.href = res
+    window.location.href = await LoginApi.socialAuthRedirect(type, encodeURIComponent(redirectUri))
   }
 }
 watch(

+ 1 - 0
src/views/Login/components/RegisterForm.vue

@@ -85,6 +85,7 @@
         </el-form-item>
       </el-col>
       <Verify
+        v-if="registerData.captchaEnable === 'true'"
         ref="verify"
         :captchaType="captchaType"
         :imgSize="{ width: '400px', height: '200px' }"

+ 2 - 1
src/views/Login/components/index.ts

@@ -4,5 +4,6 @@ import LoginFormTitle from './LoginFormTitle.vue'
 import RegisterForm from './RegisterForm.vue'
 import QrCodeForm from './QrCodeForm.vue'
 import SSOLoginVue from './SSOLogin.vue'
+import ForgetPasswordForm from './ForgetPasswordForm.vue'
 
-export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue }
+export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue, ForgetPasswordForm }

+ 12 - 33
src/views/bpm/model/CategoryDraggableModel.vue

@@ -64,6 +64,7 @@
       </div>
     </div>
   </div>
+
   <!-- 模型列表 -->
   <el-collapse-transition>
     <div v-show="isExpand">
@@ -90,7 +91,7 @@
             </div>
           </template>
         </el-table-column>
-        <el-table-column label="可见范围" prop="startUserIds" min-width="100">
+        <el-table-column label="可见范围" prop="startUserIds" min-width="150">
           <template #default="scope">
             <el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0">
               全部可见
@@ -110,7 +111,7 @@
             </el-text>
           </template>
         </el-table-column>
-        <el-table-column label="表单信息" prop="formType" min-width="200">
+        <el-table-column label="表单信息" prop="formType" min-width="150">
           <template #default="scope">
             <el-button
               v-if="scope.row.formType === BpmModelFormType.NORMAL"
@@ -162,16 +163,6 @@
             >
               修改
             </el-button>
-            <el-button
-              link
-              class="!ml-5px"
-              type="primary"
-              @click="handleDesign(scope.row)"
-              v-hasPermi="['bpm:model:update']"
-              :disabled="!isManagerUser(scope.row)"
-            >
-              设计
-            </el-button>
             <el-button
               link
               class="!ml-5px"
@@ -249,7 +240,7 @@ import { formatDate } from '@/utils/formatTime'
 import * as ModelApi from '@/api/bpm/model'
 import * as FormApi from '@/api/bpm/form'
 import { setConfAndFields2 } from '@/utils/formCreate'
-import { BpmModelFormType, BpmModelType } from '@/utils/constants'
+import { BpmModelFormType } from '@/utils/constants'
 import { checkPermi } from '@/utils/permission'
 import { useUserStoreWithOut } from '@/store/modules/user'
 import { useAppStore } from '@/store/modules/app'
@@ -337,25 +328,6 @@ const handleChangeState = async (row: any) => {
   } catch {}
 }
 
-/** 设计流程 */
-const handleDesign = (row: any) => {
-  if (row.type == BpmModelType.BPMN) {
-    push({
-      name: 'BpmModelEditor',
-      query: {
-        modelId: row.id
-      }
-    })
-  } else {
-    push({
-      name: 'SimpleModelDesign',
-      query: {
-        modelId: row.id
-      }
-    })
-  }
-}
-
 /** 发布流程 */
 const handleDeploy = async (row: any) => {
   try {
@@ -496,7 +468,14 @@ const handleDeleteCategory = async () => {
 /** 添加流程模型弹窗 */
 const modelFormRef = ref()
 const openModelForm = (type: string, id?: number) => {
-  modelFormRef.value.open(type, id)
+  if (type === 'create') {
+    push({ name: 'BpmModelCreate' })
+  } else {
+    push({
+      name: 'BpmModelUpdate',
+      params: { id }
+    })
+  }
 }
 
 watch(() => props.categoryInfo.modelList, updateModeList, { immediate: true })

+ 161 - 22
src/views/bpm/model/ModelForm.vue

@@ -123,29 +123,69 @@
           </el-radio>
         </el-radio-group>
       </el-form-item>
-      <el-form-item label="谁可以发起" prop="startUserIds">
+      <el-form-item label="谁可以发起" prop="startUserType">
         <el-select
-          v-model="formData.startUserIds"
-          multiple
-          placeholder="请选择可发起人,默认(不选择)则所有人都可以发起"
+          v-model="formData.startUserType"
+          placeholder="请选择谁可以发起"
+          @change="handleStartUserTypeChange"
         >
-          <el-option
-            v-for="user in userList"
-            :key="user.id"
-            :label="user.nickname"
-            :value="user.id"
-          />
+          <el-option label="全员" :value="0" />
+          <el-option label="指定人员" :value="1" />
+          <el-option label="均不可提交" :value="2" />
         </el-select>
-      </el-form-item>
-      <el-form-item label="流程管理员" prop="managerUserIds">
-        <el-select v-model="formData.managerUserIds" multiple placeholder="请选择流程管理员">
-          <el-option
-            v-for="user in userList"
+        <div v-if="formData.startUserType === 1" class="mt-2 flex flex-wrap gap-2">
+          <div
+            v-for="user in selectedStartUsers"
             :key="user.id"
-            :label="user.nickname"
-            :value="user.id"
-          />
+            class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+          >
+            <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
+            <el-avatar class="!m-5px" :size="28" v-else>
+              {{ user.nickname.substring(0, 1) }}
+            </el-avatar>
+            {{ user.nickname }}
+            <Icon
+              icon="ep:close"
+              class="ml-2 cursor-pointer hover:text-red-500"
+              @click="handleRemoveStartUser(user)"
+            />
+          </div>
+          <el-button type="primary" link @click="openStartUserSelect">
+            <Icon icon="ep:plus" />选择人员
+          </el-button>
+        </div>
+      </el-form-item>
+      <el-form-item label="流程管理员" prop="managerUserType">
+        <el-select
+          v-model="formData.managerUserType"
+          placeholder="请选择流程管理员"
+          @change="handleManagerUserTypeChange"
+        >
+          <el-option label="全员" :value="0" />
+          <el-option label="指定人员" :value="1" />
+          <el-option label="均不可提交" :value="2" />
         </el-select>
+        <div v-if="formData.managerUserType === 1" class="mt-2 flex flex-wrap gap-2">
+          <div
+            v-for="user in selectedManagerUsers"
+            :key="user.id"
+            class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+          >
+            <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
+            <el-avatar class="!m-5px" :size="28" v-else>
+              {{ user.nickname.substring(0, 1) }}
+            </el-avatar>
+            {{ user.nickname }}
+            <Icon
+              icon="ep:close"
+              class="ml-2 cursor-pointer hover:text-red-500"
+              @click="handleRemoveManagerUser(user)"
+            />
+          </div>
+          <el-button type="primary" link @click="openManagerUserSelect">
+            <Icon icon="ep:plus" />选择人员
+          </el-button>
+        </div>
       </el-form-item>
     </el-form>
     <template #footer>
@@ -153,6 +193,7 @@
       <el-button @click="dialogVisible = false">取 消</el-button>
     </template>
   </Dialog>
+  <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
 </template>
 <script lang="ts" setup>
 import { propTypes } from '@/utils/propTypes'
@@ -160,11 +201,12 @@ import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
 import { ElMessageBox } from 'element-plus'
 import * as ModelApi from '@/api/bpm/model'
 import * as FormApi from '@/api/bpm/form'
-import { CategoryApi } from '@/api/bpm/category'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
 import { BpmModelFormType, BpmModelType } from '@/utils/constants'
 import { UserVO } from '@/api/system/user'
 import * as UserApi from '@/api/system/user'
 import { useUserStoreWithOut } from '@/store/modules/user'
+import { FormVO } from '@/api/bpm/form'
 
 defineOptions({ name: 'ModelForm' })
 
@@ -178,7 +220,7 @@ const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref({
+const formData: any = ref({
   id: undefined,
   name: '',
   key: '',
@@ -191,6 +233,8 @@ const formData = ref({
   formCustomCreatePath: '',
   formCustomViewPath: '',
   visible: true,
+  startUserType: undefined,
+  managerUserType: undefined,
   startUserIds: [],
   managerUserIds: []
 })
@@ -208,9 +252,13 @@ const formRules = reactive({
   managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
-const formList = ref([]) // 流程表单的下拉框的数据
-const categoryList = ref([]) // 流程分类列表
+const formList = ref<FormVO[]>([]) // 流程表单的下拉框的数据
+const categoryList = ref<CategoryVO[]>([]) // 流程分类列表
 const userList = ref<UserVO[]>([]) // 用户列表
+const selectedStartUsers = ref<UserVO[]>([]) // 已选择的发起人列表
+const selectedManagerUsers = ref<UserVO[]>([]) // 已选择的管理员列表
+const userSelectFormRef = ref() // 用户选择弹窗 ref
+const currentSelectType = ref<'start' | 'manager'>('start') // 当前选择的是发起人还是管理员
 
 /** 打开弹窗 */
 const open = async (type: string, id?: string) => {
@@ -226,6 +274,19 @@ const open = async (type: string, id?: string) => {
     } finally {
       formLoading.value = false
     }
+    // 加载数据时,根据已有的用户ID列表初始化已选用户
+    if (formData.value.startUserIds?.length) {
+      formData.value.startUserType = 1
+      selectedStartUsers.value = userList.value.filter((user) =>
+        formData.value.startUserIds.includes(user.id)
+      )
+    }
+    if (formData.value.managerUserIds?.length) {
+      formData.value.managerUserType = 1
+      selectedManagerUsers.value = userList.value.filter((user) =>
+        formData.value.managerUserIds.includes(user.id)
+      )
+    }
   } else {
     formData.value.managerUserIds.push(userStore.getUser.id)
   }
@@ -293,9 +354,87 @@ const resetForm = () => {
     formCustomCreatePath: '',
     formCustomViewPath: '',
     visible: true,
+    startUserType: undefined,
+    managerUserType: undefined,
     startUserIds: [],
     managerUserIds: []
   }
   formRef.value?.resetFields()
+  selectedStartUsers.value = []
+  selectedManagerUsers.value = []
+}
+
+/** 处理发起人类型变化 */
+const handleStartUserTypeChange = (value: number) => {
+  if (value !== 1) {
+    selectedStartUsers.value = []
+    formData.value.startUserIds = []
+  }
+}
+
+/** 处理管理员类型变化 */
+const handleManagerUserTypeChange = (value: number) => {
+  if (value !== 1) {
+    selectedManagerUsers.value = []
+    formData.value.managerUserIds = []
+  }
+}
+
+/** 打开发起人选择 */
+const openStartUserSelect = () => {
+  currentSelectType.value = 'start'
+  userSelectFormRef.value.open(0, selectedStartUsers.value)
+}
+
+/** 打开管理员选择 */
+const openManagerUserSelect = () => {
+  currentSelectType.value = 'manager'
+  userSelectFormRef.value.open(0, selectedManagerUsers.value)
+}
+
+/** 处理用户选择确认 */
+const handleUserSelectConfirm = (_, users: UserVO[]) => {
+  if (currentSelectType.value === 'start') {
+    selectedStartUsers.value = users
+    formData.value.startUserIds = users.map((u) => u.id)
+  } else {
+    selectedManagerUsers.value = users
+    formData.value.managerUserIds = users.map((u) => u.id)
+  }
+}
+
+/** 移除发起人 */
+const handleRemoveStartUser = (user: UserVO) => {
+  selectedStartUsers.value = selectedStartUsers.value.filter((u) => u.id !== user.id)
+  formData.value.startUserIds = formData.value.startUserIds.filter((id: number) => id !== user.id)
+}
+
+/** 移除管理员 */
+const handleRemoveManagerUser = (user: UserVO) => {
+  selectedManagerUsers.value = selectedManagerUsers.value.filter((u) => u.id !== user.id)
+  formData.value.managerUserIds = formData.value.managerUserIds.filter(
+    (id: number) => id !== user.id
+  )
 }
 </script>
+
+<style lang="scss" scoped>
+.bg-gray-100 {
+  background-color: #f5f7fa;
+  transition: all 0.3s;
+
+  &:hover {
+    background-color: #e6e8eb;
+  }
+
+  .ep-close {
+    font-size: 14px;
+    color: #909399;
+    transition: color 0.3s;
+
+    &:hover {
+      color: #f56c6c;
+    }
+  }
+}
+</style>

+ 212 - 48
src/views/bpm/model/editor/index.vue

@@ -3,7 +3,6 @@
     <!-- 流程设计器,负责绘制流程等 -->
     <MyProcessDesigner
       key="designer"
-      v-if="xmlString !== undefined"
       v-model="xmlString"
       :value="xmlString"
       v-bind="controlForm"
@@ -11,12 +10,14 @@
       ref="processDesigner"
       @init-finished="initModeler"
       :additionalModel="controlForm.additionalModel"
+      :model="model"
       @save="save"
     />
     <!-- 流程属性器,负责编辑每个流程节点的属性 -->
     <MyProcessPenal
+      v-if="isModelerReady && modeler"
       key="penal"
-      :bpmnModeler="modeler as any"
+      :bpmnModeler="modeler"
       :prefix="controlForm.prefix"
       class="process-panel"
       :model="model"
@@ -34,12 +35,26 @@ import * as ModelApi from '@/api/bpm/model'
 
 defineOptions({ name: 'BpmModelEditor' })
 
-const router = useRouter() // 路由
-const { query } = useRoute() // 路由的查询
+const props = defineProps<{
+  modelId?: string
+  modelKey?: string
+  modelName?: string
+  value?: string
+}>()
+
+const emit = defineEmits(['success', 'init-finished'])
 const message = useMessage() // 国际化
 
-const xmlString = ref(undefined) // BPMN XML
-const modeler = ref(null) // BPMN Modeler
+// 表单信息
+const formFields = ref<string[]>([])
+const formType = ref(20)
+provide('formFields', formFields)
+provide('formType', formType)
+
+const xmlString = ref<string>('') // BPMN XML
+const modeler = shallowRef() // BPMN Modeler
+const processDesigner = ref()
+const isModelerReady = ref(false)
 const controlForm = ref({
   simulation: true,
   labelEditing: false,
@@ -50,66 +65,215 @@ const controlForm = ref({
 })
 const model = ref<ModelApi.ModelVO>() // 流程模型的信息
 
+// 初始化 bpmnInstances
+const initBpmnInstances = () => {
+  if (!modeler.value) return false
+  try {
+    const instances = {
+      modeler: modeler.value,
+      modeling: modeler.value.get('modeling'),
+      moddle: modeler.value.get('moddle'),
+      eventBus: modeler.value.get('eventBus'),
+      bpmnFactory: modeler.value.get('bpmnFactory'),
+      elementFactory: modeler.value.get('elementFactory'),
+      elementRegistry: modeler.value.get('elementRegistry'),
+      replace: modeler.value.get('replace'),
+      selection: modeler.value.get('selection')
+    }
+
+    // 检查所有实例是否都存在
+    return Object.values(instances).every((instance) => instance)
+  } catch (error) {
+    console.error('初始化 bpmnInstances 失败:', error)
+    return false
+  }
+}
+
 /** 初始化 modeler */
-const initModeler = (item) => {
-  setTimeout(() => {
+const initModeler = async (item) => {
+  try {
     modeler.value = item
-  }, 10)
+    // 等待 modeler 初始化完成
+    await nextTick()
+
+    // 确保 modeler 的所有实例都已经准备好
+    if (initBpmnInstances()) {
+      isModelerReady.value = true
+      emit('init-finished')
+
+      // 初始化完成后,设置初始值
+      if (props.modelId) {
+        // 编辑模式
+        const data = await ModelApi.getModel(props.modelId)
+        model.value = {
+          ...data,
+          bpmnXml: undefined // 清空 bpmnXml 属性
+        }
+        xmlString.value = data.bpmnXml || getDefaultBpmnXml(data.key, data.name)
+      } else if (props.modelKey && props.modelName) {
+        // 新建模式
+        xmlString.value = props.value || getDefaultBpmnXml(props.modelKey, props.modelName)
+        model.value = {
+          key: props.modelKey,
+          name: props.modelName
+        } as ModelApi.ModelVO
+      }
+
+      // 导入XML并刷新视图
+      await nextTick()
+      try {
+        await modeler.value.importXML(xmlString.value)
+        if (processDesigner.value?.refresh) {
+          processDesigner.value.refresh()
+        }
+      } catch (error) {
+        console.error('导入XML失败:', error)
+      }
+    } else {
+      console.error('modeler 实例未完全初始化')
+    }
+  } catch (error) {
+    console.error('初始化 modeler 失败:', error)
+  }
+}
+
+/** 获取默认的BPMN XML */
+const getDefaultBpmnXml = (key: string, name: string) => {
+  return `<?xml version="1.0" encoding="UTF-8"?>
+<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.activiti.org/processdef">
+  <process id="${key}" name="${name}" isExecutable="true" />
+  <bpmndi:BPMNDiagram id="BPMNDiagram">
+    <bpmndi:BPMNPlane id="${key}_di" bpmnElement="${key}" />
+  </bpmndi:BPMNDiagram>
+</definitions>`
 }
 
 /** 添加/修改模型 */
 const save = async (bpmnXml: string) => {
-  const data = {
-    ...model.value,
-    bpmnXml: bpmnXml // bpmnXml 只是初始化流程图,后续修改无法通过它获得
-  } as unknown as ModelApi.ModelVO
-  // 提交
-  if (data.id) {
-    await ModelApi.updateModelBpmn(data)
-    message.success('修改成功')
-  } else {
-    await ModelApi.updateModelBpmn(data)
-    message.success('新增成功')
+  try {
+    xmlString.value = bpmnXml
+    if (props.modelId) {
+      // 编辑模式
+      const data = {
+        ...model.value,
+        bpmnXml: bpmnXml
+      } as unknown as ModelApi.ModelVO
+      await ModelApi.updateModelBpmn(data)
+      emit('success')
+    } else {
+      // 新建模式,直接返回XML
+      emit('success', bpmnXml)
+    }
+  } catch (error) {
+    console.error('保存失败:', error)
+    message.error('保存失败')
   }
-  // 跳转回去
-  close()
 }
 
-/** 关闭按钮 */
-const close = () => {
-  router.push({ path: '/bpm/manager/model' })
+// 监听 key、name 和 value 的变化
+watch(
+  [() => props.modelKey, () => props.modelName, () => props.value],
+  async ([newKey, newName, newValue]) => {
+    if (!props.modelId && isModelerReady.value) {
+      let shouldRefresh = false
+
+      if (newKey && newName) {
+        const newXml = newValue || getDefaultBpmnXml(newKey, newName)
+        if (newXml !== xmlString.value) {
+          xmlString.value = newXml
+          shouldRefresh = true
+        }
+        model.value = {
+          ...model.value,
+          key: newKey,
+          name: newName
+        } as ModelApi.ModelVO
+      } else if (newValue && newValue !== xmlString.value) {
+        xmlString.value = newValue
+        shouldRefresh = true
+      }
+
+      if (shouldRefresh) {
+        // 确保更新后重新渲染
+        await nextTick()
+        if (processDesigner.value?.refresh) {
+          try {
+            await modeler.value?.importXML(xmlString.value)
+            processDesigner.value.refresh()
+          } catch (error) {
+            console.error('导入XML失败:', error)
+          }
+        }
+      }
+    }
+  },
+  { deep: true }
+)
+
+// 在组件卸载时清理
+onBeforeUnmount(() => {
+  isModelerReady.value = false
+  modeler.value = null
+  // 清理全局实例
+  const w = window as any
+  if (w.bpmnInstances) {
+    w.bpmnInstances = null
+  }
+})
+
+/** 获取 XML 字符串 */
+const saveXML = async () => {
+  if (!modeler.value) {
+    return { xml: xmlString.value }
+  }
+  try {
+    const result = await modeler.value.saveXML({ format: true })
+    xmlString.value = result.xml
+    return result
+  } catch (error) {
+    console.error('获取XML失败:', error)
+    return { xml: xmlString.value }
+  }
 }
 
-/** 初始化 */
-onMounted(async () => {
-  const modelId = query.modelId as unknown as number
-  if (!modelId) {
-    message.error('缺少模型 modelId 编号')
-    return
+/** 获取SVG字符串 */
+const saveSVG = async () => {
+  if (!modeler.value) {
+    return { svg: undefined }
   }
-  // 查询模型
-  const data = await ModelApi.getModel(modelId)
-  if (!data.bpmnXml) {
-    // 首次创建的 Model 模型,它是没有 bpmnXml,此时需要给它一个默认的
-    data.bpmnXml = ` <?xml version="1.0" encoding="UTF-8"?>
-<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.activiti.org/processdef">
-  <process id="${data.key}" name="${data.name}" isExecutable="true" />
-  <bpmndi:BPMNDiagram id="BPMNDiagram">
-    <bpmndi:BPMNPlane id="${data.key}_di" bpmnElement="${data.key}" />
-  </bpmndi:BPMNDiagram>
-</definitions>`
+  try {
+    return await modeler.value.saveSVG()
+  } catch (error) {
+    console.error('获取SVG失败:', error)
+    return { svg: undefined }
   }
-  model.value = {
-    ...data,
-    bpmnXml: undefined // 清空 bpmnXml 属性
+}
+
+/** 刷新视图 */
+const refresh = async () => {
+  if (processDesigner.value?.refresh && modeler.value) {
+    try {
+      await modeler.value.importXML(xmlString.value)
+      processDesigner.value.refresh()
+    } catch (error) {
+      console.error('刷新视图失败:', error)
+    }
   }
-  xmlString.value = data.bpmnXml
+}
+
+// 暴露必要的属性和方法给父组件
+defineExpose({
+  modeler,
+  isModelerReady,
+  saveXML,
+  saveSVG,
+  refresh
 })
 </script>
 <style lang="scss">
 .process-panel__container {
   position: absolute;
-  top: 90px;
-  right: 60px;
+  top: 172px;
+  right: 70px;
 }
 </style>

+ 301 - 0
src/views/bpm/model/form/BasicInfo.vue

@@ -0,0 +1,301 @@
+<template>
+  <el-form ref="formRef" :model="modelData" :rules="rules" label-width="120px" class="mt-20px">
+    <el-form-item label="流程标识" prop="key" class="mb-20px">
+      <div class="flex items-center">
+        <el-input
+          class="!w-440px"
+          v-model="modelData.key"
+          :disabled="!!modelData.id"
+          placeholder="请输入流标标识"
+        />
+        <el-tooltip
+          class="item"
+          :content="modelData.id ? '流程标识不可修改!' : '新建后,流程标识不可修改!'"
+          effect="light"
+          placement="top"
+        >
+          <Icon icon="ep:question-filled" class="ml-5px" />
+        </el-tooltip>
+      </div>
+    </el-form-item>
+    <el-form-item label="流程名称" prop="name" class="mb-20px">
+      <el-input
+        v-model="modelData.name"
+        :disabled="!!modelData.id"
+        clearable
+        placeholder="请输入流程名称"
+      />
+    </el-form-item>
+    <el-form-item label="流程分类" prop="category" class="mb-20px">
+      <el-select
+        class="!w-full"
+        v-model="modelData.category"
+        clearable
+        placeholder="请选择流程分类"
+      >
+        <el-option
+          v-for="category in categoryList"
+          :key="category.code"
+          :label="category.name"
+          :value="category.code"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item label="流程图标" prop="icon" class="mb-20px">
+      <UploadImg v-model="modelData.icon" :limit="1" height="64px" width="64px" />
+    </el-form-item>
+    <el-form-item label="流程描述" prop="description" class="mb-20px">
+      <el-input v-model="modelData.description" clearable type="textarea" />
+    </el-form-item>
+    <el-form-item label="流程类型" prop="type" class="mb-20px">
+      <el-radio-group v-model="modelData.type">
+        <el-radio
+          v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_TYPE)"
+          :key="dict.value"
+          :value="dict.value"
+        >
+          {{ dict.label }}
+        </el-radio>
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item label="是否可见" prop="visible" class="mb-20px">
+      <el-radio-group v-model="modelData.visible">
+        <el-radio
+          v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+          :key="dict.value"
+          :value="dict.value"
+        >
+          {{ dict.label }}
+        </el-radio>
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item label="谁可以发起" prop="startUserType" class="mb-20px">
+      <el-select
+        v-model="modelData.startUserType"
+        placeholder="请选择谁可以发起"
+        @change="handleStartUserTypeChange"
+      >
+        <el-option label="全员" :value="0" />
+        <el-option label="指定人员" :value="1" />
+        <el-option label="均不可提交" :value="2" />
+      </el-select>
+      <div v-if="modelData.startUserType === 1" class="mt-2 flex flex-wrap gap-2">
+        <div
+          v-for="user in selectedStartUsers"
+          :key="user.id"
+          class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+        >
+          <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
+          <el-avatar class="!m-5px" :size="28" v-else>
+            {{ user.nickname.substring(0, 1) }}
+          </el-avatar>
+          {{ user.nickname }}
+          <Icon
+            icon="ep:close"
+            class="ml-2 cursor-pointer hover:text-red-500"
+            @click="handleRemoveStartUser(user)"
+          />
+        </div>
+        <el-button type="primary" link @click="openStartUserSelect">
+          <Icon icon="ep:plus" />选择人员
+        </el-button>
+      </div>
+    </el-form-item>
+    <el-form-item label="流程管理员" prop="managerUserType" class="mb-20px">
+      <el-select
+        v-model="modelData.managerUserType"
+        placeholder="请选择流程管理员"
+        @change="handleManagerUserTypeChange"
+      >
+        <el-option label="全员" :value="0" />
+        <el-option label="指定人员" :value="1" />
+        <el-option label="均不可提交" :value="2" />
+      </el-select>
+      <div v-if="modelData.managerUserType === 1" class="mt-2 flex flex-wrap gap-2">
+        <div
+          v-for="user in selectedManagerUsers"
+          :key="user.id"
+          class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+        >
+          <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
+          <el-avatar class="!m-5px" :size="28" v-else>
+            {{ user.nickname.substring(0, 1) }}
+          </el-avatar>
+          {{ user.nickname }}
+          <Icon
+            icon="ep:close"
+            class="ml-2 cursor-pointer hover:text-red-500"
+            @click="handleRemoveManagerUser(user)"
+          />
+        </div>
+        <el-button type="primary" link @click="openManagerUserSelect">
+          <Icon icon="ep:plus" />选择人员
+        </el-button>
+      </div>
+    </el-form-item>
+  </el-form>
+
+  <!-- 用户选择弹窗 -->
+  <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
+import { UserVO } from '@/api/system/user'
+
+const props = defineProps({
+  modelValue: {
+    type: Object,
+    required: true
+  },
+  categoryList: {
+    type: Array,
+    required: true
+  },
+  userList: {
+    type: Array,
+    required: true
+  }
+})
+
+const emit = defineEmits(['update:modelValue'])
+
+const formRef = ref()
+const selectedStartUsers = ref<UserVO[]>([])
+const selectedManagerUsers = ref<UserVO[]>([])
+const userSelectFormRef = ref()
+const currentSelectType = ref<'start' | 'manager'>('start')
+
+const rules = {
+  name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }],
+  key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
+  category: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }],
+  icon: [{ required: true, message: '流程图标不能为空', trigger: 'blur' }],
+  type: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
+  visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
+  managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }]
+}
+
+// 创建本地数据副本
+const modelData = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+})
+
+// 初始化选中的用户
+watch(
+  () => props.modelValue,
+  (newVal) => {
+    if (newVal.startUserIds?.length) {
+      selectedStartUsers.value = props.userList.filter((user: UserVO) =>
+        newVal.startUserIds.includes(user.id)
+      ) as UserVO[]
+    }
+    if (newVal.managerUserIds?.length) {
+      selectedManagerUsers.value = props.userList.filter((user: UserVO) =>
+        newVal.managerUserIds.includes(user.id)
+      ) as UserVO[]
+    }
+  },
+  { immediate: true }
+)
+
+/** 打开发起人选择 */
+const openStartUserSelect = () => {
+  currentSelectType.value = 'start'
+  userSelectFormRef.value.open(0, selectedStartUsers.value)
+}
+
+/** 打开管理员选择 */
+const openManagerUserSelect = () => {
+  currentSelectType.value = 'manager'
+  userSelectFormRef.value.open(0, selectedManagerUsers.value)
+}
+
+/** 处理用户选择确认 */
+const handleUserSelectConfirm = (_, users: UserVO[]) => {
+  if (currentSelectType.value === 'start') {
+    selectedStartUsers.value = users
+    emit('update:modelValue', {
+      ...modelData.value,
+      startUserIds: users.map((u) => u.id)
+    })
+  } else {
+    selectedManagerUsers.value = users
+    emit('update:modelValue', {
+      ...modelData.value,
+      managerUserIds: users.map((u) => u.id)
+    })
+  }
+}
+
+/** 处理发起人类型变化 */
+const handleStartUserTypeChange = (value: number) => {
+  if (value !== 1) {
+    selectedStartUsers.value = []
+    emit('update:modelValue', {
+      ...modelData.value,
+      startUserIds: []
+    })
+  }
+}
+
+/** 处理管理员类型变化 */
+const handleManagerUserTypeChange = (value: number) => {
+  if (value !== 1) {
+    selectedManagerUsers.value = []
+    emit('update:modelValue', {
+      ...modelData.value,
+      managerUserIds: []
+    })
+  }
+}
+
+/** 移除发起人 */
+const handleRemoveStartUser = (user: UserVO) => {
+  selectedStartUsers.value = selectedStartUsers.value.filter((u) => u.id !== user.id)
+  emit('update:modelValue', {
+    ...modelData.value,
+    startUserIds: modelData.value.startUserIds.filter((id: number) => id !== user.id)
+  })
+}
+
+/** 移除管理员 */
+const handleRemoveManagerUser = (user: UserVO) => {
+  selectedManagerUsers.value = selectedManagerUsers.value.filter((u) => u.id !== user.id)
+  emit('update:modelValue', {
+    ...modelData.value,
+    managerUserIds: modelData.value.managerUserIds.filter((id: number) => id !== user.id)
+  })
+}
+
+/** 表单校验 */
+const validate = async () => {
+  await formRef.value?.validate()
+}
+
+defineExpose({
+  validate
+})
+</script>
+
+<style lang="scss" scoped>
+.bg-gray-100 {
+  background-color: #f5f7fa;
+  transition: all 0.3s;
+
+  &:hover {
+    background-color: #e6e8eb;
+  }
+
+  .ep-close {
+    font-size: 14px;
+    color: #909399;
+    transition: color 0.3s;
+
+    &:hover {
+      color: #f56c6c;
+    }
+  }
+}
+</style>

+ 137 - 0
src/views/bpm/model/form/FormDesign.vue

@@ -0,0 +1,137 @@
+<template>
+  <el-form ref="formRef" :model="modelData" :rules="rules" label-width="120px" class="mt-20px">
+    <el-form-item label="表单类型" prop="formType" class="mb-20px">
+      <el-radio-group v-model="modelData.formType">
+        <el-radio
+          v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)"
+          :key="dict.value"
+          :value="dict.value"
+        >
+          {{ dict.label }}
+        </el-radio>
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item v-if="modelData.formType === 10" label="流程表单" prop="formId">
+      <el-select v-model="modelData.formId" clearable style="width: 100%">
+        <el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" />
+      </el-select>
+    </el-form-item>
+    <el-form-item v-if="modelData.formType === 20" label="表单提交路由" prop="formCustomCreatePath">
+      <el-input
+        v-model="modelData.formCustomCreatePath"
+        placeholder="请输入表单提交路由"
+        style="width: 330px"
+      />
+      <el-tooltip
+        class="item"
+        content="自定义表单的提交路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/create.vue"
+        effect="light"
+        placement="top"
+      >
+        <Icon icon="ep:question" class="ml-5px" />
+      </el-tooltip>
+    </el-form-item>
+    <el-form-item v-if="modelData.formType === 20" label="表单查看地址" prop="formCustomViewPath">
+      <el-input
+        v-model="modelData.formCustomViewPath"
+        placeholder="请输入表单查看的组件地址"
+        style="width: 330px"
+      />
+      <el-tooltip
+        class="item"
+        content="自定义表单的查看组件地址,使用 Vue 的组件地址,例如说:bpm/oa/leave/detail.vue"
+        effect="light"
+        placement="top"
+      >
+        <Icon icon="ep:question" class="ml-5px" />
+      </el-tooltip>
+    </el-form-item>
+    <!-- 表单预览 -->
+    <div
+      v-if="modelData.formType === 10 && modelData.formId && formPreview.rule.length > 0"
+      class="mt-20px"
+    >
+      <div class="flex items-center mb-15px">
+        <div class="h-15px w-4px bg-[#1890ff] mr-10px"></div>
+        <span class="text-15px font-bold">表单预览</span>
+      </div>
+      <form-create
+        v-model="formPreview.formData"
+        :rule="formPreview.rule"
+        :option="formPreview.option"
+      />
+    </div>
+  </el-form>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as FormApi from '@/api/bpm/form'
+import { setConfAndFields2 } from '@/utils/formCreate'
+
+const props = defineProps({
+  modelValue: {
+    type: Object,
+    required: true
+  },
+  formList: {
+    type: Array,
+    required: true
+  }
+})
+
+const emit = defineEmits(['update:modelValue'])
+
+const formRef = ref()
+
+// 创建本地数据副本
+const modelData = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+})
+
+// 表单预览数据
+const formPreview = ref({
+  formData: {},
+  rule: [],
+  option: {
+    submitBtn: false,
+    resetBtn: false,
+    formData: {}
+  }
+})
+
+// 监听表单ID变化,加载表单数据
+watch(
+  () => modelData.value.formId,
+  async (newFormId) => {
+    if (newFormId && modelData.value.formType === 10) {
+      const data = await FormApi.getForm(newFormId)
+      setConfAndFields2(formPreview.value, data.conf, data.fields)
+      // 设置只读
+      formPreview.value.rule.forEach((item: any) => {
+        item.props = { ...item.props, disabled: true }
+      })
+    } else {
+      formPreview.value.rule = []
+    }
+  },
+  { immediate: true }
+)
+
+const rules = {
+  formType: [{ required: true, message: '表单类型不能为空', trigger: 'blur' }],
+  formId: [{ required: true, message: '流程表单不能为空', trigger: 'blur' }],
+  formCustomCreatePath: [{ required: true, message: '表单提交路由不能为空', trigger: 'blur' }],
+  formCustomViewPath: [{ required: true, message: '表单查看地址不能为空', trigger: 'blur' }]
+}
+
+/** 表单校验 */
+const validate = async () => {
+  await formRef.value?.validate()
+}
+
+defineExpose({
+  validate
+})
+</script>

+ 235 - 0
src/views/bpm/model/form/ProcessDesign.vue

@@ -0,0 +1,235 @@
+<template>
+  <!-- BPMN设计器 -->
+  <template v-if="modelData.type === BpmModelType.BPMN">
+    <BpmModelEditor
+      v-if="showDesigner"
+      :model-id="modelData.id"
+      :model-key="modelData.key"
+      :model-name="modelData.name"
+      :value="currentBpmnXml"
+      ref="bpmnEditorRef"
+      @success="handleDesignSuccess"
+      @init-finished="handleEditorInit"
+    />
+  </template>
+
+  <!-- Simple设计器 -->
+  <template v-else>
+    <SimpleModelDesign
+      v-if="showDesigner"
+      :model-id="modelData.id"
+      :model-key="modelData.key"
+      :model-name="modelData.name"
+      :start-user-ids="modelData.startUserIds"
+      :value="currentSimpleModel"
+      ref="simpleEditorRef"
+      @success="handleDesignSuccess"
+      @init-finished="handleEditorInit"
+    />
+  </template>
+</template>
+
+<script lang="ts" setup>
+import { BpmModelType } from '@/utils/constants'
+import BpmModelEditor from '../editor/index.vue'
+import SimpleModelDesign from '../../simple/SimpleModelDesign.vue'
+
+const props = defineProps({
+  modelValue: {
+    type: Object,
+    required: true
+  }
+})
+
+const emit = defineEmits(['update:modelValue', 'success'])
+
+const bpmnEditorRef = ref()
+const simpleEditorRef = ref()
+const isEditorInitialized = ref(false)
+
+// 创建本地数据副本
+const modelData = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+})
+
+// 保存当前的流程XML或数据
+const currentBpmnXml = ref('')
+const currentSimpleModel = ref('')
+
+// 初始化或更新当前的XML数据
+const initOrUpdateXmlData = () => {
+  if (modelData.value) {
+    if (modelData.value.type === BpmModelType.BPMN) {
+      currentBpmnXml.value = modelData.value.bpmnXml || ''
+    } else {
+      currentSimpleModel.value = modelData.value.simpleModel || ''
+    }
+  }
+}
+
+// 监听modelValue的变化,更新数据
+watch(
+  () => props.modelValue,
+  (newVal) => {
+    if (newVal) {
+      if (newVal.type === BpmModelType.BPMN) {
+        if (newVal.bpmnXml && newVal.bpmnXml !== currentBpmnXml.value) {
+          currentBpmnXml.value = newVal.bpmnXml
+          // 如果编辑器已经初始化,刷新视图
+          if (isEditorInitialized.value && bpmnEditorRef.value?.refresh) {
+            nextTick(() => {
+              bpmnEditorRef.value.refresh()
+            })
+          }
+        }
+      } else {
+        if (newVal.simpleModel && newVal.simpleModel !== currentSimpleModel.value) {
+          currentSimpleModel.value = newVal.simpleModel
+          // 如果编辑器已经初始化,刷新视图
+          if (isEditorInitialized.value && simpleEditorRef.value?.refresh) {
+            nextTick(() => {
+              simpleEditorRef.value.refresh()
+            })
+          }
+        }
+      }
+    }
+  },
+  { immediate: true, deep: true }
+)
+
+/** 编辑器初始化完成的回调 */
+const handleEditorInit = async () => {
+  isEditorInitialized.value = true
+
+  // 等待下一个tick,确保编辑器已经准备好
+  await nextTick()
+
+  // 初始化完成后,设置初始值
+  if (modelData.value.type === BpmModelType.BPMN) {
+    if (modelData.value.bpmnXml) {
+      currentBpmnXml.value = modelData.value.bpmnXml
+      if (bpmnEditorRef.value?.refresh) {
+        await nextTick()
+        bpmnEditorRef.value.refresh()
+      }
+    }
+  } else {
+    if (modelData.value.simpleModel) {
+      currentSimpleModel.value = modelData.value.simpleModel
+      if (simpleEditorRef.value?.refresh) {
+        await nextTick()
+        simpleEditorRef.value.refresh()
+      }
+    }
+  }
+}
+
+/** 获取当前流程数据 */
+const getProcessData = async () => {
+  try {
+    if (modelData.value.type === BpmModelType.BPMN) {
+      if (!bpmnEditorRef.value || !isEditorInitialized.value) {
+        return currentBpmnXml.value || undefined
+      }
+      const { xml } = await bpmnEditorRef.value.saveXML()
+      if (xml) {
+        currentBpmnXml.value = xml
+        return xml
+      }
+    } else {
+      if (!simpleEditorRef.value || !isEditorInitialized.value) {
+        return currentSimpleModel.value || undefined
+      }
+      const flowData = await simpleEditorRef.value.getCurrentFlowData()
+      if (flowData) {
+        currentSimpleModel.value = flowData
+        return flowData
+      }
+    }
+    return modelData.value.type === BpmModelType.BPMN
+      ? currentBpmnXml.value
+      : currentSimpleModel.value
+  } catch (error) {
+    console.error('获取流程数据失败:', error)
+    return modelData.value.type === BpmModelType.BPMN
+      ? currentBpmnXml.value
+      : currentSimpleModel.value
+  }
+}
+
+/** 表单校验 */
+const validate = async () => {
+  try {
+    // 获取最新的流程数据
+    const processData = await getProcessData()
+    if (!processData) {
+      throw new Error('请设计流程')
+    }
+    return true
+  } catch (error) {
+    throw error
+  }
+}
+
+/** 处理设计器保存成功 */
+const handleDesignSuccess = async (data?: any) => {
+  if (data) {
+    if (modelData.value.type === BpmModelType.BPMN) {
+      currentBpmnXml.value = data
+    } else {
+      currentSimpleModel.value = data
+    }
+
+    // 创建新的对象以触发响应式更新
+    const newModelData = {
+      ...modelData.value,
+      bpmnXml: modelData.value.type === BpmModelType.BPMN ? data : null,
+      simpleModel: modelData.value.type === BpmModelType.BPMN ? null : data
+    }
+
+    // 使用emit更新父组件的数据
+    await nextTick()
+    emit('update:modelValue', newModelData)
+    emit('success', data)
+  }
+}
+
+/** 是否显示设计器 */
+const showDesigner = computed(() => {
+  return Boolean(modelData.value?.key && modelData.value?.name)
+})
+
+// 组件创建时初始化数据
+onMounted(() => {
+  initOrUpdateXmlData()
+})
+
+// 组件卸载前保存数据
+onBeforeUnmount(async () => {
+  try {
+    // 获取并保存最新的流程数据
+    const data = await getProcessData()
+    if (data) {
+      // 创建新的对象以触发响应式更新
+      const newModelData = {
+        ...modelData.value,
+        bpmnXml: modelData.value.type === BpmModelType.BPMN ? data : null,
+        simpleModel: modelData.value.type === BpmModelType.BPMN ? null : data
+      }
+
+      // 使用emit更新父组件的数据
+      await nextTick()
+      emit('update:modelValue', newModelData)
+    }
+  } catch (error) {
+    console.error('保存数据失败:', error)
+  }
+})
+
+defineExpose({
+  validate,
+  getProcessData
+})
+</script>

+ 439 - 0
src/views/bpm/model/form/index.vue

@@ -0,0 +1,439 @@
+<template>
+  <ContentWrap>
+    <div class="mx-auto">
+      <!-- 头部导航栏 -->
+      <div
+        class="absolute top-0 left-0 right-0 h-50px bg-white border-bottom z-10 flex items-center px-20px"
+      >
+        <!-- 左侧标题 -->
+        <div class="w-200px flex items-center overflow-hidden">
+          <Icon icon="ep:arrow-left" class="cursor-pointer flex-shrink-0" @click="handleBack" />
+          <span class="ml-10px text-16px truncate" :title="formData.name || '创建流程'">
+            {{ formData.name || '创建流程' }}
+          </span>
+        </div>
+
+        <!-- 步骤条 -->
+        <div class="flex-1 flex items-center justify-center h-full">
+          <div class="w-400px flex items-center justify-between h-full">
+            <div
+              v-for="(step, index) in steps"
+              :key="index"
+              class="flex items-center cursor-pointer mx-15px relative h-full"
+              :class="[
+                currentStep === index
+                  ? 'text-[#3473ff] border-[#3473ff] border-b-2 border-b-solid'
+                  : 'text-gray-500'
+              ]"
+              @click="handleStepClick(index)"
+            >
+              <div
+                class="w-28px h-28px rounded-full flex items-center justify-center mr-8px border-2 border-solid text-15px"
+                :class="[
+                  currentStep === index
+                    ? 'bg-[#3473ff] text-white border-[#3473ff]'
+                    : 'border-gray-300 bg-white text-gray-500'
+                ]"
+              >
+                {{ index + 1 }}
+              </div>
+              <span class="text-16px font-bold whitespace-nowrap">{{ step.title }}</span>
+            </div>
+          </div>
+        </div>
+
+        <!-- 右侧按钮 -->
+        <div class="w-200px flex items-center justify-end gap-2">
+          <el-button v-if="route.params.id" type="success" @click="handleDeploy">发 布</el-button>
+          <el-button type="primary" @click="handleSave">保 存</el-button>
+        </div>
+      </div>
+
+      <!-- 主体内容 -->
+      <div class="mt-50px">
+        <!-- 第一步:基本信息 -->
+        <div v-if="currentStep === 0" class="mx-auto w-560px">
+          <BasicInfo
+            v-model="formData"
+            :categoryList="categoryList"
+            :userList="userList"
+            ref="basicInfoRef"
+          />
+        </div>
+
+        <!-- 第二步:表单设计 -->
+        <div v-if="currentStep === 1" class="mx-auto w-560px">
+          <FormDesign v-model="formData" :formList="formList" ref="formDesignRef" />
+        </div>
+
+        <!-- 第三步:流程设计 -->
+        <ProcessDesign
+          v-if="currentStep === 2"
+          v-model="formData"
+          ref="processDesignRef"
+          @success="handleDesignSuccess"
+        />
+      </div>
+    </div>
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { useRoute, useRouter } from 'vue-router'
+import { useMessage } from '@/hooks/web/useMessage'
+import * as ModelApi from '@/api/bpm/model'
+import * as FormApi from '@/api/bpm/form'
+import { CategoryApi } from '@/api/bpm/category'
+import * as UserApi from '@/api/system/user'
+import { useUserStoreWithOut } from '@/store/modules/user'
+import { BpmModelFormType, BpmModelType } from '@/utils/constants'
+import BasicInfo from './BasicInfo.vue'
+import FormDesign from './FormDesign.vue'
+import ProcessDesign from './ProcessDesign.vue'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+
+const router = useRouter()
+const { delView } = useTagsViewStore() // 视图操作
+const route = useRoute()
+const message = useMessage()
+const userStore = useUserStoreWithOut()
+
+// 组件引用
+const basicInfoRef = ref()
+const formDesignRef = ref()
+const processDesignRef = ref()
+
+/** 步骤校验函数 */
+const validateBasic = async () => {
+  await basicInfoRef.value?.validate()
+}
+
+/** 表单设计校验 */
+const validateForm = async () => {
+  await formDesignRef.value?.validate()
+}
+
+/** 流程设计校验 */
+const validateProcess = async () => {
+  await processDesignRef.value?.validate()
+}
+
+const currentStep = ref(0) // 步骤控制
+const steps = [
+  { title: '基本信息', validator: validateBasic },
+  { title: '表单设计', validator: validateForm },
+  { title: '流程设计', validator: validateProcess }
+]
+
+// 表单数据
+const formData: any = ref({
+  id: undefined,
+  name: '',
+  key: '',
+  category: undefined,
+  icon: undefined,
+  description: '',
+  type: BpmModelType.BPMN,
+  formType: BpmModelFormType.NORMAL,
+  formId: '',
+  formCustomCreatePath: '',
+  formCustomViewPath: '',
+  visible: true,
+  startUserType: undefined,
+  managerUserType: undefined,
+  startUserIds: [],
+  managerUserIds: []
+})
+
+// 数据列表
+const formList = ref([])
+const categoryList = ref([])
+const userList = ref<UserApi.UserVO[]>([])
+
+/** 初始化数据 */
+const initData = async () => {
+  const modelId = route.params.id as string
+  if (modelId) {
+    // 修改场景
+    formData.value = await ModelApi.getModel(modelId)
+  } else {
+    // 新增场景
+    formData.value.managerUserIds.push(userStore.getUser.id)
+  }
+
+  // 获取表单列表
+  formList.value = await FormApi.getFormSimpleList()
+  // 获取分类列表
+  categoryList.value = await CategoryApi.getCategorySimpleList()
+  // 获取用户列表
+  userList.value = await UserApi.getSimpleUserList()
+}
+
+/** 校验所有步骤数据是否完整 */
+const validateAllSteps = async () => {
+  try {
+    // 基本信息校验
+    await basicInfoRef.value?.validate()
+    if (!formData.value.key || !formData.value.name || !formData.value.category) {
+      currentStep.value = 0
+      throw new Error('请完善基本信息')
+    }
+
+    // 表单设计校验
+    await formDesignRef.value?.validate()
+    if (formData.value.formType === 10 && !formData.value.formId) {
+      currentStep.value = 1
+      throw new Error('请选择流程表单')
+    }
+    if (
+      formData.value.formType === 20 &&
+      (!formData.value.formCustomCreatePath || !formData.value.formCustomViewPath)
+    ) {
+      currentStep.value = 1
+      throw new Error('请完善自定义表单信息')
+    }
+
+    // 流程设计校验
+    // 如果已经有流程数据,则不需要重新校验
+    if (!formData.value.bpmnXml && !formData.value.simpleModel) {
+      // 如果当前不在第三步,需要先保存当前步骤数据
+      if (currentStep.value !== 2) {
+        await steps[currentStep.value].validator()
+        // 切换到第三步
+        currentStep.value = 2
+        // 等待组件渲染完成
+        await nextTick()
+      }
+
+      // 校验流程设计
+      await processDesignRef.value?.validate()
+      const processData = await processDesignRef.value?.getProcessData()
+      if (!processData) {
+        throw new Error('请设计流程')
+      }
+
+      // 保存流程数据
+      if (formData.value.type === BpmModelType.BPMN) {
+        formData.value.bpmnXml = processData
+        formData.value.simpleModel = null
+      } else {
+        formData.value.bpmnXml = null
+        formData.value.simpleModel = processData
+      }
+    }
+
+    return true
+  } catch (error) {
+    throw error
+  }
+}
+
+/** 保存操作 */
+const handleSave = async () => {
+  try {
+    // 保存前校验所有步骤的数据
+    await validateAllSteps()
+
+    // 更新表单数据
+    const modelData = {
+      ...formData.value
+    }
+
+    // 如果当前在第三步,获取最新的流程设计数据
+    if (currentStep.value === 2) {
+      const processData = await processDesignRef.value?.getProcessData()
+      if (processData) {
+        if (formData.value.type === BpmModelType.BPMN) {
+          modelData.bpmnXml = processData
+          modelData.simpleModel = null
+        } else {
+          modelData.bpmnXml = null
+          modelData.simpleModel = processData
+        }
+      }
+    }
+
+    if (formData.value.id) {
+      // 修改场景
+      await ModelApi.updateModel(modelData)
+      // 询问是否发布流程
+      try {
+        await message.confirm('修改流程成功,是否发布流程?')
+        // 用户点击确认,执行发布
+        await handleDeploy()
+      } catch {
+        // 用户点击取消,停留在当前页面
+      }
+    } else {
+      // 新增场景
+      formData.value.id = await ModelApi.createModel(modelData)
+      message.success('新增成功')
+      try {
+        await message.confirm('创建流程成功,是否继续编辑?')
+        // 用户点击继续编辑,跳转到编辑页面
+        await nextTick()
+        // 先删除当前页签
+        delView(unref(router.currentRoute))
+        // 跳转到编辑页面
+        await router.push({
+          name: 'BpmModelUpdate',
+          params: { id: formData.value.id }
+        })
+      } catch {
+        // 先删除当前页签
+        delView(unref(router.currentRoute))
+        // 用户点击返回列表
+        await router.push({ name: 'BpmModel' })
+      }
+    }
+  } catch (error: any) {
+    console.error('保存失败:', error)
+    message.warning(error.message || '请完善所有步骤的必填信息')
+  }
+}
+
+/** 发布操作 */
+const handleDeploy = async () => {
+  try {
+    // 修改场景下直接发布,新增场景下需要先确认
+    if (!formData.value.id) {
+      await message.confirm('是否确认发布该流程?')
+    }
+
+    // 校验所有步骤
+    await validateAllSteps()
+
+    // 更新表单数据
+    const modelData = {
+      ...formData.value
+    }
+
+    // 如果当前在第三步,获取最新的流程设计数据
+    if (currentStep.value === 2) {
+      const processData = await processDesignRef.value?.getProcessData()
+      if (processData) {
+        if (formData.value.type === BpmModelType.BPMN) {
+          modelData.bpmnXml = processData
+          modelData.simpleModel = null
+        } else {
+          modelData.bpmnXml = null
+          modelData.simpleModel = processData
+        }
+      }
+    }
+
+    // 先保存所有数据
+    if (formData.value.id) {
+      await ModelApi.updateModel(modelData)
+    } else {
+      const result = await ModelApi.createModel(modelData)
+      formData.value.id = result.id
+    }
+
+    // 发布
+    await ModelApi.deployModel(formData.value.id)
+    message.success('发布成功')
+    // 返回列表页
+    await router.push({ name: 'BpmModel' })
+  } catch (error: any) {
+    console.error('发布失败:', error)
+    message.warning(error.message || '发布失败')
+  }
+}
+
+/** 步骤切换处理 */
+const handleStepClick = async (index: number) => {
+  try {
+    // 如果是切换到第三步(流程设计),需要校验key和name
+    if (index === 2) {
+      if (!formData.value.key || !formData.value.name) {
+        message.warning('请先填写流程标识和流程名称')
+        return
+      }
+    }
+
+    // 保存当前步骤的数据
+    if (currentStep.value === 2) {
+      const processData = await processDesignRef.value?.getProcessData()
+      if (processData) {
+        if (formData.value.type === BpmModelType.BPMN) {
+          formData.value.bpmnXml = processData
+          formData.value.simpleModel = null
+        } else {
+          formData.value.bpmnXml = null
+          formData.value.simpleModel = processData
+        }
+      }
+    } else {
+      // 只有在向后切换时才进行校验
+      if (index > currentStep.value) {
+        if (typeof steps[currentStep.value].validator === 'function') {
+          await steps[currentStep.value].validator()
+        }
+      }
+    }
+
+    // 切换步骤
+    currentStep.value = index
+
+    // 如果切换到流程设计步骤,等待组件渲染完成后刷新设计器
+    if (index === 2) {
+      await nextTick()
+      // 等待更长时间确保组件完全初始化
+      await new Promise(resolve => setTimeout(resolve, 200))
+      if (processDesignRef.value?.refresh) {
+        await processDesignRef.value.refresh()
+      }
+    }
+  } catch (error) {
+    console.error('步骤切换失败:', error)
+    message.warning('请先完善当前步骤必填信息')
+  }
+}
+
+/** 处理设计器保存成功 */
+const handleDesignSuccess = (bpmnXml?: string) => {
+  if (bpmnXml) {
+    formData.value.bpmnXml = bpmnXml
+  }
+}
+
+/** 返回列表页 */
+const handleBack = () => {
+  // 先删除当前页签
+  delView(unref(router.currentRoute))
+  // 跳转到列表页
+  router.push({ name: 'BpmModel' })
+}
+
+/** 初始化 */
+onMounted(async () => {
+  await initData()
+})
+
+// 添加组件卸载前的清理代码
+onBeforeUnmount(() => {
+  // 清理所有的引用
+  basicInfoRef.value = null
+  formDesignRef.value = null
+  processDesignRef.value = null
+})
+</script>
+
+<style lang="scss" scoped>
+.border-bottom {
+  border-bottom: 1px solid #dcdfe6;
+}
+
+.text-primary {
+  color: #3473ff;
+}
+
+.bg-primary {
+  background-color: #3473ff;
+}
+
+.border-primary {
+  border-color: #3473ff;
+}
+</style>

+ 9 - 1
src/views/bpm/model/index.vue

@@ -106,6 +106,7 @@ import CategoryDraggableModel from './CategoryDraggableModel.vue'
 
 defineOptions({ name: 'BpmModel' })
 
+const { push } = useRouter()
 const message = useMessage() // 消息弹窗
 const loading = ref(true) // 列表的加载中
 const isCategorySorting = ref(false) // 是否 category 正处于排序状态
@@ -124,7 +125,14 @@ const handleQuery = () => {
 /** 添加/修改操作 */
 const formRef = ref()
 const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, id)
+  if (type === 'create') {
+    push({ name: 'BpmModelCreate' })
+  } else {
+    push({
+      name: 'BpmModelUpdate',
+      params: { id }
+    })
+  }
 }
 
 /** 流程表单的详情按钮操作 */

+ 0 - 404
src/views/bpm/model/index_old.vue

@@ -1,404 +0,0 @@
-<template>
-  <doc-alert title="流程设计器(BPMN)" url="https://doc.iocoder.cn/bpm/model-designer-dingding/" />
-  <doc-alert
-    title="流程设计器(钉钉、飞书)"
-    url="https://doc.iocoder.cn/bpm/model-designer-bpmn/"
-  />
-  <doc-alert title="选择审批人、发起人自选" url="https://doc.iocoder.cn/bpm/assignee/" />
-  <doc-alert title="会签、或签、依次审批" url="https://doc.iocoder.cn/bpm/multi-instance/" />
-
-  <ContentWrap>
-    <!-- 搜索工作栏 -->
-    <el-form
-      class="-mb-15px"
-      :model="queryParams"
-      ref="queryFormRef"
-      :inline="true"
-      label-width="68px"
-    >
-      <el-form-item label="流程标识" prop="key">
-        <el-input
-          v-model="queryParams.key"
-          placeholder="请输入流程标识"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="流程名称" prop="name">
-        <el-input
-          v-model="queryParams.name"
-          placeholder="请输入流程名称"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="流程分类" prop="category">
-        <el-select
-          v-model="queryParams.category"
-          placeholder="请选择流程分类"
-          clearable
-          class="!w-240px"
-        >
-          <el-option
-            v-for="category in categoryList"
-            :key="category.code"
-            :label="category.name"
-            :value="category.code"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item>
-        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
-        <el-button
-          type="primary"
-          plain
-          @click="openForm('create')"
-          v-hasPermi="['bpm:model:create']"
-        >
-          <Icon icon="ep:plus" class="mr-5px" /> 新建
-        </el-button>
-      </el-form-item>
-    </el-form>
-  </ContentWrap>
-
-  <!-- 列表 -->
-  <ContentWrap>
-    <el-table v-loading="loading" :data="list">
-      <el-table-column label="流程名称" align="center" prop="name" min-width="200" />
-      <el-table-column label="流程图标" align="center" prop="icon" min-width="100">
-        <template #default="scope">
-          <el-image :src="scope.row.icon" class="h-32px w-32px" />
-        </template>
-      </el-table-column>
-      <el-table-column label="可见范围" align="center" prop="startUserIds" min-width="100">
-        <template #default="scope">
-          <el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0">
-            全部可见
-          </el-text>
-          <el-text v-else-if="scope.row.startUsers.length == 1">
-            {{ scope.row.startUsers[0].nickname }}
-          </el-text>
-          <el-text v-else>
-            <el-tooltip
-              class="box-item"
-              effect="dark"
-              placement="top"
-              :content="scope.row.startUsers.map((user: any) => user.nickname).join('、')"
-            >
-              {{ scope.row.startUsers[0].nickname }}等 {{ scope.row.startUsers.length }} 人可见
-            </el-tooltip>
-          </el-text>
-        </template>
-      </el-table-column>
-      <el-table-column label="流程分类" align="center" prop="categoryName" min-width="100" />
-      <el-table-column label="表单信息" align="center" prop="formType" min-width="200">
-        <template #default="scope">
-          <el-button
-            v-if="scope.row.formType === 10"
-            type="primary"
-            link
-            @click="handleFormDetail(scope.row)"
-          >
-            <span>{{ scope.row.formName }}</span>
-          </el-button>
-          <el-button
-            v-else-if="scope.row.formType === 20"
-            type="primary"
-            link
-            @click="handleFormDetail(scope.row)"
-          >
-            <span>{{ scope.row.formCustomCreatePath }}</span>
-          </el-button>
-          <label v-else>暂无表单</label>
-        </template>
-      </el-table-column>
-      <el-table-column label="最后发布" align="center" prop="deploymentTime" min-width="250">
-        <template #default="scope">
-          <span v-if="scope.row.processDefinition">
-            {{ formatDate(scope.row.processDefinition.deploymentTime) }}
-          </span>
-          <el-tag v-if="scope.row.processDefinition" class="ml-10px">
-            v{{ scope.row.processDefinition.version }}
-          </el-tag>
-          <el-tag v-else type="warning">未部署</el-tag>
-          <el-tag
-            v-if="scope.row.processDefinition?.suspensionState === 2"
-            type="warning"
-            class="ml-10px"
-          >
-            已停用
-          </el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column label="操作" align="center" width="200" fixed="right">
-        <template #default="scope">
-          <el-button
-            link
-            type="primary"
-            @click="openForm('update', scope.row.id)"
-            v-hasPermi="['bpm:model:update']"
-            :disabled="!isManagerUser(scope.row)"
-          >
-            修改
-          </el-button>
-          <el-button
-            link
-            class="!ml-5px"
-            type="primary"
-            @click="handleDesign(scope.row)"
-            v-hasPermi="['bpm:model:update']"
-            :disabled="!isManagerUser(scope.row)"
-          >
-            设计
-          </el-button>
-          <el-button
-            link
-            class="!ml-5px"
-            type="primary"
-            @click="handleDeploy(scope.row)"
-            v-hasPermi="['bpm:model:deploy']"
-            :disabled="!isManagerUser(scope.row)"
-          >
-            发布
-          </el-button>
-          <el-dropdown
-            class="!align-middle ml-5px"
-            @command="(command) => handleCommand(command, scope.row)"
-            v-hasPermi="['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete']"
-          >
-            <el-button type="primary" link>更多</el-button>
-            <template #dropdown>
-              <el-dropdown-menu>
-                <el-dropdown-item
-                  command="handleDefinitionList"
-                  v-if="checkPermi(['bpm:process-definition:query'])"
-                >
-                  历史
-                </el-dropdown-item>
-                <el-dropdown-item
-                  command="handleChangeState"
-                  v-if="checkPermi(['bpm:model:update']) && scope.row.processDefinition"
-                  :disabled="!isManagerUser(scope.row)"
-                >
-                  {{ scope.row.processDefinition.suspensionState === 1 ? '停用' : '启用' }}
-                </el-dropdown-item>
-                <el-dropdown-item
-                  type="danger"
-                  command="handleDelete"
-                  v-if="checkPermi(['bpm:model:delete'])"
-                  :disabled="!isManagerUser(scope.row)"
-                >
-                  删除
-                </el-dropdown-item>
-              </el-dropdown-menu>
-            </template>
-          </el-dropdown>
-        </template>
-      </el-table-column>
-    </el-table>
-    <!-- 分页 -->
-    <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
-      v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
-  </ContentWrap>
-
-  <!-- 表单弹窗:添加/修改流程 -->
-  <ModelForm ref="formRef" @success="getList" />
-
-  <!-- 弹窗:表单详情 -->
-  <Dialog title="表单详情" v-model="formDetailVisible" width="800">
-    <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
-  </Dialog>
-</template>
-
-<script lang="ts" setup>
-import { formatDate } from '@/utils/formatTime'
-import * as ModelApi from '@/api/bpm/model'
-import * as FormApi from '@/api/bpm/form'
-import ModelForm from './ModelForm.vue'
-import { setConfAndFields2 } from '@/utils/formCreate'
-import { CategoryApi } from '@/api/bpm/category'
-import { BpmModelType } from '@/utils/constants'
-import { checkPermi } from '@/utils/permission'
-import { useUserStoreWithOut } from '@/store/modules/user'
-
-defineOptions({ name: 'BpmModel' })
-
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
-const { push } = useRouter() // 路由
-const userStore = useUserStoreWithOut() // 用户信息缓存
-
-const loading = ref(true) // 列表的加载中
-const total = ref(0) // 列表的总页数
-const list = ref([]) // 列表的数据
-const queryParams = reactive({
-  pageNo: 1,
-  pageSize: 10,
-  key: undefined,
-  name: undefined,
-  category: undefined
-})
-const queryFormRef = ref() // 搜索的表单
-const categoryList = ref([]) // 流程分类列表
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    const data = await ModelApi.getModelList(queryParams)
-    list.value = data.list
-    total.value = data.total
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.pageNo = 1
-  getList()
-}
-
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value.resetFields()
-  handleQuery()
-}
-
-/** '更多'操作按钮 */
-const handleCommand = (command: string, row: any) => {
-  switch (command) {
-    case 'handleDefinitionList':
-      handleDefinitionList(row)
-      break
-    case 'handleDelete':
-      handleDelete(row)
-      break
-    case 'handleChangeState':
-      handleChangeState(row)
-      break
-    default:
-      break
-  }
-}
-
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, id)
-}
-
-/** 删除按钮操作 */
-const handleDelete = async (row: any) => {
-  try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await ModelApi.deleteModel(row.id)
-    message.success(t('common.delSuccess'))
-    // 刷新列表
-    await getList()
-  } catch {}
-}
-
-/** 更新状态操作 */
-const handleChangeState = async (row: any) => {
-  const state = row.processDefinition.suspensionState
-  const newState = state === 1 ? 2 : 1
-  try {
-    // 修改状态的二次确认
-    const id = row.id
-    debugger
-    const statusState = state === 1 ? '停用' : '启用'
-    const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
-    await message.confirm(content)
-    // 发起修改状态
-    await ModelApi.updateModelState(id, newState)
-    message.success(statusState + '成功')
-    // 刷新列表
-    await getList()
-  } catch {}
-}
-
-/** 设计流程 */
-const handleDesign = (row: any) => {
-  if (row.type == BpmModelType.BPMN) {
-    push({
-      name: 'BpmModelEditor',
-      query: {
-        modelId: row.id
-      }
-    })
-  } else {
-    push({
-      name: 'SimpleModelDesign',
-      query: {
-        modelId: row.id
-      }
-    })
-  }
-}
-
-/** 发布流程 */
-const handleDeploy = async (row: any) => {
-  try {
-    // 删除的二次确认
-    await message.confirm('是否部署该流程!!')
-    // 发起部署
-    await ModelApi.deployModel(row.id)
-    message.success(t('部署成功'))
-    // 刷新列表
-    await getList()
-  } catch {}
-}
-
-/** 跳转到指定流程定义列表 */
-const handleDefinitionList = (row) => {
-  push({
-    name: 'BpmProcessDefinition',
-    query: {
-      key: row.key
-    }
-  })
-}
-
-/** 流程表单的详情按钮操作 */
-const formDetailVisible = ref(false)
-const formDetailPreview = ref({
-  rule: [],
-  option: {}
-})
-const handleFormDetail = async (row: any) => {
-  if (row.formType == 10) {
-    // 设置表单
-    const data = await FormApi.getForm(row.formId)
-    setConfAndFields2(formDetailPreview, data.conf, data.fields)
-    // 弹窗打开
-    formDetailVisible.value = true
-  } else {
-    await push({
-      path: row.formCustomCreatePath
-    })
-  }
-}
-
-/** 判断是否可以操作 */
-const isManagerUser = (row: any) => {
-  const userId = userStore.getUser.id
-  return row.managerUserIds && row.managerUserIds.includes(userId)
-}
-
-/** 初始化 **/
-onMounted(async () => {
-  await getList()
-  // 查询流程分类列表
-  categoryList.value = await CategoryApi.getCategorySimpleList()
-})
-</script>

+ 45 - 6
src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue

@@ -9,7 +9,7 @@
         <el-tabs v-model="activeTab">
           <!-- 表单信息 -->
           <el-tab-pane label="表单填写" name="form">
-            <div class="form-scroll-area">
+            <div class="form-scroll-area" v-loading="processInstanceStartLoading">
               <el-scrollbar>
                 <el-row>
                   <el-col :span="17">
@@ -75,7 +75,11 @@
 <script lang="ts" setup>
 import { decodeFields, setConfAndFields2 } from '@/utils/formCreate'
 import { BpmModelType } from '@/utils/constants'
-import { CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts'
+import {
+  CandidateStrategy,
+  NodeId,
+  FieldPermissionType
+} from '@/components/SimpleProcessDesignerV2/src/consts'
 import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
 import ProcessInstanceSimpleViewer from '../detail/ProcessInstanceSimpleViewer.vue'
 import ProcessInstanceTimeline from '../detail/ProcessInstanceTimeline.vue'
@@ -90,7 +94,7 @@ const props = defineProps<{
   selectProcessDefinition: any
 }>()
 const emit = defineEmits(['cancel'])
-
+const processInstanceStartLoading = ref(false) // 流程实例发起中
 const { push, currentRoute } = useRouter() // 路由
 const message = useMessage() // 消息弹窗
 const { delView } = useTagsViewStore() // 视图操作
@@ -129,8 +133,10 @@ const initProcessInfo = async (row: any, formVariables?: any) => {
       }
     }
     setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
+
     await nextTick()
     fApi.value?.btn.show(false) // 隐藏提交按钮
+
     // 获取流程审批信息
     await getApprovalDetail(row)
 
@@ -152,7 +158,12 @@ const initProcessInfo = async (row: any, formVariables?: any) => {
 /** 获取审批详情 */
 const getApprovalDetail = async (row: any) => {
   try {
-    const data = await ProcessInstanceApi.getApprovalDetail({ processDefinitionId: row.id })
+    // TODO 获取审批详情,设置 activityId 为发起人节点(为了获取字段权限。暂时只对 Simple 设计器有效)
+    const data = await ProcessInstanceApi.getApprovalDetail({
+      processDefinitionId: row.id,
+      activityId: NodeId.START_USER_NODE_ID
+    })
+
     if (!data) {
       message.error('查询不到审批详情信息!')
       return
@@ -170,15 +181,43 @@ const getApprovalDetail = async (row: any) => {
 
     // 获取审批节点,显示 Timeline 的数据
     activityNodes.value = data.activityNodes
+    // 获取表单字段权限
+    const formFieldsPermission = data.formFieldsPermission
+    // 设置表单字段权限
+    if (formFieldsPermission) {
+      Object.keys(formFieldsPermission).forEach((item) => {
+        setFieldPermission(item, formFieldsPermission[item])
+      })
+    }
   } finally {
   }
 }
 
+/**
+ * 设置表单权限
+ */
+const setFieldPermission = (field: string, permission: string) => {
+  if (permission === FieldPermissionType.READ) {
+    //@ts-ignore
+    fApi.value?.disabled(true, field)
+  }
+  if (permission === FieldPermissionType.WRITE) {
+    //@ts-ignore
+    fApi.value?.disabled(false, field)
+  }
+  if (permission === FieldPermissionType.NONE) {
+    //@ts-ignore
+    fApi.value?.hidden(true, field)
+  }
+}
+
 /** 提交按钮 */
 const submitForm = async () => {
   if (!fApi.value || !props.selectProcessDefinition) {
     return
   }
+  // 流程表单校验
+  await fApi.value.validate()
   // 如果有指定审批人,需要校验
   if (startUserSelectTasks.value?.length > 0) {
     for (const userTask of startUserSelectTasks.value) {
@@ -191,7 +230,7 @@ const submitForm = async () => {
   }
 
   // 提交请求
-  fApi.value.btn.loading(true)
+  processInstanceStartLoading.value = true
   try {
     await ProcessInstanceApi.createProcessInstance({
       processDefinitionId: props.selectProcessDefinition.id,
@@ -206,7 +245,7 @@ const submitForm = async () => {
       name: 'BpmProcessInstanceMy'
     })
   } finally {
-    fApi.value.btn.loading(false)
+    processInstanceStartLoading.value = false
   }
 }
 

+ 261 - 161
src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue

@@ -20,9 +20,9 @@
         <el-form
           label-position="top"
           class="mb-auto"
-          ref="formRef"
-          :model="genericForm"
-          :rules="genericRule"
+          ref="approveFormRef"
+          :model="approveReasonForm"
+          :rules="approveReasonRule"
           label-width="100px"
         >
           <el-card v-if="runningTask?.formId > 0" class="mb-15px !-mt-10px">
@@ -38,17 +38,17 @@
           </el-card>
           <el-form-item label="审批意见" prop="reason">
             <el-input
-              v-model="genericForm.reason"
+              v-model="approveReasonForm.reason"
               placeholder="请输入审批意见"
               type="textarea"
               :rows="4"
             />
           </el-form-item>
           <el-form-item>
-            <el-button :disabled="formLoading" type="success" @click="handleAudit(true)">
+            <el-button :disabled="formLoading" type="success" @click="handleAudit(true, approveFormRef)">
               {{ getButtonDisplayName(OperationButtonType.APPROVE) }}
             </el-button>
-            <el-button @click="popOverVisible.approve = false"> 取消 </el-button>
+            <el-button @click="closePropover('approve', approveFormRef)"> 取消 </el-button>
           </el-form-item>
         </el-form>
       </div>
@@ -72,35 +72,24 @@
         <el-form
           label-position="top"
           class="mb-auto"
-          ref="formRef"
-          :model="genericForm"
-          :rules="genericRule"
+          ref="rejectFormRef"
+          :model="rejectReasonForm"
+          :rules="rejectReasonRule"
           label-width="100px"
         >
-          <el-card v-if="runningTask?.formId > 0" class="mb-15px !-mt-10px">
-            <template #header>
-              <span class="el-icon-picture-outline"> 填写表单【{{ runningTask?.formName }}】 </span>
-            </template>
-            <form-create
-              v-model="approveForm.value"
-              v-model:api="approveFormFApi"
-              :option="approveForm.option"
-              :rule="approveForm.rule"
-            />
-          </el-card>
           <el-form-item label="审批意见" prop="reason">
             <el-input
-              v-model="genericForm.reason"
+              v-model="rejectReasonForm.reason"
               placeholder="请输入审批意见"
               type="textarea"
               :rows="4"
             />
           </el-form-item>
           <el-form-item>
-            <el-button :disabled="formLoading" type="danger" @click="handleAudit(false)">
+            <el-button :disabled="formLoading" type="danger" @click="handleAudit(false,rejectFormRef)">
               {{ getButtonDisplayName(OperationButtonType.REJECT) }}
             </el-button>
-            <el-button @click="popOverVisible.reject = false"> 取消 </el-button>
+            <el-button @click="closePropover('reject', rejectFormRef)"> 取消 </el-button>
           </el-form-item>
         </el-form>
       </div>
@@ -124,14 +113,14 @@
         <el-form
           label-position="top"
           class="mb-auto"
-          ref="formRef"
-          :model="genericForm"
-          :rules="genericRule"
+          ref="copyFormRef"
+          :model="copyForm"
+          :rules="copyFormRule"
           label-width="100px"
         >
           <el-form-item label="抄送人" prop="copyUserIds">
             <el-select
-              v-model="genericForm.copyUserIds"
+              v-model="copyForm.copyUserIds"
               clearable
               style="width: 100%"
               multiple
@@ -147,7 +136,7 @@
           </el-form-item>
           <el-form-item label="抄送意见" prop="copyReason">
             <el-input
-              v-model="genericForm.copyReason"
+              v-model="copyForm.copyReason"
               clearable
               placeholder="请输入抄送意见"
               type="textarea"
@@ -158,13 +147,13 @@
             <el-button :disabled="formLoading" type="primary" @click="handleCopy">
               {{ getButtonDisplayName(OperationButtonType.COPY) }}
             </el-button>
-            <el-button @click="popOverVisible.copy = false"> 取消 </el-button>
+            <el-button @click="closePropover('copy', copyFormRef)"> 取消 </el-button>
           </el-form-item>
         </el-form>
       </div>
     </el-popover>
 
-    <!-- 【转】按钮 -->
+    <!-- 【转】按钮 -->
     <el-popover
       :visible="popOverVisible.transfer"
       placement="top-start"
@@ -182,13 +171,13 @@
         <el-form
           label-position="top"
           class="mb-auto"
-          ref="formRef"
-          :model="genericForm"
-          :rules="genericRule"
+          ref="transferFormRef"
+          :model="transferForm"
+          :rules="transferFormRule"
           label-width="100px"
         >
           <el-form-item label="新审批人" prop="assigneeUserId">
-            <el-select v-model="genericForm.assigneeUserId" clearable style="width: 100%">
+            <el-select v-model="transferForm.assigneeUserId" clearable style="width: 100%">
               <el-option
                 v-for="item in userOptions"
                 :key="item.id"
@@ -199,7 +188,7 @@
           </el-form-item>
           <el-form-item label="审批意见" prop="reason">
             <el-input
-              v-model="genericForm.reason"
+              v-model="transferForm.reason"
               clearable
               placeholder="请输入审批意见"
               type="textarea"
@@ -210,7 +199,7 @@
             <el-button :disabled="formLoading" type="primary" @click="handleTransfer()">
               {{ getButtonDisplayName(OperationButtonType.TRANSFER) }}
             </el-button>
-            <el-button @click="popOverVisible.transfer = false"> 取消 </el-button>
+            <el-button @click="closePropover('transfer', transferFormRef)"> 取消 </el-button>
           </el-form-item>
         </el-form>
       </div>
@@ -234,13 +223,13 @@
         <el-form
           label-position="top"
           class="mb-auto"
-          ref="formRef"
-          :model="genericForm"
-          :rules="genericRule"
+          ref="delegateFormRef"
+          :model="delegateForm"
+          :rules="delegateFormRule"
           label-width="100px"
         >
           <el-form-item label="接收人" prop="delegateUserId">
-            <el-select v-model="genericForm.delegateUserId" clearable style="width: 100%">
+            <el-select v-model="delegateForm.delegateUserId" clearable style="width: 100%">
               <el-option
                 v-for="item in userOptions"
                 :key="item.id"
@@ -251,7 +240,7 @@
           </el-form-item>
           <el-form-item label="审批意见" prop="reason">
             <el-input
-              v-model="genericForm.reason"
+              v-model="delegateForm.reason"
               clearable
               placeholder="请输入审批意见"
               type="textarea"
@@ -262,7 +251,7 @@
             <el-button :disabled="formLoading" type="primary" @click="handleDelegate()">
               {{ getButtonDisplayName(OperationButtonType.DELEGATE) }}
             </el-button>
-            <el-button @click="popOverVisible.delegate = false"> 取消 </el-button>
+            <el-button @click="closePropover('delegate', delegateFormRef)"> 取消 </el-button>
           </el-form-item>
         </el-form>
       </div>
@@ -286,13 +275,13 @@
         <el-form
           label-position="top"
           class="mb-auto"
-          ref="formRef"
-          :model="genericForm"
-          :rules="genericRule"
+          ref="addSignFormRef"
+          :model="addSignForm"
+          :rules="addSignFormRule"
           label-width="100px"
         >
           <el-form-item label="加签处理人" prop="addSignUserIds">
-            <el-select v-model="genericForm.addSignUserIds" multiple clearable style="width: 100%">
+            <el-select v-model="addSignForm.addSignUserIds" multiple clearable style="width: 100%">
               <el-option
                 v-for="item in userOptions"
                 :key="item.id"
@@ -303,7 +292,7 @@
           </el-form-item>
           <el-form-item label="审批意见" prop="reason">
             <el-input
-              v-model="genericForm.reason"
+              v-model="addSignForm.reason"
               clearable
               placeholder="请输入审批意见"
               type="textarea"
@@ -317,7 +306,7 @@
             <el-button :disabled="formLoading" type="primary" @click="handlerAddSign('after')">
               向后{{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }}
             </el-button>
-            <el-button @click="popOverVisible.addSign = false"> 取消 </el-button>
+            <el-button @click="closePropover('addSign', addSignFormRef)"> 取消 </el-button>
           </el-form-item>
         </el-form>
       </div>
@@ -340,13 +329,13 @@
         <el-form
           label-position="top"
           class="mb-auto"
-          ref="formRef"
-          :model="genericForm"
-          :rules="genericRule"
+          ref="deleteSignFormRef"
+          :model="deleteSignForm"
+          :rules="deleteSignFormRule"
           label-width="100px"
         >
           <el-form-item label="减签人员" prop="deleteSignTaskId">
-            <el-select v-model="genericForm.deleteSignTaskId" clearable style="width: 100%">
+            <el-select v-model="deleteSignForm.deleteSignTaskId" clearable style="width: 100%">
               <el-option
                 v-for="item in runningTask.children"
                 :key="item.id"
@@ -357,7 +346,7 @@
           </el-form-item>
           <el-form-item label="审批意见" prop="reason">
             <el-input
-              v-model="genericForm.reason"
+              v-model="deleteSignForm.reason"
               clearable
               placeholder="请输入审批意见"
               type="textarea"
@@ -368,7 +357,7 @@
             <el-button :disabled="formLoading" type="primary" @click="handlerDeleteSign()">
               减签
             </el-button>
-            <el-button @click="popOverVisible.deleteSign = false"> 取消 </el-button>
+            <el-button @click="closePropover('deleteSign', deleteSignFormRef)"> 取消 </el-button>
           </el-form-item>
         </el-form>
       </div>
@@ -383,7 +372,7 @@
       v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.RETURN)"
     >
       <template #reference>
-        <div @click="openReturnPopover" class="hover-bg-gray-100 rounded-xl p-6px">
+        <div @click="openPopover('return')" class="hover-bg-gray-100 rounded-xl p-6px">
           <Icon :size="14" icon="ep:back" />&nbsp;
           {{ getButtonDisplayName(OperationButtonType.RETURN) }}
         </div>
@@ -392,13 +381,13 @@
         <el-form
           label-position="top"
           class="mb-auto"
-          ref="formRef"
-          :model="genericForm"
-          :rules="genericRule"
+          ref="returnFormRef"
+          :model="returnForm"
+          :rules="returnFormRule"
           label-width="100px"
         >
           <el-form-item label="退回节点" prop="targetTaskDefinitionKey">
-            <el-select v-model="genericForm.targetTaskDefinitionKey" clearable style="width: 100%">
+            <el-select v-model="returnForm.targetTaskDefinitionKey" clearable style="width: 100%">
               <el-option
                 v-for="item in returnList"
                 :key="item.taskDefinitionKey"
@@ -409,7 +398,7 @@
           </el-form-item>
           <el-form-item label="退回理由" prop="returnReason">
             <el-input
-              v-model="genericForm.returnReason"
+              v-model="returnForm.returnReason"
               clearable
               placeholder="请输入退回理由"
               type="textarea"
@@ -420,7 +409,7 @@
             <el-button :disabled="formLoading" type="primary" @click="handleReturn()">
               {{ getButtonDisplayName(OperationButtonType.RETURN) }}
             </el-button>
-            <el-button @click="popOverVisible.return = false"> 取消 </el-button>
+            <el-button @click="closePropover('return', returnFormRef)"> 取消 </el-button>
           </el-form-item>
         </el-form>
       </div>
@@ -445,15 +434,15 @@
         <el-form
           label-position="top"
           class="mb-auto"
-          ref="formRef"
-          :model="genericForm"
-          :rules="genericRule"
+          ref="cancelFormRef"
+          :model="cancelForm"
+          :rules="cancelFormRule"
           label-width="100px"
         >
           <el-form-item label="取消理由" prop="cancelReason">
             <span class="text-#878c93 text-12px">&nbsp; 取消后,该审批流程将自动结束</span>
             <el-input
-              v-model="genericForm.cancelReason"
+              v-model="cancelForm.cancelReason"
               clearable
               placeholder="请输入取消理由"
               type="textarea"
@@ -462,9 +451,9 @@
           </el-form-item>
           <el-form-item>
             <el-button :disabled="formLoading" type="primary" @click="handleCancel()">
-              取消
+              确认
             </el-button>
-            <el-button @click="popOverVisible.cancel = false"> 取消 </el-button>
+            <el-button @click="closePropover('cancel', cancelFormRef)"> 取消 </el-button>
           </el-form-item>
         </el-form>
       </div>
@@ -488,26 +477,29 @@ import { useUserStoreWithOut } from '@/store/modules/user'
 import { setConfAndFields2 } from '@/utils/formCreate'
 import * as TaskApi from '@/api/bpm/task'
 import * as ProcessInstanceApi from '@/api/bpm/processInstance'
-import { propTypes } from '@/utils/propTypes'
+import * as UserApi from '@/api/system/user'
 import {
   OperationButtonType,
   OPERATION_BUTTON_NAME
 } from '@/components/SimpleProcessDesignerV2/src/consts'
-import { BpmProcessInstanceStatus } from '@/utils/constants'
-
+import { BpmProcessInstanceStatus, BpmModelFormType } from '@/utils/constants'
+import type { FormInstance, FormRules } from 'element-plus'
 defineOptions({ name: 'ProcessInstanceBtnContainer' })
 
 const router = useRouter() // 路由
 const message = useMessage() // 消息弹窗
-const { proxy } = getCurrentInstance() as any
 
 const userId = useUserStoreWithOut().getUser.id // 当前登录的编号
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const props = defineProps({
-  processInstance: propTypes.object, // 流程实例信息
-  processDefinition: propTypes.object, // 流程定义信息
-  userOptions: propTypes.any
-})
+
+const props = defineProps< {
+  processInstance: any,  // 流程实例信息
+  processDefinition: any,  // 流程定义信息
+  userOptions: UserApi.UserVO[],
+  normalForm: any, // 流程表单 formCreate
+  normalFormApi: any, // 流程表单 formCreate Api
+  writableFields: string[] // 流程表单可以编辑的字段
+}>()
 
 const formLoading = ref(false) // 表单加载中
 const popOverVisible = ref({
@@ -525,21 +517,99 @@ const returnList = ref([] as any) // 退回节点
 
 // ========== 审批信息 ==========
 const runningTask = ref<any>() // 运行中的任务
-const genericForm = ref<any>({}) // 通用表单
 const approveForm = ref<any>({}) // 审批通过时,额外的补充信息
 const approveFormFApi = ref<any>({}) // approveForms 的 fAPi
-const formRef = ref()
-const genericRule = reactive({
+
+// 审批通过意见表单
+const approveFormRef = ref<FormInstance>()
+const approveReasonForm = reactive({
+  reason: ''
+})
+const approveReasonRule = reactive<FormRules<typeof approveReasonForm>>({
   reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }],
-  returnReason: [{ required: true, message: '退回理由不能为空', trigger: 'blur' }],
-  cancelReason: [{ required: true, message: '取消理由不能为空', trigger: 'blur' }],
-  copyUserIds: [{ required: true, message: '抄送人不能为空', trigger: 'change' }],
+})
+// 拒绝表单
+const rejectFormRef = ref<FormInstance>()
+const rejectReasonForm = reactive({
+  reason: ''
+})
+const rejectReasonRule = reactive<FormRules<typeof rejectReasonForm>>({
+  reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }],
+})
+
+// 抄送表单
+const copyFormRef = ref<FormInstance>()
+const copyForm = reactive({
+  copyUserIds: [],
+  copyReason: ''
+})
+const copyFormRule = reactive<FormRules<typeof copyForm>>({
+  copyUserIds: [{ required: true, message: '抄送人不能为空', trigger: 'change' }]
+})
+
+// 转办表单
+const transferFormRef = ref<FormInstance>()
+const transferForm = reactive({
+  assigneeUserId: undefined,
+  reason: ''
+})
+const transferFormRule = reactive<FormRules<typeof transferForm>>({
   assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }],
+  reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }],
+})
+
+// 委派表单
+const delegateFormRef = ref<FormInstance>()
+const delegateForm = reactive({
+  delegateUserId: undefined,
+  reason: ''
+})
+const delegateFormRule = reactive<FormRules<typeof delegateForm>>({
   delegateUserId: [{ required: true, message: '接收人不能为空', trigger: 'change' }],
+  reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }],
+})
+
+// 加签表单
+const addSignFormRef = ref<FormInstance>()
+const addSignForm = reactive({
+  addSignUserIds: undefined,
+  reason: ''
+})
+const addSignFormRule = reactive<FormRules<typeof addSignForm>>({
   addSignUserIds: [{ required: true, message: '加签处理人不能为空', trigger: 'change' }],
+  reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }],
+})
+
+// 减签表单
+const deleteSignFormRef = ref<FormInstance>()
+const deleteSignForm = reactive({
+  deleteSignTaskId: undefined,
+  reason: ''
+})
+const deleteSignFormRule = reactive<FormRules<typeof deleteSignForm>>({
   deleteSignTaskId: [{ required: true, message: '减签人员不能为空', trigger: 'change' }],
-  targetTaskDefinitionKey: [{ required: true, message: '退回节点不能为空', trigger: 'change' }]
-}) // 表单校验规则
+ reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }],
+})
+
+// 退回表单
+const returnFormRef = ref<FormInstance>()
+const returnForm = reactive({
+  targetTaskDefinitionKey: undefined,
+  returnReason: ''
+})
+const returnFormRule = reactive<FormRules<typeof returnForm>>({
+  targetTaskDefinitionKey: [{ required: true, message: '退回节点不能为空', trigger: 'change' }],
+  returnReason: [{ required: true, message: '退回理由不能为空', trigger: 'blur' }]
+})
+
+// 取消表单
+const cancelFormRef = ref<FormInstance>()
+const cancelForm = reactive({
+  cancelReason: ''
+})
+const cancelFormRule = reactive<FormRules<typeof cancelForm>>({
+  cancelReason: [{ required: true, message: '取消理由不能为空', trigger: 'blur' }],
+})
 
 /** 监听 approveFormFApis,实现它对应的 form-create 初始化后,隐藏掉对应的表单提交按钮 */
 watch(
@@ -553,43 +623,57 @@ watch(
   }
 )
 
-/** 弹出退回气泡卡 */
-const openReturnPopover = async () => {
-  returnList.value = await TaskApi.getTaskListByReturn(runningTask.value.id)
-  if (returnList.value.length === 0) {
-    message.warning('当前没有可退回的节点')
-    return
-  }
-  await openPopover('return')
-}
-
 /** 弹出气泡卡 */
 const openPopover = async (type: string) => {
+  if (type === 'approve') {
+    // 校验流程表单
+     const valid = await validateNormalForm();
+     if (!valid) {
+      message.warning('表单校验不通过,请先完善表单!!')
+      return;
+     }
+  }
+  if (type === 'return') {
+    // 获取退回节点
+    returnList.value = await TaskApi.getTaskListByReturn(runningTask.value.id)
+    if (returnList.value.length === 0) {
+      message.warning('当前没有可退回的节点')
+      return
+    }
+  }
   Object.keys(popOverVisible.value).forEach((item) => {
     popOverVisible.value[item] = item === type
   })
-  await nextTick()
-  formRef.value.resetFields()
+  // await nextTick()
+  // formRef.value.resetFields()
+}
+
+/** 关闭气泡卡 */
+const closePropover = (type: string, formRef: FormInstance | undefined) => {
+  if (formRef) {
+    formRef.resetFields()
+  } 
+  popOverVisible.value[type] = false
 }
 
 /** 处理审批通过和不通过的操作 */
-const handleAudit = async (pass: boolean) => {
+const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) => {
   formLoading.value = true
   try {
-    const genericFormRef = proxy.$refs['formRef']
-    // 1.2 校验表单
-    const elForm = unref(genericFormRef)
-    if (!elForm) return
-    const valid = await elForm.validate()
-    if (!valid) return
-
-    // 2.1 提交审批
-    const data = {
-      id: runningTask.value.id,
-      reason: genericForm.value.reason
-    }
+    // 校验表单
+    if (!formRef) return
+    await formRef.validate()
     if (pass) {
-      // 审批通过,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交
+       // 获取修改的流程变量, 暂时只支持流程表单
+       const variables = getUpdatedProcessInstanceVaiables();
+      // 审批通过数据
+      const data = {
+        id: runningTask.value.id,
+        reason: approveReasonForm.reason,
+        variables // 审批通过, 把修改的字段值赋于流程实例变量
+      }
+      // 多表单处理,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交
+      // TODO 芋艿 任务有多表单这里要如何处理,会和可编辑的字段冲突
       const formCreateApi = approveFormFApi.value
       if (Object.keys(formCreateApi)?.length > 0) {
         await formCreateApi.validate()
@@ -600,11 +684,18 @@ const handleAudit = async (pass: boolean) => {
       popOverVisible.value.approve = false
       message.success('审批通过成功')
     } else {
+       // 审批不通过数据
+       const data = {
+        id: runningTask.value.id,
+        reason: rejectReasonForm.reason,
+      }
       await TaskApi.rejectTask(data)
       popOverVisible.value.reject = false
       message.success('审批不通过成功')
     }
-    // 2.2 加载最新数据
+    // 重置表单
+    formRef.resetFields()
+    // 加载最新数据
     reload()
   } finally {
     formLoading.value = false
@@ -615,19 +706,17 @@ const handleAudit = async (pass: boolean) => {
 const handleCopy = async () => {
   formLoading.value = true
   try {
-    const copyFormRef = proxy.$refs['formRef']
     // 1. 校验表单
-    const elForm = unref(copyFormRef)
-    if (!elForm) return
-    const valid = await elForm.validate()
-    if (!valid) return
+    if (!copyFormRef.value) return
+    await copyFormRef.value.validate()
     // 2. 提交抄送
     const data = {
       id: runningTask.value.id,
-      reason: genericForm.value.copyReason,
-      copyUserIds: genericForm.value.copyUserIds
+      reason: copyForm.copyReason,
+      copyUserIds:copyForm.copyUserIds
     }
     await TaskApi.copyTask(data)
+    copyFormRef.value.resetFields()
     popOverVisible.value.copy = false
     message.success('操作成功')
   } finally {
@@ -639,20 +728,17 @@ const handleCopy = async () => {
 const handleTransfer = async () => {
   formLoading.value = true
   try {
-    const transferFormRef = proxy.$refs['formRef']
     // 1.1 校验表单
-    const elForm = unref(transferFormRef)
-    if (!elForm) return
-    const valid = await elForm.validate()
-    if (!valid) return
+    if (!transferFormRef.value) return
+    await transferFormRef.value.validate()
     // 1.2 提交转交
     const data = {
       id: runningTask.value.id,
-      reason: genericForm.value.reason,
-      assigneeUserId: genericForm.value.assigneeUserId
+      reason: transferForm.reason,
+      assigneeUserId: transferForm.assigneeUserId
     }
-
     await TaskApi.transferTask(data)
+    transferFormRef.value.resetFields()
     popOverVisible.value.transfer = false
     message.success('操作成功')
     // 2. 加载最新数据
@@ -666,21 +752,20 @@ const handleTransfer = async () => {
 const handleDelegate = async () => {
   formLoading.value = true
   try {
-    const deletegateFormRef = proxy.$refs['formRef']
+ 
     // 1.1 校验表单
-    const elForm = unref(deletegateFormRef)
-    if (!elForm) return
-    const valid = await elForm.validate()
-    if (!valid) return
+    if (!delegateFormRef.value) return
+    await delegateFormRef.value.validate()
     // 1.2 处理委派
     const data = {
       id: runningTask.value.id,
-      reason: genericForm.value.reason,
-      delegateUserId: genericForm.value.delegateUserId
+      reason: delegateForm.reason,
+      delegateUserId: delegateForm.delegateUserId
     }
 
     await TaskApi.delegateTask(data)
     popOverVisible.value.delegate = false
+    delegateFormRef.value.resetFields()
     message.success('操作成功')
     // 2. 加载最新数据
     reload()
@@ -693,21 +778,19 @@ const handleDelegate = async () => {
 const handlerAddSign = async (type: string) => {
   formLoading.value = true
   try {
-    const transferFormRef = proxy.$refs['formRef']
     // 1.1 校验表单
-    const elForm = unref(transferFormRef)
-    if (!elForm) return
-    const valid = await elForm.validate()
-    if (!valid) return
+    if (!addSignFormRef.value) return
+    await addSignFormRef.value.validate()
     // 1.2 提交加签
     const data = {
       id: runningTask.value.id,
       type,
-      reason: genericForm.value.reason,
-      userIds: genericForm.value.addSignUserIds
+      reason: addSignForm.reason,
+      userIds: addSignForm.addSignUserIds
     }
     await TaskApi.signCreateTask(data)
     message.success('操作成功')
+    addSignFormRef.value.resetFields()
     popOverVisible.value.addSign = false
     // 2 加载最新数据
     reload()
@@ -720,21 +803,19 @@ const handlerAddSign = async (type: string) => {
 const handleReturn = async () => {
   formLoading.value = true
   try {
-    const returnFormRef = proxy.$refs['formRef']
     // 1.1 校验表单
-    const elForm = unref(returnFormRef)
-    if (!elForm) return
-    const valid = await elForm.validate()
-    if (!valid) return
+    if (!returnFormRef.value) return
+    await returnFormRef.value.validate()
     // 1.2 提交退回
     const data = {
       id: runningTask.value.id,
-      reason: genericForm.value.returnReason,
-      targetTaskDefinitionKey: genericForm.value.targetTaskDefinitionKey
+      reason: returnForm.returnReason,
+      targetTaskDefinitionKey: returnForm.targetTaskDefinitionKey
     }
 
     await TaskApi.returnTask(data)
     popOverVisible.value.return = false
+    returnFormRef.value.resetFields()
     message.success('操作成功')
     // 2 重新加载数据
     reload()
@@ -747,19 +828,17 @@ const handleReturn = async () => {
 const handleCancel = async () => {
   formLoading.value = true
   try {
-    const cancelFormRef = proxy.$refs['formRef']
     // 1.1 校验表单
-    const elForm = unref(cancelFormRef)
-    if (!elForm) return
-    const valid = await elForm.validate()
-    if (!valid) return
+    if (!cancelFormRef.value) return
+    await cancelFormRef.value.validate()
     // 1.2 提交取消
     await ProcessInstanceApi.cancelProcessInstanceByStartUser(
       props.processInstance.id,
-      genericForm.value.cancelReason
+      cancelForm.cancelReason
     )
     popOverVisible.value.return = false
     message.success('操作成功')
+    cancelFormRef.value.resetFields()
     // 2 重新加载数据
     reload()
   } finally {
@@ -786,19 +865,17 @@ const getDeleteSignUserLabel = (task: any): string => {
 const handlerDeleteSign = async () => {
   formLoading.value = true
   try {
-    const deleteFormRef = proxy.$refs['formRef']
     // 1.1 校验表单
-    const elForm = unref(deleteFormRef)
-    if (!elForm) return
-    const valid = await elForm.validate()
-    if (!valid) return
+    if (!deleteSignFormRef.value) return
+    await deleteSignFormRef.value.validate()
     // 1.2 提交减签
     const data = {
-      id: genericForm.value.deleteSignTaskId,
-      reason: genericForm.value.reason
+      id: deleteSignForm.deleteSignTaskId,
+      reason: deleteSignForm.reason
     }
     await TaskApi.signDeleteTask(data)
     message.success('减签成功')
+    deleteSignFormRef.value.resetFields()
     popOverVisible.value.deleteSign = false
     // 2 加载最新数据
     reload()
@@ -852,7 +929,6 @@ const getButtonDisplayName = (btnType: OperationButtonType) => {
 }
 
 const loadTodoTask = (task: any) => {
-  genericForm.value = {}
   approveForm.value = {}
   approveFormFApi.value = {}
   runningTask.value = task
@@ -866,6 +942,30 @@ const loadTodoTask = (task: any) => {
   }
 }
 
+/** 校验流程表单 */
+const validateNormalForm = async () => {
+  if (props.processDefinition?.formType === BpmModelFormType.NORMAL) {
+    let valid = true
+    try {
+      await props.normalFormApi?.validate()
+    } catch {
+      valid = false;
+    }
+    return valid;
+  } else {
+    return true;
+  }
+}
+/** 从可以编辑的流程表单字段,获取需要修改的流程实例的变量 */
+const getUpdatedProcessInstanceVaiables = ()=> {
+  const variables = {}
+  props.writableFields.forEach( (field) => {
+    const fieldValue = props.normalFormApi.getValue(field)
+    variables[field] = fieldValue;
+  })
+  return variables
+}
+
 defineExpose({ loadTodoTask })
 </script>
 

+ 2 - 2
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue

@@ -25,7 +25,7 @@
           </div>
         </div>
       </template>
-      <div class="flex flex-col items-start gap2" :id="`activity-task-${activity.id}`">
+      <div class="flex flex-col items-start gap2" :id="`activity-task-${activity.id}-${index}`">
         <!-- 第一行:节点名称、时间 -->
         <div class="flex w-full">
           <div class="font-bold"> {{ activity.name }}</div>
@@ -113,7 +113,7 @@
                 </div>
               </div>
             </div>
-            <teleport defer :to="`#activity-task-${activity.id}`">
+            <teleport defer :to="`#activity-task-${activity.id}-${index}`">
               <div
                 v-if="
                   task.reason &&

+ 15 - 7
src/views/bpm/processInstance/detail/index.vue

@@ -49,7 +49,7 @@
                       class="form-box flex flex-col mb-30px flex-1"
                     >
                       <!-- 情况一:流程表单 -->
-                      <el-col v-if="processDefinition?.formType === 10">
+                      <el-col v-if="processDefinition?.formType === BpmModelFormType.NORMAL">
                         <form-create
                           v-model="detailForm.value"
                           v-model:api="fApi"
@@ -58,7 +58,7 @@
                         />
                       </el-col>
                       <!-- 情况二:业务表单 -->
-                      <div v-if="processDefinition?.formType === 20">
+                      <div v-if="processDefinition?.formType === BpmModelFormType.CUSTOM">
                         <BusinessFormComponent :id="processInstance.businessKey" />
                       </div>
                     </div>
@@ -116,6 +116,9 @@
             :process-instance="processInstance"
             :process-definition="processDefinition"
             :userOptions="userOptions"
+            :normal-form="detailForm"
+            :normal-form-api="fApi"
+            :writable-fields="writableFields"
             @success="refresh"
           />
         </div>
@@ -126,7 +129,7 @@
 <script lang="ts" setup>
 import { formatDate } from '@/utils/formatTime'
 import { DICT_TYPE } from '@/utils/dict'
-import { BpmModelType } from '@/utils/constants'
+import { BpmModelType, BpmModelFormType } from '@/utils/constants'
 import { setConfAndFields2 } from '@/utils/formCreate'
 import { registerComponent } from '@/utils/routerHelper'
 import type { ApiAttrs } from '@form-create/element-ui/types/config'
@@ -171,6 +174,8 @@ const detailForm = ref({
   value: {}
 }) // 流程实例的表单详情
 
+const writableFields: Array<string> = [] // 表单可以编辑的字段
+
 /** 获得详情 */
 const getDetail = () => {
   getApprovalDetail()
@@ -202,11 +207,12 @@ const getApprovalDetail = async () => {
     processDefinition.value = data.processDefinition
 
     // 设置表单信息
-    if (processDefinition.value.formType === 10) {
+    if (processDefinition.value.formType === BpmModelFormType.NORMAL) {
       // 获取表单字段权限
       const formFieldsPermission = data.formFieldsPermission
-
-      if (detailForm.value.rule.length > 0) {
+      // 清空可编辑字段为空
+      writableFields.splice(0)
+      if (detailForm.value.rule?.length > 0) {
         // 避免刷新 form-create 显示不了
         detailForm.value.value = processInstance.value.formVariables
       } else {
@@ -271,6 +277,8 @@ const setFieldPermission = (field: string, permission: string) => {
   if (permission === FieldPermissionType.WRITE) {
     //@ts-ignore
     fApi.value?.disabled(false, field)
+    // 加入可以编辑的字段
+    writableFields.push(field)
   }
   if (permission === FieldPermissionType.NONE) {
     //@ts-ignore
@@ -314,6 +322,7 @@ $process-header-height: 194px;
   overflow: auto;
 
   .form-scroll-area {
+    display: flex;
     height: calc(
       100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
         $process-header-height - 40px
@@ -323,7 +332,6 @@ $process-header-height: 194px;
         $process-header-height - 40px
     );
     overflow: auto;
-    display: flex;
     flex-direction: column;
 
     :deep(.box-card) {

+ 38 - 38
src/views/bpm/processInstance/index.vue

@@ -25,13 +25,14 @@
       </el-form-item>
 
       <!-- TODO @ tuituji:style 可以使用 unocss -->
-      <el-form-item label="" prop="category" :style="{ position: 'absolute', right: '130px' }">
-        <!-- TODO @tuituji:应该选择好分类,就触发搜索啦。 -->
+      <el-form-item label="" prop="category" :style="{ position: 'absolute', right: '300px' }">
+        <!-- TODO @tuituji:应该选择好分类,就触发搜索啦。 RE:done & to check-->
         <el-select
           v-model="queryParams.category"
           placeholder="请选择流程分类"
           clearable
           class="!w-155px"
+          @change="handleQuery"
         >
           <el-option
             v-for="category in categoryList"
@@ -42,21 +43,38 @@
         </el-select>
       </el-form-item>
 
+      <el-form-item label="" prop="status" :style="{ position: 'absolute', right: '130px' }">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择流程状态"
+          clearable
+          class="!w-155px"
+          @change="handleQuery"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+
       <!-- 高级筛选 -->
       <!-- TODO @ tuituji:style 可以使用 unocss -->
       <el-form-item :style="{ position: 'absolute', right: '0px' }">
-        <el-button v-popover="popoverRef" v-click-outside="onClickOutside" :icon="List">
-          高级筛选
-        </el-button>
         <el-popover
-          ref="popoverRef"
-          trigger="click"
-          virtual-triggering
+          :visible="showPopover"
           persistent
           :width="400"
           :show-arrow="false"
           placement="bottom-end"
         >
+          <template #reference>
+            <el-button @click="showPopover = !showPopover">
+              <Icon icon="ep:plus" class="mr-5px" />高级筛选
+            </el-button>
+          </template>
           <el-form-item label="流程发起人" class="bold-label" label-position="top" prop="category">
             <el-select
               v-model="queryParams.category"
@@ -86,21 +104,6 @@
               class="!w-390px"
             />
           </el-form-item>
-          <el-form-item label="流程状态" class="bold-label" label-position="top" prop="status">
-            <el-select
-              v-model="queryParams.status"
-              placeholder="请选择流程状态"
-              clearable
-              class="!w-390px"
-            >
-              <el-option
-                v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
           <el-form-item label="发起时间" class="bold-label" label-position="top" prop="createTime">
             <el-date-picker
               v-model="queryParams.createTime"
@@ -112,8 +115,13 @@
               class="!w-240px"
             />
           </el-form-item>
+          <!-- TODO tuituiji:参考钉钉,1)按照清空、取消、确认排序。2)右对齐。3)确认增加 primary -->
+          <el-form-item class="bold-label" label-position="top">
+            <el-button @click="handleQuery"> 确认</el-button>
+            <el-button @click="showPopover = false"> 取消</el-button>
+            <el-button @click="resetQuery"> 清空</el-button>
+          </el-form-item>
         </el-popover>
-        <!-- TODO @tuituji:这里应该有确认,和取消、清空搜索条件,三个按钮。 -->
       </el-form-item>
     </el-form>
   </ContentWrap>
@@ -130,7 +138,7 @@
         fixed="left"
       />
       <!-- TODO @芋艿:摘要 -->
-      <!-- TODO @tuituji:流程状态。可见需求文档里  -->
+      <!-- TODO tuituiji:参考钉钉;1)审批中时,展示审批任务;2)非审批中,展示状态 -->
       <el-table-column label="流程状态" prop="status" width="120">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
@@ -198,8 +206,7 @@
   </ContentWrap>
 </template>
 <script lang="ts" setup>
-// TODO @tuituji:List 改成 <Icon icon="ep:plus" class="mr-5px" /> 类似这种组件哈。
-import { List } from '@element-plus/icons-vue'
+// TODO @tuituji:List 改成 <Icon icon="ep:plus" class="mr-5px" /> 类似这种组件哈。 RE:done & to check
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
 import { ElMessageBox } from 'element-plus'
@@ -241,6 +248,8 @@ const getList = async () => {
   }
 }
 
+const showPopover = ref(false)
+
 /** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.pageNo = 1
@@ -273,7 +282,7 @@ const handleCreate = async (row?: ProcessInstanceVO) => {
 }
 
 /** 查看详情 */
-const handleDetail = (row) => {
+const handleDetail = (row: ProcessInstanceVO) => {
   router.push({
     name: 'BpmProcessInstanceDetail',
     query: {
@@ -283,7 +292,7 @@ const handleDetail = (row) => {
 }
 
 /** 取消按钮操作 */
-const handleCancel = async (row) => {
+const handleCancel = async (row: ProcessInstanceVO) => {
   // 二次确认
   const { value } = await ElMessageBox.prompt('请输入取消原因', '取消流程', {
     confirmButtonText: t('common.ok'),
@@ -298,15 +307,6 @@ const handleCancel = async (row) => {
   await getList()
 }
 
-// TODO @tuituji:这个 import 是不是没用哈?
-import { ClickOutside as vClickOutside } from 'element-plus'
-
-// TODO @tuituji:onClickAdvancedSearch。方法名叫这个,会更好一些哇?打开高级搜索。
-const popoverRef = ref()
-const onClickOutside = () => {
-  unref(popoverRef).popperRef?.delayHide?.()
-}
-
 /** 激活时 **/
 onActivated(() => {
   getList()

+ 142 - 6
src/views/bpm/simple/SimpleModelDesign.vue

@@ -1,6 +1,15 @@
 <template>
   <ContentWrap :bodyStyle="{ padding: '20px 16px' }">
-    <SimpleProcessDesigner :model-id="modelId" @success="close" />
+    <SimpleProcessDesigner
+      :model-id="modelId"
+      :model-key="modelKey"
+      :model-name="modelName"
+      :value="currentValue"
+      @success="handleSuccess"
+      @init-finished="handleInit"
+      :start-user-ids="startUserIds"
+      ref="designerRef"
+    />
   </ContentWrap>
 </template>
 <script setup lang="ts">
@@ -9,11 +18,138 @@ import { SimpleProcessDesigner } from '@/components/SimpleProcessDesignerV2/src/
 defineOptions({
   name: 'SimpleModelDesign'
 })
-const router = useRouter() // 路由
-const { query } = useRoute() // 路由的查询
-const modelId = query.modelId as string
-const close = () => {
-  router.push({ path: '/bpm/manager/model' })
+
+const props = defineProps<{
+  modelId?: string
+  modelKey?: string
+  modelName?: string
+  value?: string
+  startUserIds?: number[]
+}>()
+
+const emit = defineEmits(['success', 'init-finished'])
+const designerRef = ref()
+const isInitialized = ref(false)
+const currentValue = ref('')
+
+// 初始化或更新当前值
+const initOrUpdateValue = async () => {
+  console.log('initOrUpdateValue', props.value)
+  if (props.value) {
+    currentValue.value = props.value
+    // 如果设计器已经初始化,立即加载数据
+    if (isInitialized.value && designerRef.value) {
+      try {
+        await designerRef.value.loadProcessData(props.value)
+        await nextTick()
+        if (designerRef.value.refresh) {
+          await designerRef.value.refresh()
+        }
+      } catch (error) {
+        console.error('加载流程数据失败:', error)
+      }
+    }
+  }
+}
+
+// 监听属性变化
+watch(
+  [() => props.modelKey, () => props.modelName, () => props.value],
+  async ([newKey, newName, newValue], [oldKey, oldName, oldValue]) => {
+    if (designerRef.value && isInitialized.value) {
+      try {
+        if (newKey && newName && (newKey !== oldKey || newName !== oldName)) {
+          await designerRef.value.updateModel(newKey, newName)
+        }
+        if (newValue && newValue !== oldValue) {
+          currentValue.value = newValue
+          await designerRef.value.loadProcessData(newValue)
+          await nextTick()
+          if (designerRef.value.refresh) {
+            await designerRef.value.refresh()
+          }
+        }
+      } catch (error) {
+        console.error('更新流程数据失败:', error)
+      }
+    }
+  },
+  { deep: true, immediate: true }
+)
+
+// 初始化完成回调
+const handleInit = async () => {
+  try {
+    isInitialized.value = true
+    emit('init-finished')
+
+    // 等待下一个tick,确保设计器已经准备好
+    await nextTick()
+
+    // 初始化完成后,设置初始值
+    if (props.modelKey && props.modelName) {
+      await designerRef.value.updateModel(props.modelKey, props.modelName)
+    }
+    if (props.value) {
+      currentValue.value = props.value
+      await designerRef.value.loadProcessData(props.value)
+      // 再次刷新确保数据正确加载
+      await nextTick()
+      if (designerRef.value.refresh) {
+        await designerRef.value.refresh()
+      }
+    }
+  } catch (error) {
+    console.error('初始化流程数据失败:', error)
+  }
+}
+
+// 修改成功回调
+const handleSuccess = (data?: any) => {
+  console.warn('handleSuccess', data)
+  if (data && data !== currentValue.value) {
+    currentValue.value = data
+    emit('success', data)
+  }
+}
+
+/** 获取当前流程数据 */
+const getCurrentFlowData = async () => {
+  try {
+    if (designerRef.value) {
+      const data = await designerRef.value.getCurrentFlowData()
+      if (data) {
+        currentValue.value = data
+      }
+      return data
+    }
+    return currentValue.value || undefined
+  } catch (error) {
+    console.error('获取流程数据失败:', error)
+    return currentValue.value || undefined
+  }
 }
+
+// 组件创建时初始化数据
+onMounted(() => {
+  initOrUpdateValue()
+})
+
+// 组件卸载前保存数据
+onBeforeUnmount(async () => {
+  try {
+    const data = await getCurrentFlowData()
+    if (data) {
+      emit('success', data)
+    }
+  } catch (error) {
+    console.error('保存数据失败:', error)
+  }
+})
+
+defineExpose({
+  getCurrentFlowData,
+  refresh: () => designerRef.value?.refresh?.()
+})
 </script>
 <style lang="scss" scoped></style>

+ 94 - 19
src/views/bpm/task/done/index.vue

@@ -16,7 +16,7 @@
       class="-mb-15px"
       label-width="68px"
     >
-      <el-form-item label="任务名称" prop="name">
+      <el-form-item label="" prop="name">
         <el-input
           v-model="queryParams.name"
           class="!w-240px"
@@ -25,27 +25,96 @@
           @keyup.enter="handleQuery"
         />
       </el-form-item>
-      <el-form-item label="创建时间" prop="createTime">
-        <el-date-picker
-          v-model="queryParams.createTime"
-          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-          class="!w-240px"
-          end-placeholder="结束日期"
-          start-placeholder="开始日期"
-          type="daterange"
-          value-format="YYYY-MM-DD HH:mm:ss"
-        />
-      </el-form-item>
       <el-form-item>
         <el-button @click="handleQuery">
           <Icon class="mr-5px" icon="ep:search" />
           搜索
         </el-button>
-        <el-button @click="resetQuery">
-          <Icon class="mr-5px" icon="ep:refresh" />
-          重置
-        </el-button>
       </el-form-item>
+
+      <el-form-item label="" prop="category" :style="{ position: 'absolute', right: '300px' }">
+        <el-select
+          v-model="queryParams.category"
+          placeholder="请选择流程分类"
+          clearable
+          class="!w-155px"
+          @change="handleQuery"
+        >
+          <el-option
+            v-for="category in categoryList"
+            :key="category.code"
+            :label="category.name"
+            :value="category.code"
+          />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="" prop="status" :style="{ position: 'absolute', right: '130px' }">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择流程状态"
+          clearable
+          class="!w-155px"
+          @change="handleQuery"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item> 
+
+      <!-- 高级筛选 -->
+      <el-form-item :style="{ position: 'absolute', right: '0px' }">
+        <el-popover
+          :visible="showPopover"
+          persistent
+          :width="400"
+          :show-arrow="false"
+          placement="bottom-end"
+        >
+          <template #reference>
+            <el-button @click="showPopover = !showPopover" >
+              <Icon icon="ep:plus" class="mr-5px" />高级筛选 
+            </el-button>
+            
+          </template>
+          <el-form-item label="流程发起人" class="bold-label" label-position="top" prop="category">
+            <el-select
+              v-model="queryParams.category"
+              placeholder="请选择流程发起人"
+              clearable
+              class="!w-390px"
+            >
+              <el-option
+                v-for="category in categoryList"
+                :key="category.code"
+                :label="category.name"
+                :value="category.code"
+              />
+            </el-select>
+          </el-form-item>          
+          <el-form-item label="发起时间" class="bold-label" label-position="top" prop="createTime">
+            <el-date-picker
+              v-model="queryParams.createTime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              type="daterange"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item class="bold-label" label-position="top">
+            <el-button @click="handleQuery"> 确认</el-button>
+            <el-button @click="showPopover = false"> 取消</el-button>
+            <el-button @click="resetQuery"> 清空</el-button>
+        </el-form-item>
+        </el-popover>
+      </el-form-item>
+
     </el-form>
   </ContentWrap>
 
@@ -110,9 +179,10 @@
   </ContentWrap>
 </template>
 <script lang="ts" setup>
-import { DICT_TYPE } from '@/utils/dict'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { dateFormatter, formatPast2 } from '@/utils/formatTime'
 import * as TaskApi from '@/api/bpm/task'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
 
 defineOptions({ name: 'BpmTodoTask' })
 
@@ -125,9 +195,13 @@ const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   name: '',
+  category: undefined,  
+  status: undefined,
   createTime: []
 })
 const queryFormRef = ref() // 搜索的表单
+const categoryList = ref<CategoryVO[]>([]) // 流程分类列表
+const showPopover = ref(false)
 
 /** 查询任务列表 */
 const getList = async () => {
@@ -165,7 +239,8 @@ const handleAudit = (row: any) => {
 }
 
 /** 初始化 **/
-onMounted(() => {
-  getList()
+onMounted(async () => {
+  await getList()
+  categoryList.value = await CategoryApi.getCategorySimpleList()
 })
 </script>

+ 76 - 18
src/views/bpm/task/todo/index.vue

@@ -16,7 +16,7 @@
       class="-mb-15px"
       label-width="68px"
     >
-      <el-form-item label="任务名称" prop="name">
+      <el-form-item label="" prop="name">
         <el-input
           v-model="queryParams.name"
           class="!w-240px"
@@ -25,27 +25,79 @@
           @keyup.enter="handleQuery"
         />
       </el-form-item>
-      <el-form-item label="创建时间" prop="createTime">
-        <el-date-picker
-          v-model="queryParams.createTime"
-          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-          class="!w-240px"
-          end-placeholder="结束日期"
-          start-placeholder="开始日期"
-          type="daterange"
-          value-format="YYYY-MM-DD HH:mm:ss"
-        />
-      </el-form-item>
       <el-form-item>
         <el-button @click="handleQuery">
           <Icon class="mr-5px" icon="ep:search" />
           搜索
         </el-button>
-        <el-button @click="resetQuery">
-          <Icon class="mr-5px" icon="ep:refresh" />
-          重置
-        </el-button>
       </el-form-item>
+
+      <el-form-item label="" prop="category" :style="{ position: 'absolute', right: '130px' }">
+        <el-select
+          v-model="queryParams.category"
+          placeholder="请选择流程分类"
+          clearable
+          class="!w-155px"
+          @change="handleQuery"
+        >
+          <el-option
+            v-for="category in categoryList"
+            :key="category.code"
+            :label="category.name"
+            :value="category.code"
+          />
+        </el-select>
+      </el-form-item>
+
+      <!-- 高级筛选 -->
+      <el-form-item :style="{ position: 'absolute', right: '0px' }">
+        <el-popover
+          :visible="showPopover"
+          persistent
+          :width="400"
+          :show-arrow="false"
+          placement="bottom-end"
+        >
+          <template #reference>
+            <el-button @click="showPopover = !showPopover" >
+              <Icon icon="ep:plus" class="mr-5px" />高级筛选 
+            </el-button>
+            
+          </template>
+          <el-form-item label="流程发起人" class="bold-label" label-position="top" prop="category">
+            <el-select
+              v-model="queryParams.category"
+              placeholder="请选择流程发起人"
+              clearable
+              class="!w-390px"
+            >
+              <el-option
+                v-for="category in categoryList"
+                :key="category.code"
+                :label="category.name"
+                :value="category.code"
+              />
+            </el-select>
+          </el-form-item>          
+          <el-form-item label="发起时间" class="bold-label" label-position="top" prop="createTime">
+            <el-date-picker
+              v-model="queryParams.createTime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              type="daterange"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item class="bold-label" label-position="top">
+            <el-button @click="handleQuery"> 确认</el-button>
+            <el-button @click="showPopover = false"> 取消</el-button>
+            <el-button @click="resetQuery"> 清空</el-button>
+        </el-form-item>
+        </el-popover>
+      </el-form-item>
+
     </el-form>
   </ContentWrap>
 
@@ -95,6 +147,7 @@
 <script lang="ts" setup>
 import { dateFormatter } from '@/utils/formatTime'
 import * as TaskApi from '@/api/bpm/task'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
 
 defineOptions({ name: 'BpmTodoTask' })
 
@@ -107,9 +160,11 @@ const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   name: '',
+  category: undefined,
   createTime: []
 })
 const queryFormRef = ref() // 搜索的表单
+const categoryList = ref<CategoryVO[]>([]) // 流程分类列表
 
 /** 查询任务列表 */
 const getList = async () => {
@@ -123,6 +178,8 @@ const getList = async () => {
   }
 }
 
+const showPopover = ref(false)
+
 /** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.pageNo = 1
@@ -147,7 +204,8 @@ const handleAudit = (row: any) => {
 }
 
 /** 初始化 **/
-onMounted(() => {
-  getList()
+onMounted(async () => {
+  await getList()
+  categoryList.value = await CategoryApi.getCategorySimpleList()
 })
 </script>

+ 3 - 3
src/views/crm/business/BusinessForm.vue

@@ -101,7 +101,7 @@
             <el-input
               disabled
               v-model="formData.totalProductPrice"
-              :formatter="erpPriceTableColumnFormatter"
+              :formatter="erpPriceInputFormatter"
             />
           </el-form-item>
         </el-col>
@@ -123,7 +123,7 @@
               disabled
               v-model="formData.totalPrice"
               placeholder="请输入商机金额"
-              :formatter="erpPriceTableColumnFormatter"
+              :formatter="erpPriceInputFormatter"
             />
           </el-form-item>
         </el-col>
@@ -142,7 +142,7 @@ import * as CustomerApi from '@/api/crm/customer'
 import * as UserApi from '@/api/system/user'
 import { useUserStore } from '@/store/modules/user'
 import BusinessProductForm from './components/BusinessProductForm.vue'
-import { erpPriceMultiply, erpPriceTableColumnFormatter } from '@/utils'
+import { erpPriceMultiply, erpPriceInputFormatter } from '@/utils'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗

+ 3 - 3
src/views/crm/contract/ContractForm.vue

@@ -159,7 +159,7 @@
             <el-input
               disabled
               v-model="formData.totalProductPrice"
-              :formatter="erpPriceTableColumnFormatter"
+              :formatter="erpPriceInputFormatter"
             />
           </el-form-item>
         </el-col>
@@ -181,7 +181,7 @@
               disabled
               v-model="formData.totalPrice"
               placeholder="请输入商机金额"
-              :formatter="erpPriceTableColumnFormattere"
+              :formatter="erpPriceInputFormatter"
             />
           </el-form-item>
         </el-col>
@@ -199,7 +199,7 @@ import * as ContractApi from '@/api/crm/contract'
 import * as UserApi from '@/api/system/user'
 import * as ContactApi from '@/api/crm/contact'
 import * as BusinessApi from '@/api/crm/business'
-import { erpPriceMultiply, erpPriceTableColumnFormatter } from '@/utils'
+import { erpPriceMultiply, erpPriceInputFormatter } from '@/utils'
 import { useUserStore } from '@/store/modules/user'
 import ContractProductForm from '@/views/crm/contract/components/ContractProductForm.vue'
 

+ 6 - 1
src/views/erp/purchase/order/components/PurchaseOrderItemForm.vue

@@ -66,7 +66,11 @@
       </el-table-column>
       <el-table-column label="产品单价" fixed="right" min-width="120">
         <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.productPrice`" class="mb-0px!">
+          <el-form-item
+            :prop="`${$index}.productPrice`"
+            :rules="formRules.productPrice"
+            class="mb-0px!"
+          >
             <el-input-number
               v-model="row.productPrice"
               controls-position="right"
@@ -153,6 +157,7 @@ const formLoading = ref(false) // 表单的加载中
 const formData = ref([])
 const formRules = reactive({
   productId: [{ required: true, message: '产品不能为空', trigger: 'blur' }],
+  productPrice: [{ required: true, message: '产品单价不能为空', trigger: 'blur' }],
   count: [{ required: true, message: '产品数量不能为空', trigger: 'blur' }]
 })
 const formRef = ref([]) // 表单 Ref

+ 6 - 4
src/views/infra/build/index.vue

@@ -135,7 +135,8 @@ const makeTemplate = () => {
 
 /** 复制 **/
 const copy = async (text: string) => {
-  const { copy, copied, isSupported } = useClipboard({ source: text })
+  const textToCopy = JSON.stringify(text, null, 2)
+  const { copy, copied, isSupported } = useClipboard({ source: textToCopy })
   if (!isSupported) {
     message.error(t('common.copyError'))
   } else {
@@ -149,17 +150,18 @@ const copy = async (text: string) => {
 /**
  * 代码高亮
  */
-const highlightedCode = (code) => {
+const highlightedCode = (code: string) => {
   // 处理语言和代码
   let language = 'json'
   if (formType.value === 2) {
     language = 'xml'
   }
+  // debugger
   if (!isString(code)) {
-    code = JSON.stringify(code)
+    code = JSON.stringify(code, null, 2)
   }
   // 高亮
-  const result = hljs.highlight(language, code, true)
+  const result = hljs.highlight(code, { language: language, ignoreIllegals: true })
   return result.value || '&nbsp;'
 }
 

+ 10 - 0
src/views/infra/file/index.vue

@@ -95,6 +95,9 @@
       />
       <el-table-column label="操作" align="center">
         <template #default="scope">
+          <el-button link type="primary" @click="copyToClipboard(scope.row.url)">
+            复制链接
+          </el-button>
           <el-button
             link
             type="danger"
@@ -172,6 +175,13 @@ const openForm = () => {
   formRef.value.open()
 }
 
+/** 复制到剪贴板方法 */
+const copyToClipboard = (text: string) => {
+  navigator.clipboard.writeText(text).then(() => {
+    message.success('复制成功')
+  })
+}
+
 /** 删除按钮操作 */
 const handleDelete = async (id: number) => {
   try {

+ 0 - 1
src/views/mall/promotion/bargain/activity/index.vue

@@ -151,7 +151,6 @@ import * as BargainActivityApi from '@/api/mall/promotion/bargain/bargainActivit
 import BargainActivityForm from './BargainActivityForm.vue'
 import { formatDate } from '@/utils/formatTime'
 import { fenToYuanFormat } from '@/utils/formatter'
-import { closeBargainActivity } from '@/api/mall/promotion/bargain/bargainActivity'
 
 defineOptions({ name: 'PromotionBargainActivity' })
 

+ 9 - 2
src/views/mall/statistics/product/components/ProductRank.vue

@@ -10,7 +10,7 @@
     </template>
     <!-- 排行列表 -->
     <el-table v-loading="loading" :data="list" @sort-change="handleSortChange">
-      <el-table-column label="商品ID" prop="spuId" min-width="70" />
+      <el-table-column label="商品 ID" prop="spuId" min-width="70" />
       <el-table-column label="商品图片" align="center" prop="picUrl" width="80">
         <template #default="{ row }">
           <el-image
@@ -27,7 +27,13 @@
       <el-table-column label="加购件数" prop="cartCount" min-width="105" sortable="custom" />
       <el-table-column label="下单件数" prop="orderCount" min-width="105" sortable="custom" />
       <el-table-column label="支付件数" prop="orderPayCount" min-width="105" sortable="custom" />
-      <el-table-column label="支付金额" prop="orderPayPrice" min-width="105" sortable="custom" />
+      <el-table-column
+        label="支付金额"
+        prop="orderPayPrice"
+        min-width="105"
+        sortable="custom"
+        :formatter="fenToYuanFormat"
+      />
       <el-table-column label="收藏数" prop="favoriteCount" min-width="90" sortable="custom" />
       <el-table-column
         label="访客-支付转化率(%)"
@@ -50,6 +56,7 @@
 import { ProductStatisticsApi, ProductStatisticsVO } from '@/api/mall/statistics/product'
 import { CardTitle } from '@/components/Card'
 import { buildSortingField } from '@/utils'
+import { fenToYuanFormat } from '@/utils/formatter'
 
 /** 商品排行 */
 defineOptions({ name: 'ProductRank' })

+ 1 - 1
src/views/mp/draft/components/NewsForm.vue

@@ -144,7 +144,7 @@ const accountId = inject<number>('accountId')
 
 // ========== 文件上传 ==========
 const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-permanent' // 上传永久素材的地址
-const editorConfig = createEditorConfig(UPLOAD_URL, accountId)
+const editorConfig = createEditorConfig(UPLOAD_URL, unref(accountId))
 
 // v-model=newsList
 const emit = defineEmits<{

+ 1 - 1
src/views/report/goview/index.vue

@@ -8,5 +8,5 @@
 <script lang="ts" setup>
 defineOptions({ name: 'GoView' })
 
-const src = 'http://127.0.0.1:3000'
+const src = ref(import.meta.env.VITE_GOVIEW_URL)
 </script>

+ 12 - 8
src/views/system/area/index.vue

@@ -16,6 +16,7 @@
         <template #default="{ height, width }">
           <!-- Virtualized Table 虚拟化表格:高性能,解决表格在大数据量下的卡顿问题 -->
           <el-table-v2
+            v-loading="loading"
             :columns="columns"
             :data="list"
             :width="width"
@@ -31,7 +32,7 @@
   <AreaForm ref="formRef" />
 </template>
 <script setup lang="tsx">
-import type { Column } from 'element-plus'
+import { Column } from 'element-plus'
 import AreaForm from './AreaForm.vue'
 import * as AreaApi from '@/api/system/area'
 
@@ -40,7 +41,7 @@ defineOptions({ name: 'SystemArea' })
 // 表格的 column 字段
 const columns: Column[] = [
   {
-    dataKey: 'id', // 需要渲染当前列的数据字段。例如说:{id:9527, name:'Mike'},则填 id
+    dataKey: 'id', // 需要渲染当前列的数据字段
     title: '编号', // 显示在单元格表头的文本
     width: 400, // 当前列的宽度,必须设置
     fixed: true, // 是否固定列
@@ -52,14 +53,17 @@ const columns: Column[] = [
     width: 200
   }
 ]
-// 表格的数据
-const list = ref([])
+const loading = ref(true) // 列表的加载中
+const list = ref([]) // 表格的数据
 
-/**
- * 获得数据列表
- */
+/** 获得数据列表 */
 const getList = async () => {
-  list.value = await AreaApi.getAreaTree()
+  loading.value = true
+  try {
+    list.value = await AreaApi.getAreaTree()
+  } finally {
+    loading.value = false
+  }
 }
 
 /** 添加/修改操作 */

+ 113 - 72
src/views/system/menu/index.vue

@@ -53,10 +53,6 @@
           <Icon class="mr-5px" icon="ep:plus" />
           新增
         </el-button>
-        <el-button plain type="danger" @click="toggleExpandAll">
-          <Icon class="mr-5px" icon="ep:sort" />
-          展开/折叠
-        </el-button>
         <el-button plain @click="refreshMenu">
           <Icon class="mr-5px" icon="ep:refresh" />
           刷新菜单缓存
@@ -67,65 +63,22 @@
 
   <!-- 列表 -->
   <ContentWrap>
-    <el-table
-      v-if="refreshTable"
-      v-loading="loading"
-      :data="list"
-      :default-expand-all="isExpandAll"
-      row-key="id"
-    >
-      <el-table-column :show-overflow-tooltip="true" label="菜单名称" prop="name" width="250" />
-      <el-table-column align="center" label="图标" prop="icon" width="100">
-        <template #default="scope">
-          <Icon :icon="scope.row.icon" />
-        </template>
-      </el-table-column>
-      <el-table-column label="排序" prop="sort" width="60" />
-      <el-table-column :show-overflow-tooltip="true" label="权限标识" prop="permission" />
-      <el-table-column :show-overflow-tooltip="true" label="组件路径" prop="component" />
-      <el-table-column :show-overflow-tooltip="true" label="组件名称" prop="componentName" />
-      <el-table-column label="状态" prop="status">
-        <template #default="scope">
-          <el-switch
-            class="ml-4px"
-            v-model="scope.row.status"
-            v-hasPermi="['system:menu:update']"
-            :active-value="CommonStatusEnum.ENABLE"
-            :inactive-value="CommonStatusEnum.DISABLE"
-            :loading="menuStatusUpdating[scope.row.id]"
-            @change="(val) => handleStatusChanged(scope.row, val as number)"
+    <div style="height: 700px">
+      <!-- AutoResizer 自动调节大小 -->
+      <el-auto-resizer>
+        <template #default="{ height, width }">
+          <!-- Virtualized Table 虚拟化表格:高性能,解决表格在大数据量下的卡顿问题 -->
+          <el-table-v2
+            v-loading="loading"
+            :columns="columns"
+            :data="list"
+            :width="width"
+            :height="height"
+            expand-column-key="name"
           />
         </template>
-      </el-table-column>
-      <el-table-column align="center" label="操作">
-        <template #default="scope">
-          <el-button
-            v-hasPermi="['system:menu:update']"
-            link
-            type="primary"
-            @click="openForm('update', scope.row.id)"
-          >
-            修改
-          </el-button>
-          <el-button
-            v-hasPermi="['system:menu:create']"
-            link
-            type="primary"
-            @click="openForm('create', undefined, scope.row.id)"
-          >
-            新增
-          </el-button>
-          <el-button
-            v-hasPermi="['system:menu:delete']"
-            link
-            type="danger"
-            @click="handleDelete(scope.row.id)"
-          >
-            删除
-          </el-button>
-        </template>
-      </el-table-column>
-    </el-table>
+      </el-auto-resizer>
+    </div>
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->
@@ -138,6 +91,10 @@ import * as MenuApi from '@/api/system/menu'
 import { MenuVO } from '@/api/system/menu'
 import MenuForm from './MenuForm.vue'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import { h } from 'vue'
+import { Column, ElButton } from 'element-plus'
+import { Icon } from '@/components/Icon'
+import { hasPermission } from '@/directives/permission/hasPermi'
 import { CommonStatusEnum } from '@/utils/constants'
 
 defineOptions({ name: 'SystemMenu' })
@@ -146,6 +103,101 @@ const { wsCache } = useCache()
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 
+// 表格的 column 字段
+const columns: Column[] = [
+  {
+    dataKey: 'name',
+    title: '菜单名称',
+    width: 250
+  },
+  {
+    dataKey: 'icon',
+    title: '图标',
+    width: 150,
+    cellRenderer: ({ rowData }) => {
+      return h(Icon, {
+        icon: rowData.icon
+      })
+    }
+  },
+  {
+    dataKey: 'sort',
+    title: '排序',
+    width: 100
+  },
+  {
+    dataKey: 'permission',
+    title: '权限标识',
+    width: 240
+  },
+  {
+    dataKey: 'component',
+    title: '组件路径',
+    width: 240
+  },
+  {
+    dataKey: 'componentName',
+    title: '组件名称',
+    width: 240
+  },
+  {
+    dataKey: 'status',
+    title: '状态',
+    width: 160,
+    cellRenderer: ({ rowData }) => {
+      return h(ElSwitch, {
+        modelValue: rowData.status,
+        activeValue: CommonStatusEnum.ENABLE,
+        inactiveValue: CommonStatusEnum.DISABLE,
+        loading: menuStatusUpdating.value[rowData.id],
+        disabled: !hasPermission(['system:menu:update']),
+        onChange: (val) => handleStatusChanged(rowData, val as number)
+      })
+    }
+  },
+  {
+    dataKey: 'operation',
+    title: '操作',
+    width: 200,
+    cellRenderer: ({ rowData }) => {
+      return h(
+        'div',
+        [
+          hasPermission(['system:menu:update']) &&
+            h(
+              ElButton,
+              {
+                link: true,
+                type: 'primary',
+                onClick: () => openForm('update', rowData.id)
+              },
+              '修改'
+            ),
+          hasPermission(['system:menu:create']) &&
+            h(
+              ElButton,
+              {
+                link: true,
+                type: 'primary',
+                onClick: () => openForm('create', undefined, rowData.id)
+              },
+              '新增'
+            ),
+          hasPermission(['system:menu:delete']) &&
+            h(
+              ElButton,
+              {
+                link: true,
+                type: 'danger',
+                onClick: () => handleDelete(rowData.id)
+              },
+              '删除'
+            )
+        ].filter(Boolean)
+      )
+    }
+  }
+]
 const loading = ref(true) // 列表的加载中
 const list = ref<any>([]) // 列表的数据
 const queryParams = reactive({
@@ -153,8 +205,6 @@ const queryParams = reactive({
   status: undefined
 })
 const queryFormRef = ref() // 搜索的表单
-const isExpandAll = ref(false) // 是否展开,默认全部折叠
-const refreshTable = ref(true) // 重新渲染表格状态
 
 /** 查询列表 */
 const getList = async () => {
@@ -184,15 +234,6 @@ const openForm = (type: string, id?: number, parentId?: number) => {
   formRef.value.open(type, id, parentId)
 }
 
-/** 展开/折叠操作 */
-const toggleExpandAll = () => {
-  refreshTable.value = false
-  isExpandAll.value = !isExpandAll.value
-  nextTick(() => {
-    refreshTable.value = true
-  })
-}
-
 /** 刷新菜单缓存按钮操作 */
 const refreshMenu = async () => {
   try {

+ 1 - 0
src/views/system/user/index.vue

@@ -124,6 +124,7 @@
                 :active-value="0"
                 :inactive-value="1"
                 @change="handleStatusChange(scope.row)"
+                :disabled="!checkPermi(['system:user:update'])"
               />
             </template>
           </el-table-column>

+ 1 - 0
types/env.d.ts

@@ -25,6 +25,7 @@ interface ImportMetaEnv {
   readonly VITE_DROP_CONSOLE: string
   readonly VITE_SOURCEMAP: string
   readonly VITE_OUT_DIR: string
+  readonly VITE_GOVIEW_URL: string
 }
 
 declare global {

+ 2 - 1
vite.config.ts

@@ -44,7 +44,8 @@ export default ({command, mode}: ConfigEnv): UserConfig => {
             preprocessorOptions: {
                 scss: {
                     additionalData: '@use "@/styles/variables.scss" as *;',
-                    javascriptEnabled: true
+                    javascriptEnabled: true,
+                    silenceDeprecations: ["legacy-js-api"], // 参考自 https://stackoverflow.com/questions/78997907/the-legacy-js-api-is-deprecated-and-will-be-removed-in-dart-sass-2-0-0
                 }
             }
         },