Procházet zdrojové kódy

Merge branch 'feature/bpm' of https://gitee.com/yudaocode/yudao-ui-admin-vue3

YunaiV před 6 měsíci
rodič
revize
e9a4efbf6a
62 změnil soubory, kde provedl 2683 přidání a 1835 odebrání
  1. 1 0
      package.json
  2. 34 69
      pnpm-lock.yaml
  3. 4 0
      src/api/bpm/model/index.ts
  4. 2 1
      src/api/bpm/processInstance/index.ts
  5. 65 13
      src/components/SimpleProcessDesignerV2/src/NodeHandler.vue
  6. 14 0
      src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue
  7. 10 114
      src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue
  8. 51 1
      src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue
  9. 154 14
      src/components/SimpleProcessDesignerV2/src/consts.ts
  10. 34 15
      src/components/SimpleProcessDesignerV2/src/node.ts
  11. 25 214
      src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue
  12. 201 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/RouterNodeConfig.vue
  13. 141 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/TriggerNodeConfig.vue
  14. 82 14
      src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue
  15. 272 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/components/Condition.vue
  16. 181 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestParamSetting.vue
  17. 88 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/components/UserTaskListener.vue
  18. 1 2
      src/components/SimpleProcessDesignerV2/src/nodes/DelayTimerNode.vue
  19. 1 1
      src/components/SimpleProcessDesignerV2/src/nodes/EndEventNode.vue
  20. 7 4
      src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue
  21. 8 5
      src/components/SimpleProcessDesignerV2/src/nodes/InclusiveNode.vue
  22. 97 0
      src/components/SimpleProcessDesignerV2/src/nodes/RouterNode.vue
  23. 2 2
      src/components/SimpleProcessDesignerV2/src/nodes/StartUserNode.vue
  24. 97 0
      src/components/SimpleProcessDesignerV2/src/nodes/TriggerNode.vue
  25. 1 1
      src/components/SimpleProcessDesignerV2/src/nodes/UserTaskNode.vue
  26. binární
      src/components/SimpleProcessDesignerV2/theme/iconfont.ttf
  27. binární
      src/components/SimpleProcessDesignerV2/theme/iconfont.woff
  28. binární
      src/components/SimpleProcessDesignerV2/theme/iconfont.woff2
  29. 55 16
      src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss
  30. 2 22
      src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue
  31. 39 0
      src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json
  32. 1 1
      src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue
  33. 3 0
      src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
  34. 33 2
      src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue
  35. 21 12
      src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue
  36. 31 1
      src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
  37. 10 10
      src/directives/permission/hasPermi.ts
  38. 56 44
      src/layout/components/TagsView/src/TagsView.vue
  39. 2 1
      src/router/modules/remaining.ts
  40. 24 2
      src/store/modules/tagsView.ts
  41. 5 5
      src/store/modules/user.ts
  42. 6 0
      src/utils/constants.ts
  43. 29 0
      src/utils/download.ts
  44. 4 15
      src/utils/permission.ts
  45. 161 60
      src/views/bpm/model/CategoryDraggableModel.vue
  46. 0 440
      src/views/bpm/model/ModelForm.vue
  47. 15 187
      src/views/bpm/model/editor/index.vue
  48. 25 53
      src/views/bpm/model/form/BasicInfo.vue
  49. 289 0
      src/views/bpm/model/form/ExtraSettings.vue
  50. 1 10
      src/views/bpm/model/form/FormDesign.vue
  51. 6 170
      src/views/bpm/model/form/ProcessDesign.vue
  52. 89 120
      src/views/bpm/model/form/index.vue
  53. 1 5
      src/views/bpm/model/index.vue
  54. 86 40
      src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue
  55. 15 20
      src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue
  56. 11 0
      src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue
  57. 50 0
      src/views/bpm/processInstance/detail/SignDialog.vue
  58. 9 0
      src/views/bpm/processInstance/index.vue
  59. 4 121
      src/views/bpm/simple/SimpleModelDesign.vue
  60. 1 0
      src/views/bpm/task/copy/index.vue
  61. 14 5
      src/views/bpm/task/done/index.vue
  62. 12 3
      src/views/bpm/task/todo/index.vue

+ 1 - 0
package.json

@@ -73,6 +73,7 @@
     "vue-i18n": "9.10.2",
     "vue-router": "4.4.5",
     "vue-types": "^5.1.1",
+    "vue3-signature": "^0.2.4",
     "vuedraggable": "^4.1.0",
     "web-storage-cache": "^1.1.1",
     "xml-js": "^1.6.11"

+ 34 - 69
pnpm-lock.yaml

@@ -45,8 +45,8 @@ dependencies:
     specifier: ^1.1.5
     version: 1.1.5
   bpmn-js-token-simulation:
-    specifier: ^0.10.0
-    version: 0.10.0
+    specifier: ^0.36.0
+    version: 0.36.0
   camunda-bpmn-moddle:
     specifier: ^7.0.1
     version: 7.0.1
@@ -149,6 +149,9 @@ dependencies:
   vue-types:
     specifier: ^5.1.1
     version: 5.1.3(vue@3.5.12)
+  vue3-signature:
+    specifier: ^0.2.4
+    version: 0.2.4(vue@3.5.12)
   vuedraggable:
     specifier: ^4.1.0
     version: 4.1.0(vue@3.5.12)
@@ -4581,12 +4584,14 @@ packages:
       min-dom: 4.2.1
     dev: true
 
-  /bpmn-js-token-simulation@0.10.0:
-    resolution: {integrity: sha512-QuZQ/KVXKt9Vl+XENyOBoTW2Aw+uKjuBlKdCJL6El7AyM7DkJ5bZkSYURshId1SkBDdYg2mJ1flSmsrhGuSfwg==, tarball: https://registry.npmmirror.com/bpmn-js-token-simulation/-/bpmn-js-token-simulation-0.10.0.tgz}
+  /bpmn-js-token-simulation@0.36.0:
+    resolution: {integrity: sha512-vz+RHlbZCev/6dzk6FhJRz8M0aZ1GL7Xrza0ecWqeg4tHbgPozgyOm3tXTz75XdtOwRVVBzmCjcciXQX7A55wQ==, tarball: https://registry.npmmirror.com/bpmn-js-token-simulation/-/bpmn-js-token-simulation-0.36.0.tgz}
+    engines: {node: '>= 16'}
     dependencies:
-      min-dash: 3.8.1
-      min-dom: 0.2.0
-      svg.js: 2.7.1
+      inherits-browser: 0.1.0
+      min-dash: 4.2.2
+      min-dom: 4.2.1
+      randomcolor: 0.6.2
     dev: false
 
   /bpmn-js@17.11.1:
@@ -4927,51 +4932,13 @@ packages:
       dot-prop: 5.3.0
     dev: true
 
-  /component-classes@1.2.6:
-    resolution: {integrity: sha512-hPFGULxdwugu1QWW3SvVOCUHLzO34+a2J6Wqy0c5ASQkfi9/8nZcBB0ZohaEbXOQlCflMAEMmEWk7u7BVs4koA==, tarball: https://registry.npmmirror.com/component-classes/-/component-classes-1.2.6.tgz}
-    dependencies:
-      component-indexof: 0.0.3
-    dev: false
-
-  /component-closest@0.1.4:
-    resolution: {integrity: sha512-NF9hMj6JKGM5sb6wP/dg7GdJOttaIH9PcTsUNdWcrvu7Kw/5R5swQAFpgaYEHlARrNMyn4Wf7O1PlRej+pt76Q==, tarball: https://registry.npmmirror.com/component-closest/-/component-closest-0.1.4.tgz}
-    dependencies:
-      component-matches-selector: 0.1.7
-    dev: false
-
-  /component-delegate@0.2.4:
-    resolution: {integrity: sha512-OlpcB/6Fi+kXQPh/TfXnSvvmrU04ghz7vcJh/jgLF0Ni+I+E3WGlKJQbBGDa5X+kVUG8WxOgjP+8iWbz902fPg==, tarball: https://registry.npmmirror.com/component-delegate/-/component-delegate-0.2.4.tgz}
-    dependencies:
-      component-closest: 0.1.4
-      component-event: 0.1.4
-    dev: false
-
   /component-emitter@1.3.1:
     resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==, tarball: https://registry.npmmirror.com/component-emitter/-/component-emitter-1.3.1.tgz}
     dev: true
 
-  /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}
 
-  /component-indexof@0.0.3:
-    resolution: {integrity: sha512-puDQKvx/64HZXb4hBwIcvQLaLgux8o1CbWl39s41hrIIZDl1lJiD5jc22gj3RBeGK0ovxALDYpIbyjqDUUl0rw==, tarball: https://registry.npmmirror.com/component-indexof/-/component-indexof-0.0.3.tgz}
-    dev: false
-
-  /component-matches-selector@0.1.7:
-    resolution: {integrity: sha512-Yb2+pVBvrqkQVpPaDBF0DYXRreBveXJNrpJs9FnFu8PF6/5IIcz5oDZqiH9nB5hbD2/TmFVN5ZCxBzqu7yFFYQ==, tarball: https://registry.npmmirror.com/component-matches-selector/-/component-matches-selector-0.1.7.tgz}
-    dependencies:
-      component-query: 0.0.3
-      global-object: 1.0.0
-    dev: false
-
-  /component-query@0.0.3:
-    resolution: {integrity: sha512-VgebQseT1hz1Ps7vVp2uaSg+N/gsI5ts3AZUSnN6GMA2M82JH7o+qYifWhmVE/e8w/H48SJuA3nA9uX8zRe95Q==, tarball: https://registry.npmmirror.com/component-query/-/component-query-0.0.3.tgz}
-    dev: false
-
   /compute-scroll-into-view@1.0.20:
     resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==, tarball: https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz}
     dev: false
@@ -5521,6 +5488,10 @@ packages:
     resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==, tarball: https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz}
     dev: true
 
+  /default-passive-events@2.0.0:
+    resolution: {integrity: sha512-eMtt76GpDVngZQ3ocgvRcNCklUMwID1PaNbCNxfpDXuiOXttSh0HzBbda1HU9SIUsDc02vb7g9+3I5tlqe/qMQ==, tarball: https://registry.npmmirror.com/default-passive-events/-/default-passive-events-2.0.0.tgz}
+    dev: false
+
   /define-data-property@1.1.4:
     resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==, tarball: https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz}
     engines: {node: '>= 0.4'}
@@ -6674,10 +6645,6 @@ packages:
       global-prefix: 3.0.0
     dev: true
 
-  /global-object@1.0.0:
-    resolution: {integrity: sha512-mSPSkY6UsHv6hgW0V2dfWBWTS8TnPnLx3ECVNoWp6rBI2Bg66VYoqGoTFlH/l7XhAZ/l+StYlntXlt87BEeCcg==, tarball: https://registry.npmmirror.com/global-object/-/global-object-1.0.0.tgz}
-    dev: false
-
   /global-prefix@3.0.0:
     resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==, tarball: https://registry.npmmirror.com/global-prefix/-/global-prefix-3.0.0.tgz}
     engines: {node: '>=6'}
@@ -7899,10 +7866,6 @@ packages:
     engines: {node: '>=18'}
     dev: true
 
-  /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}
 
@@ -7912,18 +7875,6 @@ packages:
       dom-walk: 0.1.2
     dev: false
 
-  /min-dom@0.2.0:
-    resolution: {integrity: sha512-VmxugbnAcVZGqvepjhOA4d4apmrpX8mMaRS+/jo0dI5Yorzrr4Ru9zc9KVALlY/+XakVCb8iQ+PYXljihQcsNw==, tarball: https://registry.npmmirror.com/min-dom/-/min-dom-0.2.0.tgz}
-    dependencies:
-      component-classes: 1.2.6
-      component-closest: 0.1.4
-      component-delegate: 0.2.4
-      component-event: 0.1.4
-      component-matches-selector: 0.1.7
-      component-query: 0.0.3
-      domify: 1.4.2
-    dev: false
-
   /min-dom@4.2.1:
     resolution: {integrity: sha512-TMoL8SEEIhUWYgkj7XMSgxmwSyGI+4fP2KFFGnN3FbHfbGHVdsLYSz8LoIsgPhz4dWRmLvxWWSMgzZMJW5sZuA==, tarball: https://registry.npmmirror.com/min-dom/-/min-dom-4.2.1.tgz}
     dependencies:
@@ -8714,6 +8665,10 @@ packages:
     resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==, tarball: https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz}
     dev: true
 
+  /randomcolor@0.6.2:
+    resolution: {integrity: sha512-Mn6TbyYpFgwFuQ8KJKqf3bqqY9O1y37/0jgSK/61PUxV4QfIMv0+K2ioq8DfOjkBslcjwSzRfIDEXfzA9aCx7A==, tarball: https://registry.npmmirror.com/randomcolor/-/randomcolor-0.6.2.tgz}
+    dev: false
+
   /rd@2.0.1:
     resolution: {integrity: sha512-/XdKU4UazUZTXFmI0dpABt8jSXPWcEyaGdk340KdHnsEOdkTctlX23aAK7ChQDn39YGNlAJr1M5uvaKt4QnpNw==, tarball: https://registry.npmmirror.com/rd/-/rd-2.0.1.tgz}
     dependencies:
@@ -9128,6 +9083,10 @@ packages:
     engines: {node: '>=14'}
     dev: true
 
+  /signature_pad@3.0.0-beta.4:
+    resolution: {integrity: sha512-cOf2NhVuTiuNqe2X/ycEmizvCDXk0DoemhsEpnkcGnA4kS5iJYTCqZ9As7tFBbsch45Q1EdX61833+6sjJ8rrw==, tarball: https://registry.npmmirror.com/signature_pad/-/signature_pad-3.0.0-beta.4.tgz}
+    dev: false
+
   /sirv@2.0.4:
     resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==, tarball: https://registry.npmmirror.com/sirv/-/sirv-2.0.4.tgz}
     engines: {node: '>= 10'}
@@ -9561,10 +9520,6 @@ packages:
     resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==, tarball: https://registry.npmmirror.com/svg-tags/-/svg-tags-1.0.0.tgz}
     dev: true
 
-  /svg.js@2.7.1:
-    resolution: {integrity: sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==, tarball: https://registry.npmmirror.com/svg.js/-/svg.js-2.7.1.tgz}
-    dev: false
-
   /svgo@2.8.0:
     resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==, tarball: https://registry.npmmirror.com/svgo/-/svgo-2.8.0.tgz}
     engines: {node: '>=10.13.0'}
@@ -10324,6 +10279,16 @@ packages:
       vue: 3.5.12(typescript@5.3.3)
     dev: false
 
+  /vue3-signature@0.2.4(vue@3.5.12):
+    resolution: {integrity: sha512-XFwwFVK9OG3F085pKIq2SlNVqx32WdFH+TXbGEWc5FfEKpx8oMmZuAwZZ50K/pH2FgmJSE8IRwU9DDhrLpd6iA==, tarball: https://registry.npmmirror.com/vue3-signature/-/vue3-signature-0.2.4.tgz}
+    peerDependencies:
+      vue: ^3.2.0
+    dependencies:
+      default-passive-events: 2.0.0
+      signature_pad: 3.0.0-beta.4
+      vue: 3.5.12(typescript@5.3.3)
+    dev: false
+
   /vue@3.5.12(typescript@5.3.3):
     resolution: {integrity: sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==, tarball: https://registry.npmmirror.com/vue/-/vue-3.5.12.tgz}
     peerDependencies:

+ 4 - 0
src/api/bpm/model/index.ts

@@ -72,3 +72,7 @@ export const deleteModel = async (id: number) => {
 export const deployModel = async (id: number) => {
   return await request.post({ url: '/bpm/model/deploy?id=' + id })
 }
+
+export const cleanModel = async (id: number) => {
+  return await request.delete({ url: '/bpm/model/clean?id=' + id })
+}

+ 2 - 1
src/api/bpm/processInstance/index.ts

@@ -36,6 +36,7 @@ export type ApprovalTaskInfo = {
   assigneeUser: User
   status: number
   reason: string
+  signPicUrl: string
 }
 
 // 审批节点信息
@@ -89,7 +90,7 @@ export const getProcessInstanceCopyPage = async (params: any) => {
 
 // 获取审批详情
 export const getApprovalDetail = async (params: any) => {
-  return await request.get({ url: 'bpm/process-instance/get-approval-detail' , params })
+  return await request.get({ url: 'bpm/process-instance/get-approval-detail', params })
 }
 
 // 获取表单字段权限

+ 65 - 13
src/components/SimpleProcessDesignerV2/src/NodeHandler.vue

@@ -40,13 +40,24 @@
             <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 class="handler-item-icon delay">
+              <span class="iconfont icon-size icon-delay"></span>
             </div>
             <div class="handler-item-text">延迟器</div>
           </div>
-        </div>
+          <div class="handler-item" @click="addNode(NodeType.ROUTER_BRANCH_NODE)">
+            <div class="handler-item-icon router">
+              <span class="iconfont icon-size icon-router"></span>
+            </div>
+            <div class="handler-item-text">路由分支</div>
+          </div>
+          <div class="handler-item" @click="addNode(NodeType.TRIGGER_NODE)">
+            <div class="handler-item-icon trigger">
+              <span class="iconfont icon-size icon-trigger"></span>
+            </div>
+            <div class="handler-item-text">触发器</div>
+          </div>
+        </div> 
         <template #reference>
           <div class="add-icon"><Icon icon="ep:plus" /></div>
         </template>
@@ -60,12 +71,14 @@ import {
   ApproveMethodType,
   AssignEmptyHandlerType,
   AssignStartUserHandlerType,
+  ConditionType,
   NODE_DEFAULT_NAME,
   NodeType,
   RejectHandlerType,
-  SimpleFlowNode
+  SimpleFlowNode,
+  DEFAULT_CONDITION_GROUP_VALUE
 } from './consts'
-import { generateUUID } from '@/utils'
+import {generateUUID} from '@/utils'
 
 defineOptions({
   name: 'NodeHandler'
@@ -120,7 +133,16 @@ const addNode = (type: number) => {
         type: AssignEmptyHandlerType.APPROVE
       },
       assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT,
-      childNode: props.childNode
+      childNode: props.childNode,
+      taskCreateListener: {
+        enable: false
+      },
+      taskAssignListener: {
+        enable: false
+      },
+      taskCompleteListener: {
+        enable: false
+      }
     }
     emits('update:childNode', data)
   }
@@ -147,8 +169,11 @@ const addNode = (type: number) => {
           showText: '',
           type: NodeType.CONDITION_NODE,
           childNode: undefined,
-          conditionType: 1,
-          defaultFlow: false
+          conditionSetting: {
+            defaultFlow: false,
+            conditionType: ConditionType.RULE,
+            conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
+          }
         },
         {
           id: 'Flow_' + generateUUID(),
@@ -156,8 +181,9 @@ const addNode = (type: number) => {
           showText: '未满足其它条件时,将进入此分支',
           type: NodeType.CONDITION_NODE,
           childNode: undefined,
-          conditionType: undefined,
-          defaultFlow: true
+          conditionSetting: {
+            defaultFlow: true
+          }
         }
       ]
     }
@@ -201,7 +227,11 @@ const addNode = (type: number) => {
           showText: '',
           type: NodeType.CONDITION_NODE,
           childNode: undefined,
-          defaultFlow: false
+          conditionSetting: {
+            defaultFlow: false,
+            conditionType: ConditionType.RULE,
+            conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
+          }
         },
         {
           id: 'Flow_' + generateUUID(),
@@ -209,7 +239,9 @@ const addNode = (type: number) => {
           showText: '未满足其它条件时,将进入此分支',
           type: NodeType.CONDITION_NODE,
           childNode: undefined,
-          defaultFlow: true
+          conditionSetting: {
+            defaultFlow: true
+          }
         }
       ]
     }
@@ -225,6 +257,26 @@ const addNode = (type: number) => {
     }
     emits('update:childNode', data)
   }
+  if (type === NodeType.ROUTER_BRANCH_NODE) {
+    const data: SimpleFlowNode = {
+      id: 'GateWay_' + generateUUID(),
+      name: NODE_DEFAULT_NAME.get(NodeType.ROUTER_BRANCH_NODE) as string,
+      showText: '',
+      type: NodeType.ROUTER_BRANCH_NODE,
+      childNode: props.childNode
+    }
+    emits('update:childNode', data)
+  }
+  if (type === NodeType.TRIGGER_NODE) {
+    const data: SimpleFlowNode = {
+      id: 'Activity_' + generateUUID(),
+      name: NODE_DEFAULT_NAME.get(NodeType.TRIGGER_NODE) as string,
+      showText: '',
+      type: NodeType.TRIGGER_NODE,
+      childNode: props.childNode
+    }
+    emits('update:childNode', data)
+  }
 }
 </script>
 

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

@@ -44,6 +44,18 @@
     :flow-node="currentNode"
     @update:flow-node="handleModelValueUpdate"
   />
+  <!-- 路由分支节点 -->
+  <RouterNode
+    v-if="currentNode && currentNode.type === NodeType.ROUTER_BRANCH_NODE"
+    :flow-node="currentNode"
+    @update:flow-node="handleModelValueUpdate"
+  />
+   <!-- 触发器节点 -->
+   <TriggerNode
+    v-if="currentNode && currentNode.type === NodeType.TRIGGER_NODE"
+    :flow-node="currentNode"
+    @update:flow-node="handleModelValueUpdate"
+  />
   <!-- 递归显示孩子节点  -->
   <ProcessNodeTree
     v-if="currentNode && currentNode.childNode"
@@ -67,6 +79,8 @@ 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 RouterNode from './nodes/RouterNode.vue'
+import TriggerNode from './nodes/TriggerNode.vue'
 import { SimpleFlowNode, NodeType } from './consts'
 import { useWatchNode } from './node'
 defineOptions({

+ 10 - 114
src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue

@@ -40,7 +40,7 @@ defineOptions({
   name: 'SimpleProcessDesigner'
 })
 
-const emits = defineEmits(['success', 'init-finished']) // 保存成功事件
+const emits = defineEmits(['success']) // 保存成功事件
 
 const props = defineProps({
   modelId: {
@@ -56,16 +56,13 @@ const props = defineProps({
     required: false
   },
   // 可发起流程的人员编号
-  startUserIds : {
+  startUserIds: {
     type: Array,
     required: false
-  },
-  value: {
-    type: [String, Object],
-    required: false
   }
 })
 
+const processData = inject('processData') as Ref
 const loading = ref(false)
 const formFields = ref<string[]>([])
 const formType = ref(20)
@@ -76,9 +73,6 @@ 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)
@@ -88,9 +82,11 @@ provide('deptList', deptOptions)
 provide('userGroupList', userGroupOptions)
 provide('deptTree', deptTreeOptions)
 provide('startUserIds', props.startUserIds)
-
+provide('tasks', [])
+provide('processInstance', {})
 const message = useMessage() // 国际化
 const processNodeTree = ref<SimpleFlowNode | undefined>()
+provide('processNodeTree', processNodeTree)
 const errorDialogVisible = ref(false)
 let errorNodes: SimpleFlowNode[] = []
 
@@ -112,70 +108,13 @@ const updateModel = () => {
   }
 }
 
-// 加载流程数据
-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) {
     return
   }
 
-  // 校验节点
-  errorNodes = []
-  validateNode(simpleModelNode, errorNodes)
-  if (errorNodes.length > 0) {
-    errorDialogVisible.value = true
-    return
-  }
-
   try {
-    if (props.modelId) {
-      // 编辑模式
-      const data = {
-        id: props.modelId,
-        simpleModel: simpleModelNode
-      }
-      await updateBpmSimpleModel(data)
-    }
-    // 无论是编辑还是新建模式,都更新当前值并触发事件
-    currentValue.value = simpleModelNode
+    processData.value = simpleModelNode
     emits('success', simpleModelNode)
   } catch (error) {
     console.error('保存失败:', error)
@@ -246,61 +185,18 @@ onMounted(async () => {
     deptTreeOptions.value = handleTree(deptOptions.value as DeptApi.DeptVO[], 'id')
     // 获取用户组列表
     userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
-
     // 加载流程数据
-    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)
+    if (processData.value) {
+      processNodeTree.value = processData?.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
-})
+defineExpose({})
 </script>

+ 51 - 1
src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue

@@ -3,6 +3,22 @@
     <div class="position-absolute top-0px right-0px bg-#fff">
       <el-row type="flex" justify="end">
         <el-button-group key="scale-control" size="default">
+          <el-button v-if="!readonly" size="default" @click="exportJson">
+            <Icon icon="ep:download" /> 导出
+          </el-button>
+          <el-button v-if="!readonly" size="default" @click="importJson">
+            <Icon icon="ep:upload" />导入
+          </el-button>
+          <!-- 用于打开本地文件-->
+          <input
+            v-if="!readonly"
+            type="file"
+            id="files"
+            ref="refFile"
+            style="display: none"
+            accept=".json"
+            @change="importLocalFile"
+          />
           <el-button size="default" :icon="ScaleToOriginal" @click="processReZoom()" />
           <el-button size="default" :plain="true" :icon="ZoomOut" @click="zoomOut()" />
           <el-button size="default" class="w-80px"> {{ scaleValue }}% </el-button>
@@ -34,6 +50,8 @@ import ProcessNodeTree from './ProcessNodeTree.vue'
 import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from './consts'
 import { useWatchNode } from './node'
 import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue'
+import { isString } from '@/utils/is'
+import download from '@/utils/download'
 
 defineOptions({
   name: 'SimpleProcessModel'
@@ -52,7 +70,7 @@ const props = defineProps({
 })
 
 const emits = defineEmits<{
-  'save': [node: SimpleFlowNode | undefined]
+  save: [node: SimpleFlowNode | undefined]
 }>()
 
 const processNodeTree = useWatchNode(props)
@@ -85,6 +103,16 @@ const processReZoom = () => {
 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) {
@@ -143,6 +171,28 @@ const getCurrentFlowData = async () => {
 defineExpose({
   getCurrentFlowData
 })
+
+/** 导出 JSON */
+const exportJson = () => {
+  download.json(new Blob([JSON.stringify(processNodeTree.value)]), 'model.json')
+}
+
+/** 导入 JSON */
+const refFile = ref()
+const importJson = () => {
+  refFile.value.click()
+}
+const importLocalFile = () => {
+  const file = refFile.value.files[0]
+  const reader = new FileReader()
+  reader.readAsText(file)
+  reader.onload = function () {
+    if (isString(this.result)) {
+      processNodeTree.value = JSON.parse(this.result)
+      emits('save', processNodeTree.value)
+    }
+  }
+}
 </script>
 
 <style lang="scss" scoped></style>

+ 154 - 14
src/components/SimpleProcessDesignerV2/src/consts.ts

@@ -28,6 +28,11 @@ export enum NodeType {
    */
   DELAY_TIMER_NODE = 14,
 
+  /**
+   * 触发器节点
+   */
+  TRIGGER_NODE = 15,
+
   /**
    * 条件节点
    */
@@ -44,7 +49,11 @@ export enum NodeType {
   /**
    * 包容分支节点 (对应包容网关)
    */
-  INCLUSIVE_BRANCH_NODE = 53
+  INCLUSIVE_BRANCH_NODE = 53,
+  /**
+   * 路由分支节点
+   */
+  ROUTER_BRANCH_NODE = 54
 }
 
 export enum NodeId {
@@ -93,18 +102,27 @@ export interface SimpleFlowNode {
   assignEmptyHandler?: AssignEmptyHandler
   // 审批节点的审批人与发起人相同时,对应的处理类型
   assignStartUserHandlerType?: number
-  // 条件类型
-  conditionType?: ConditionType
-  // 条件表达式
-  conditionExpression?: string
-  // 条件组
-  conditionGroups?: ConditionGroup
-  // 是否默认的条件
-  defaultFlow?: boolean
+  // 创建任务监听器
+  taskCreateListener?: ListenerHandler
+  // 创建任务监听器
+  taskAssignListener?: ListenerHandler
+  // 创建任务监听器
+  taskCompleteListener?: ListenerHandler
+  // 条件设置
+  conditionSetting?: ConditionSetting
   // 活动的状态,用于前端节点状态展示
   activityStatus?: TaskStatusEnum
   // 延迟设置
   delaySetting?: DelaySetting
+  // 路由分支
+  routerGroups?: RouterSetting[]
+  defaultFlowId?: string
+  // 签名
+  signEnable?: boolean
+  // 审批意见
+  reasonRequire?: boolean
+  // 触发器设置
+  triggerSetting?: TriggerSetting
 }
 // 候选人策略枚举 ( 用于审批节点。抄送节点 )
 export enum CandidateStrategy {
@@ -222,6 +240,41 @@ export type AssignEmptyHandler = {
   userIds?: number[]
 }
 
+/**
+ * 监听器的结构定义
+ */
+export type ListenerHandler = {
+  enable: boolean
+  path?: string
+  header?: ListenerParam[]
+  body?: ListenerParam[]
+}
+export type ListenerParam = {
+  key: string
+  type: number
+  value: string
+}
+export enum ListenerParamTypeEnum {
+  /**
+   * 固定值
+   */
+  FIXED_VALUE = 1,
+  /**
+   * 表单
+   */
+  FROM_FORM = 2
+}
+export const LISTENER_MAP_TYPES = [
+  {
+    value: 1,
+    label: '固定值'
+  },
+  {
+    value: 2,
+    label: '表单'
+  }
+]
+
 // 审批拒绝类型枚举
 export enum RejectHandlerType {
   /**
@@ -315,6 +368,20 @@ export enum TimeUnitType {
   DAY = 3
 }
 
+/**
+ * 条件节点设置结构定义,用于条件节点
+ */
+export type ConditionSetting =  {
+  // 条件类型
+  conditionType?: ConditionType,
+  // 条件表达式
+  conditionExpression?: string,
+  // 条件组
+  conditionGroups?: ConditionGroup,
+  // 是否默认的条件
+  defaultFlow?: boolean
+}
+
 // 条件配置类型 ( 用于条件节点配置 )
 export enum ConditionType {
   /**
@@ -389,8 +456,6 @@ export enum OperationButtonType {
  * 条件规则结构定义
  */
 export type ConditionRule = {
-  type: number
-  opName: string
   opCode: string
   leftSide: string
   rightSide: string
@@ -405,6 +470,24 @@ export type ConditionGroup = {
   // 条件数组
   conditions: Condition[]
 }
+/**
+ * 条件组默认值
+ */
+export const DEFAULT_CONDITION_GROUP_VALUE = {
+  and: true,
+  conditions: [
+    {
+      and: true,
+      rules: [
+        {
+          opCode: '==',
+          leftSide: '',
+          rightSide: ''
+        }
+      ]
+    }
+  ]
+}
 
 /**
  * 条件结构定义
@@ -421,6 +504,8 @@ 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, '请设置延迟器')
+NODE_DEFAULT_TEXT.set(NodeType.ROUTER_BRANCH_NODE, '请设置路由节点')
+NODE_DEFAULT_TEXT.set(NodeType.TRIGGER_NODE, '请设置触发器')
 
 export const NODE_DEFAULT_NAME = new Map<number, string>()
 NODE_DEFAULT_NAME.set(NodeType.USER_TASK_NODE, '审批人')
@@ -428,6 +513,8 @@ 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, '延迟器')
+NODE_DEFAULT_NAME.set(NodeType.ROUTER_BRANCH_NODE, '路由分支')
+NODE_DEFAULT_NAME.set(NodeType.TRIGGER_NODE, '触发器')
 
 // 候选人策略。暂时不从字典中取。 后续可能调整。控制显示顺序
 export const CANDIDATE_STRATEGY: DictDataVO[] = [
@@ -460,8 +547,8 @@ export const APPROVE_METHODS: DictDataVO[] = [
 ]
 
 export const CONDITION_CONFIG_TYPES: DictDataVO[] = [
-  { label: '条件表达式', value: ConditionType.EXPRESSION },
-  { label: '条件规则', value: ConditionType.RULE }
+  { label: '条件规则', value: ConditionType.RULE },
+  { label: '条件表达式', value: ConditionType.EXPRESSION }
 ]
 
 // 时间单位类型
@@ -575,7 +662,15 @@ export enum ProcessVariableEnum {
   /**
    * 发起用户 ID
    */
-  START_USER_ID = 'PROCESS_START_USER_ID'
+  START_USER_ID = 'PROCESS_START_USER_ID',
+  /**
+   * 发起时间
+   */
+  START_TIME = 'PROCESS_START_TIME',
+  /**
+   * 流程定义名称
+   */
+  PROCESS_DEFINITION_NAME = 'PROCESS_DEFINITION_NAME'
 }
 
 /**
@@ -604,3 +699,48 @@ export const DELAY_TYPE = [
   { label: '固定时长', value: DelayTypeEnum.FIXED_TIME_DURATION },
   { label: '固定日期', value: DelayTypeEnum.FIXED_DATE_TIME }
 ]
+
+/**
+ * 路由分支结构定义
+ */
+export type RouterSetting = {
+  nodeId: string
+  conditionType: ConditionType
+  conditionExpression: string
+  conditionGroups: ConditionGroup
+}
+
+// ==================== 触发器相关定义 ==================== 
+/**
+ * 触发器节点结构定义
+ */
+export type TriggerSetting = {
+  type: TriggerTypeEnum
+  httpRequestSetting: HttpRequestTriggerSetting
+}
+
+/**
+ * 触发器类型枚举
+ */
+export enum TriggerTypeEnum {
+  /**
+   * 发送 HTTP 请求触发器
+   */
+  HTTP_REQUEST = 1,
+}
+
+/**
+ * HTTP 请求触发器结构定义
+ */
+export type HttpRequestTriggerSetting = {
+  // 请求 URL
+  url: string
+  // 请求头参数设置
+  header?: ListenerParam[] // TODO 需要重命名一下
+  // 请求体参数设置
+  body?: ListenerParam[]
+}
+
+export const TRIGGER_TYPES: DictDataVO[] = [
+  { label: 'HTTP 请求', value: TriggerTypeEnum.HTTP_REQUEST }
+]

+ 34 - 15
src/components/SimpleProcessDesignerV2/src/node.ts

@@ -1,4 +1,3 @@
-import { cloneDeep } from 'lodash-es'
 import { TaskStatusEnum } from '@/api/bpm/task'
 import * as RoleApi from '@/api/system/role'
 import * as DeptApi from '@/api/system/dept'
@@ -14,9 +13,11 @@ import {
   NODE_DEFAULT_NAME,
   AssignStartUserHandlerType,
   AssignEmptyHandlerType,
-  FieldPermissionType
+  FieldPermissionType,
+  ListenerParam
 } from './consts'
-import { parseFormFields } from '@/components/FormCreate/src/utils/index'
+import { parseFormFields } from '@/components/FormCreate/src/utils'
+
 export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> {
   const node = ref<SimpleFlowNode>(props.flowNode)
   watch(
@@ -46,9 +47,9 @@ export function useFormFieldsPermission(defaultPermission: FieldPermissionType)
   // 字段权限配置. 需要有 field, title,  permissioin 属性
   const fieldsPermissionConfig = ref<Array<Record<string, any>>>([])
 
-  const formType = inject<Ref<number>>('formType') // 表单类型
+  const formType = inject<Ref<number | undefined>>('formType', ref()) // 表单类型
 
-  const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
+  const formFields = inject<Ref<string[]>>('formFields', ref([])) // 流程表单字段
 
   const getNodeConfigFormFields = (nodeFormFields?: Array<Record<string, string>>) => {
     nodeFormFields = toRaw(nodeFormFields)
@@ -108,12 +109,11 @@ export function useFormFieldsPermission(defaultPermission: FieldPermissionType)
  * @description 获取表单的字段
  */
 export function useFormFields() {
-  const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
+  const formFields = inject<Ref<string[]>>('formFields', ref([])) // 流程表单字段
   return parseFormCreateFields(unref(formFields))
 }
 
 export type UserTaskFormType = {
-  //candidateParamArray: any[]
   candidateStrategy: CandidateStrategy
   approveMethod: ApproveMethodType
   roleIds?: number[] // 角色
@@ -136,10 +136,29 @@ export type UserTaskFormType = {
   timeDuration?: number
   maxRemindCount?: number
   buttonsSetting: any[]
+  taskCreateListenerEnable?: boolean
+  taskCreateListenerPath?: string
+  taskCreateListener?: {
+    header: ListenerParam[],
+    body: ListenerParam[]
+  }
+  taskAssignListenerEnable?: boolean
+  taskAssignListenerPath?: string
+  taskAssignListener?: {
+    header: ListenerParam[],
+    body: ListenerParam[]
+  }
+  taskCompleteListenerEnable?: boolean
+  taskCompleteListenerPath?: string
+  taskCompleteListener?:{
+    header: ListenerParam[],
+    body: ListenerParam[]
+  }
+  signEnable: boolean
+  reasonRequire: boolean
 }
 
 export type CopyTaskFormType = {
-  // candidateParamArray: any[]
   candidateStrategy: CandidateStrategy
   roleIds?: number[] // 角色
   deptIds?: number[] // 部门
@@ -156,13 +175,13 @@ export type CopyTaskFormType = {
  * @description 节点表单数据。 用于审批节点、抄送节点
  */
 export function useNodeForm(nodeType: NodeType) {
-  const roleOptions = inject<Ref<RoleApi.RoleVO[]>>('roleList') // 角色列表
-  const postOptions = inject<Ref<PostApi.PostVO[]>>('postList') // 岗位列表
-  const userOptions = inject<Ref<UserApi.UserVO[]>>('userList') // 用户列表
-  const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList') // 部门列表
-  const userGroupOptions = inject<Ref<UserGroupApi.UserGroupVO[]>>('userGroupList') // 用户组列表
-  const deptTreeOptions = inject('deptTree') // 部门树
-  const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
+  const roleOptions = inject<Ref<RoleApi.RoleVO[]>>('roleList', ref([])) // 角色列表
+  const postOptions = inject<Ref<PostApi.PostVO[]>>('postList', ref([])) // 岗位列表
+  const userOptions = inject<Ref<UserApi.UserVO[]>>('userList', ref([])) // 用户列表
+  const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList', ref([])) // 部门列表
+  const userGroupOptions = inject<Ref<UserGroupApi.UserGroupVO[]>>('userGroupList', ref([])) // 用户组列表
+  const deptTreeOptions = inject('deptTree', ref()) // 部门树
+  const formFields = inject<Ref<string[]>>('formFields', ref([])) // 流程表单字段
   const configForm = ref<UserTaskFormType | CopyTaskFormType>()
   if (nodeType === NodeType.USER_TASK_NODE) {
     configForm.value = {

+ 25 - 214
src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue

@@ -26,121 +26,11 @@
       </div>
     </template>
     <div>
-      <div class="mb-3 font-size-16px" v-if="currentNode.defaultFlow"
+      <div class="mb-3 font-size-16px" v-if="currentNode.conditionSetting?.defaultFlow"
         >未满足其它条件时,将进入此分支(该分支不可编辑和删除)</div
       >
       <div v-else>
-        <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
-                v-for="(dict, index) in conditionConfigTypes"
-                :key="index"
-                :value="dict.value"
-                :label="dict.value"
-              >
-                {{ dict.label }}
-              </el-radio>
-            </el-radio-group>
-          </el-form-item>
-
-          <el-form-item
-            v-if="currentNode.conditionType === 1"
-            label="条件表达式"
-            prop="conditionExpression"
-          >
-            <el-input
-              type="textarea"
-              v-model="currentNode.conditionExpression"
-              clearable
-              style="width: 100%"
-            />
-          </el-form-item>
-          <el-form-item v-if="currentNode.conditionType === 2" label="条件规则">
-            <div class="condition-group-tool">
-              <div class="flex items-center">
-                <div class="mr-4">条件组关系</div>
-                <el-switch
-                  v-model="conditionGroups.and"
-                  inline-prompt
-                  active-text="且"
-                  inactive-text="或"
-                />
-              </div>
-            </div>
-            <el-space direction="vertical" :spacer="conditionGroups.and ? '且' : '或'">
-              <el-card
-                class="condition-group"
-                style="width: 530px"
-                v-for="(condition, cIdx) in conditionGroups.conditions"
-                :key="cIdx"
-              >
-                <div class="condition-group-delete" v-if="conditionGroups.conditions.length > 1">
-                  <Icon
-                    color="#0089ff"
-                    icon="ep:circle-close-filled"
-                    :size="18"
-                    @click="deleteConditionGroup(cIdx)"
-                  />
-                </div>
-                <template #header>
-                  <div class="flex items-center justify-between">
-                    <div>条件组</div>
-                    <div class="flex">
-                      <div class="mr-4">规则关系</div>
-                      <el-switch
-                        v-model="condition.and"
-                        inline-prompt
-                        active-text="且"
-                        inactive-text="或"
-                      />
-                    </div>
-                  </div>
-                </template>
-
-                <div class="flex pt-2" v-for="(rule, rIdx) in condition.rules" :key="rIdx">
-                  <div class="mr-2">
-                    <el-select style="width: 160px" v-model="rule.leftSide">
-                      <el-option
-                        v-for="(item, index) in fieldOptions"
-                        :key="index"
-                        :label="item.title"
-                        :value="item.field"
-                        :disabled="!item.required"
-                      />
-                    </el-select>
-                  </div>
-                  <div class="mr-2">
-                    <el-select v-model="rule.opCode" style="width: 100px">
-                      <el-option
-                        v-for="item in COMPARISON_OPERATORS"
-                        :key="item.value"
-                        :label="item.label"
-                        :value="item.value"
-                      />
-                    </el-select>
-                  </div>
-                  <div class="mr-2">
-                    <el-input v-model="rule.rightSide" style="width: 160px" />
-                  </div>
-                  <div class="mr-1 flex items-center" v-if="condition.rules.length > 1">
-                    <Icon
-                      icon="ep:delete"
-                      :size="18"
-                      @click="deleteConditionRule(condition, rIdx)"
-                    />
-                  </div>
-                  <div class="flex items-center">
-                    <Icon icon="ep:plus" :size="18" @click="addConditionRule(condition, rIdx)" />
-                  </div>
-                </div>
-              </el-card>
-            </el-space>
-            <div title="添加条件组" class="mt-4 cursor-pointer">
-              <Icon color="#0089ff" icon="ep:plus" :size="24" @click="addConditionGroup" />
-            </div>
-          </el-form-item>
-        </el-form>
+        <Condition ref="conditionRef" v-model="condition" />
       </div>
     </div>
     <template #footer>
@@ -155,33 +45,17 @@
 <script setup lang="ts">
 import {
   SimpleFlowNode,
-  CONDITION_CONFIG_TYPES,
   ConditionType,
   COMPARISON_OPERATORS,
-  ConditionGroup,
-  Condition,
-  ConditionRule,
   ProcessVariableEnum
 } from '../consts'
 import { getDefaultConditionNodeName } from '../utils'
 import { useFormFields } from '../node'
-import { BpmModelFormType } from '@/utils/constants'
+import Condition from './components/Condition.vue'
 const message = useMessage() // 消息弹窗
 defineOptions({
   name: 'ConditionNodeConfig'
 })
-const formType = inject<Ref<number>>('formType') // 表单类型
-const conditionConfigTypes = computed(() => {
-  return CONDITION_CONFIG_TYPES.filter((item) => {
-    // 业务表单暂时去掉条件规则选项
-    if (formType?.value === BpmModelFormType.CUSTOM && item.value === ConditionType.RULE) {
-      return false
-    } else {
-      return true
-    }
-  })
-})
-
 const props = defineProps({
   conditionNode: {
     type: Object as () => SimpleFlowNode,
@@ -193,12 +67,10 @@ const props = defineProps({
   }
 })
 const settingVisible = ref(false)
+const currentNode = ref<SimpleFlowNode>(props.conditionNode)
+const condition = ref<any>()
 const open = () => {
-  if (currentNode.value.conditionType === ConditionType.RULE) {
-    if (currentNode.value.conditionGroups) {
-      conditionGroups.value = currentNode.value.conditionGroups
-    }
-  }
+  condition.value = currentNode.value.conditionSetting
   settingVisible.value = true
 }
 
@@ -219,10 +91,10 @@ const blurEvent = () => {
   showInput.value = false
   currentNode.value.name =
     currentNode.value.name ||
-    getDefaultConditionNodeName(props.nodeIndex, currentNode.value?.defaultFlow)
+    getDefaultConditionNodeName(props.nodeIndex, currentNode.value?.conditionSetting?.defaultFlow)
 }
 
-const currentNode = ref<SimpleFlowNode>(props.conditionNode)
+
 
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
@@ -239,31 +111,27 @@ const handleClose = async (done: (cancel?: boolean) => void) => {
     done()
   }
 }
-// 表单校验规则
-const formRules = reactive({
-  conditionType: [{ required: true, message: '配置方式不能为空', trigger: 'blur' }],
-  conditionExpression: [{ required: true, message: '条件表达式不能为空', trigger: 'blur' }]
-})
-const formRef = ref() // 表单 Ref
 
+const conditionRef = ref()
 // 保存配置
 const saveConfig = async () => {
-  if (!currentNode.value.defaultFlow) {
+  if (!currentNode.value.conditionSetting?.defaultFlow) {
     // 校验表单
-    if (!formRef) return false
-    const valid = await formRef.value.validate()
+    const valid = await conditionRef.value.validate()
     if (!valid) return false
     const showText = getShowText()
     if (!showText) {
       return false
     }
     currentNode.value.showText = showText
-    if (currentNode.value.conditionType === ConditionType.EXPRESSION) {
-      currentNode.value.conditionGroups = undefined
+    currentNode.value.conditionSetting!.conditionType = condition.value?.conditionType
+    if (currentNode.value.conditionSetting?.conditionType === ConditionType.EXPRESSION) {
+      currentNode.value.conditionSetting.conditionGroups = undefined
+      currentNode.value.conditionSetting.conditionExpression = condition.value?.conditionExpression
     }
-    if (currentNode.value.conditionType === ConditionType.RULE) {
-      currentNode.value.conditionExpression = undefined
-      currentNode.value.conditionGroups = conditionGroups.value
+    if (currentNode.value.conditionSetting!.conditionType === ConditionType.RULE) {
+      currentNode.value.conditionSetting!.conditionExpression = undefined
+      currentNode.value.conditionSetting!.conditionGroups = condition.value?.conditionGroups
     }
   }
   settingVisible.value = false
@@ -271,16 +139,16 @@ const saveConfig = async () => {
 }
 const getShowText = (): string => {
   let showText = ''
-  if (currentNode.value.conditionType === ConditionType.EXPRESSION) {
-    if (currentNode.value.conditionExpression) {
-      showText = `表达式:${currentNode.value.conditionExpression}`
+  if (condition.value?.conditionType === ConditionType.EXPRESSION) {
+    if (condition.value.conditionExpression) {
+      showText = `表达式:${condition.value.conditionExpression}`
     }
   }
-  if (currentNode.value.conditionType === ConditionType.RULE) {
+  if (condition.value?.conditionType === ConditionType.RULE) {
     // 条件组是否为与关系
-    const groupAnd = conditionGroups.value.and
+    const groupAnd = condition.value.conditionGroups?.and
     let warningMesg: undefined | string = undefined
-    const conditionGroup = conditionGroups.value.conditions.map((item) => {
+    const conditionGroup = condition.value.conditionGroups?.conditions.map((item) => {
       return (
         '(' +
         item.rules
@@ -303,70 +171,13 @@ const getShowText = (): string => {
       message.warning(warningMesg)
       showText = ''
     } else {
-      showText = conditionGroup.join(groupAnd ? ' 且 ' : ' 或 ')
+      showText = conditionGroup!.join(groupAnd ? ' 且 ' : ' 或 ')
     }
   }
   return showText
 }
 
-// 改变条件配置方式
-const changeConditionType = () => {}
-
-const conditionGroups = ref<ConditionGroup>({
-  and: true,
-  conditions: [
-    {
-      and: true,
-      rules: [
-        {
-          type: 1,
-          opName: '等于',
-          opCode: '==',
-          leftSide: '',
-          rightSide: ''
-        }
-      ]
-    }
-  ]
-})
-// 添加条件组
-const addConditionGroup = () => {
-  const condition = {
-    and: true,
-    rules: [
-      {
-        type: 1,
-        opName: '等于',
-        opCode: '==',
-        leftSide: '',
-        rightSide: ''
-      }
-    ]
-  }
-  conditionGroups.value.conditions.push(condition)
-}
-// 删除条件组
-const deleteConditionGroup = (idx: number) => {
-  conditionGroups.value.conditions.splice(idx, 1)
-}
-
-// 添加条件规则
-const addConditionRule = (condition: Condition, idx: number) => {
-  const rule: ConditionRule = {
-    type: 1,
-    opName: '等于',
-    opCode: '==',
-    leftSide: '',
-    rightSide: ''
-  }
-  condition.rules.splice(idx + 1, 0, rule)
-}
-
-const deleteConditionRule = (condition: Condition, idx: number) => {
-  condition.rules.splice(idx, 1)
-}
 const fieldsInfo = useFormFields()
-
 /** 条件规则可选择的表单字段 */
 const fieldOptions = computed(() => {
   const fieldsCopy = fieldsInfo.slice()

+ 201 - 0
src/components/SimpleProcessDesignerV2/src/nodes-config/RouterNodeConfig.vue

@@ -0,0 +1,201 @@
+<template>
+  <el-drawer
+    :append-to-body="true"
+    v-model="settingVisible"
+    :show-close="false"
+    :size="630"
+    :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 label-position="top">
+        <el-card class="mb-15px" v-for="(item, index) in routerGroups" :key="index">
+          <template #header>
+            <div class="flex flex-items-center">
+              <el-text size="large">路由{{ index + 1 }}</el-text>
+              <el-select class="ml-15px" v-model="item.nodeId" style="width: 180px">
+                <el-option
+                  v-for="node in nodeOptions"
+                  :key="node.value"
+                  :label="node.label"
+                  :value="node.value"
+                />
+              </el-select>
+              <el-button class="mla" type="danger" link @click="deleteRouterGroup(index)">
+                删除
+              </el-button>
+            </div>
+          </template>
+          <Condition
+            :ref="($event) => (conditionRef[index] = $event)"
+            v-model="routerGroups[index]"
+          />
+        </el-card>
+      </el-form>
+
+      <el-button class="w-1/1" type="primary" :icon="Plus" @click="addRouterGroup">
+        新增路由分支
+      </el-button>
+    </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 { Plus } from '@element-plus/icons-vue'
+import { SimpleFlowNode, NodeType, ConditionType, RouterSetting } from '../consts'
+import { useWatchNode, useDrawer, useNodeName } from '../node'
+import Condition from './components/Condition.vue'
+
+defineOptions({
+  name: 'RouterNodeConfig'
+})
+const message = useMessage() // 消息弹窗
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  }
+})
+const processNodeTree = inject<Ref<SimpleFlowNode>>('processNodeTree')
+// 抽屉配置
+const { settingVisible, closeDrawer, openDrawer } = useDrawer()
+// 当前节点
+const currentNode = useWatchNode(props)
+// 节点名称
+const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.ROUTER_BRANCH_NODE)
+const routerGroups = ref<RouterSetting[]>([])
+const nodeOptions = ref<any>([])
+const conditionRef = ref([])
+
+/** 保存配置 */
+const saveConfig = async () => {
+  // 校验表单
+  let valid = true
+  for (const item of conditionRef.value) {
+    if (item && !(await item.validate())) {
+      valid = false
+    }
+  }
+  if (!valid) return false
+  const showText = getShowText()
+  if (!showText) return false
+  currentNode.value.name = nodeName.value!
+  currentNode.value.showText = showText
+  currentNode.value.routerGroups = routerGroups.value
+  settingVisible.value = false
+  return true
+}
+// 显示路由分支节点配置, 由父组件传过来
+const showRouteNodeConfig = (node: SimpleFlowNode) => {
+  getRouterNode(processNodeTree?.value)
+  routerGroups.value = []
+  nodeName.value = node.name
+  if (node.routerGroups) {
+    routerGroups.value = node.routerGroups
+  }
+}
+
+const getShowText = () => {
+  if (!routerGroups.value || !Array.isArray(routerGroups.value) || routerGroups.value.length <= 0) {
+    message.warning('请配置路由!')
+    return ''
+  }
+  for (const route of routerGroups.value) {
+    if (!route.nodeId || !route.conditionType) {
+      message.warning('请完善路由配置项!')
+      return ''
+    }
+    if (route.conditionType === ConditionType.EXPRESSION && !route.conditionExpression) {
+      message.warning('请完善路由配置项!')
+      return ''
+    }
+    if (route.conditionType === ConditionType.RULE) {
+      for (const condition of route.conditionGroups.conditions) {
+        for (const rule of condition.rules) {
+          if (!rule.leftSide || !rule.rightSide) {
+            message.warning('请完善路由配置项!')
+            return ''
+          }
+        }
+      }
+    }
+  }
+  return `${routerGroups.value.length}条路由分支`
+}
+
+const addRouterGroup = () => {
+  routerGroups.value.push({
+    nodeId: '',
+    conditionType: ConditionType.RULE,
+    conditionExpression: '',
+    conditionGroups: {
+      and: true,
+      conditions: [
+        {
+          and: true,
+          rules: [
+            {
+              opCode: '==',
+              leftSide: '',
+              rightSide: ''
+            }
+          ]
+        }
+      ]
+    }
+  })
+}
+
+const deleteRouterGroup = (index: number) => {
+  routerGroups.value.splice(index, 1)
+}
+
+// 递归获取所有节点
+const getRouterNode = (node) => {
+  // TODO 最好还需要满足以下要求
+  // 并行分支、包容分支内部节点不能跳转到外部节点
+  // 条件分支节点可以向上跳转到外部节点
+  while (true) {
+    if (!node) break
+    if (node.type !== NodeType.ROUTER_BRANCH_NODE && node.type !== NodeType.CONDITION_NODE) {
+      nodeOptions.value.push({
+        label: node.name,
+        value: node.id
+      })
+    }
+    if (!node.childNode || node.type === NodeType.END_EVENT_NODE) {
+      break
+    }
+    if (node.conditionNodes && node.conditionNodes.length) {
+      node.conditionNodes.forEach((item) => {
+        getRouterNode(item)
+      })
+    }
+    node = node.childNode
+  }
+}
+
+defineExpose({ openDrawer, showRouteNodeConfig }) // 暴露方法给父组件
+</script>

+ 141 - 0
src/components/SimpleProcessDesignerV2/src/nodes-config/TriggerNodeConfig.vue

@@ -0,0 +1,141 @@
+<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="type">
+          <el-select v-model="configForm.type">
+            <el-option
+              v-for="(item, index) in TRIGGER_TYPES"
+              :key="index"
+              :value="item.value"
+              :label="item.label"
+            />
+          </el-select>
+        </el-form-item>
+        <div
+          v-if="configForm.type === TriggerTypeEnum.HTTP_REQUEST && configForm.httpRequestSetting"
+        >
+          <el-form-item>
+            <el-alert
+              title="仅支持 POST 请求,以请求体方式接收参数"
+              type="warning"
+              show-icon
+              :closable="false"
+            />
+          </el-form-item>
+          <el-form-item label="请求地址" prop="httpRequestSetting.url">
+            <el-input v-model="configForm.httpRequestSetting.url" />
+          </el-form-item>
+          <HttpRequestParamSetting
+            :header="configForm.httpRequestSetting.header"
+            :body="configForm.httpRequestSetting.body"
+            :bind="'httpRequestSetting'"
+          />
+        </div>
+      </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, TriggerSetting, TRIGGER_TYPES, TriggerTypeEnum } from '../consts'
+import { useWatchNode, useDrawer, useNodeName } from '../node'
+import HttpRequestParamSetting from './components/HttpRequestParamSetting.vue'
+
+defineOptions({
+  name: 'TriggerNodeConfig'
+})
+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.TRIGGER_NODE)
+// 触发器表单配置
+const formRef = ref() // 表单 Ref
+// 表单校验规则
+const formRules = reactive({
+  type: [{ required: true, message: '触发器类型不能为空', trigger: 'change' }],
+  httpRequestSetting: {
+    url: [{ required: true, message: '请求地址不能为空', trigger: 'blur' }]
+  }
+})
+// 触发器配置表单数据
+const configForm = ref<TriggerSetting>({
+  type: TriggerTypeEnum.HTTP_REQUEST,
+  httpRequestSetting: {
+    url: '',
+    header: [],
+    body: []
+  }
+})
+
+/** 保存配置 */
+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
+  currentNode.value.triggerSetting = configForm.value
+  settingVisible.value = false
+  return true
+}
+/** 获取节点展示内容 */
+const getShowText = (): string => {
+  let showText = ''
+  if (configForm.value.type === TriggerTypeEnum.HTTP_REQUEST) {
+    showText = `${configForm.value.httpRequestSetting.url}`
+  }
+  return showText
+}
+
+/** 显示触发器节点配置, 由父组件传过来 */
+const showTriggerNodeConfig = (node: SimpleFlowNode) => {
+  nodeName.value = node.name
+  if (node.triggerSetting) {
+    configForm.value.type = node.triggerSetting.type
+    configForm.value.httpRequestSetting = node.triggerSetting.httpRequestSetting
+  }
+}
+
+defineExpose({ openDrawer, showTriggerNodeConfig }) // 暴露方法给父组件
+</script>
+
+<style lang="scss" scoped></style>

+ 82 - 14
src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue

@@ -3,7 +3,7 @@
     :append-to-body="true"
     v-model="settingVisible"
     :show-close="false"
-    :size="550"
+    :size="580"
     :before-close="saveConfig"
     class="justify-start"
   >
@@ -19,7 +19,8 @@
           :placeholder="nodeName"
         />
         <div v-else class="node-name">
-          {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
+          {{ nodeName }}
+          <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
         </div>
         <div class="divide-line"></div>
       </div>
@@ -46,14 +47,13 @@
                 v-model="configForm.candidateStrategy"
                 @change="changeCandidateStrategy"
               >
-                <el-radio
-                  v-for="(dict, index) in CANDIDATE_STRATEGY"
-                  :key="index"
-                  :value="dict.value"
-                  :label="dict.value"
-                >
-                  {{ dict.label }}
-                </el-radio>
+                <el-row>
+                  <el-col v-for="(dict, index) in CANDIDATE_STRATEGY" :key="index" :span="8">
+                    <el-radio :value="dict.value" :label="dict.value">
+                      {{ dict.label }}
+                    </el-radio>
+                  </el-col>
+                </el-row>
               </el-radio-group>
             </el-form-item>
             <el-form-item
@@ -148,7 +148,7 @@
                   :key="idx"
                   :label="item.title"
                   :value="item.field"
-                  :disabled ="!item.required"
+                  :disabled="!item.required"
                 />
               </el-select>
             </el-form-item>
@@ -163,7 +163,7 @@
                   :key="idx"
                   :label="item.title"
                   :value="item.field"
-                  :disabled ="!item.required"
+                  :disabled="!item.required"
                 />
               </el-select>
             </el-form-item>
@@ -356,6 +356,16 @@
                 </div>
               </el-radio-group>
             </el-form-item>
+
+            <el-divider content-position="left">是否需要签名</el-divider>
+            <el-form-item prop="signEnable">
+              <el-switch v-model="configForm.signEnable" active-text="是" inactive-text="否" />
+            </el-form-item>
+
+            <el-divider content-position="left">审批意见</el-divider>
+            <el-form-item prop="reasonRequire">
+              <el-switch v-model="configForm.reasonRequire" active-text="必填" inactive-text="非必填" />
+            </el-form-item>
           </el-form>
         </div>
       </el-tab-pane>
@@ -435,6 +445,9 @@
           </div>
         </div>
       </el-tab-pane>
+      <el-tab-pane label="监听器" name="listener">
+        <UserTaskListener ref="userTaskListenerRef" v-model="configForm" :form-field-options="formFieldOptions" />
+      </el-tab-pane>
     </el-tabs>
     <template #footer>
       <el-divider />
@@ -484,6 +497,7 @@ import {
 import { defaultProps } from '@/utils/tree'
 import { cloneDeep } from 'lodash-es'
 import { convertTimeUnit, getApproveTypeText } from '../utils'
+import UserTaskListener from './components/UserTaskListener.vue'
 defineOptions({
   name: 'UserTaskNodeConfig'
 })
@@ -609,9 +623,11 @@ const {
   cTimeoutMaxRemindCount
 } = useTimeoutHandler()
 
+const userTaskListenerRef = ref()
+
 // 保存配置
 const saveConfig = async () => {
-  activeTabName.value = 'user'
+  // activeTabName.value = 'user'
   // 设置审批节点名称
   currentNode.value.name = nodeName.value!
   // 设置审批类型
@@ -624,7 +640,8 @@ const saveConfig = async () => {
   }
 
   if (!formRef) return false
-  const valid = await formRef.value.validate()
+  if (!userTaskListenerRef) return false
+  const valid = (await formRef.value.validate()) && (await userTaskListenerRef.value.validate())
   if (!valid) return false
   const showText = getShowText()
   if (!showText) return false
@@ -663,6 +680,31 @@ const saveConfig = async () => {
   currentNode.value.fieldsPermission = fieldsPermissionConfig.value
   // 设置按钮权限
   currentNode.value.buttonsSetting = buttonsSetting.value
+  // 创建任务监听器
+  currentNode.value.taskCreateListener = {
+    enable: configForm.value.taskCreateListenerEnable ?? false,
+    path: configForm.value.taskCreateListenerPath,
+    header: configForm.value.taskCreateListener?.header,
+    body: configForm.value.taskCreateListener?.body
+  }
+  // 指派任务监听器
+  currentNode.value.taskAssignListener = {
+    enable: configForm.value.taskAssignListenerEnable ?? false,
+    path: configForm.value.taskAssignListenerPath,
+    header: configForm.value.taskAssignListener?.header,
+    body: configForm.value.taskAssignListener?.body
+  }
+  // 完成任务监听器
+  currentNode.value.taskCompleteListener = {
+    enable: configForm.value.taskCompleteListenerEnable ?? false,
+    path: configForm.value.taskCompleteListenerPath,
+    header: configForm.value.taskCompleteListener?.header,
+    body: configForm.value.taskCompleteListener?.body
+  }
+  // 签名
+  currentNode.value.signEnable = configForm.value.signEnable
+  // 审批意见
+  currentNode.value.reasonRequire = configForm.value.reasonRequire
 
   currentNode.value.showText = showText
   settingVisible.value = false
@@ -714,6 +756,32 @@ const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
   buttonsSetting.value = cloneDeep(node.buttonsSetting) || DEFAULT_BUTTON_SETTING
   // 4. 表单字段权限配置
   getNodeConfigFormFields(node.fieldsPermission)
+  // 5. 监听器
+  // 5.1 创建任务
+  configForm.value.taskCreateListenerEnable = node.taskCreateListener!.enable
+  configForm.value.taskCreateListenerPath = node.taskCreateListener!.path
+  configForm.value.taskCreateListener = {
+    header: node.taskCreateListener?.header ?? [],
+    body: node.taskCreateListener?.body ?? []
+  }
+  // 5.2 指派任务
+  configForm.value.taskAssignListenerEnable = node.taskAssignListener!.enable
+  configForm.value.taskAssignListenerPath = node.taskAssignListener!.path
+  configForm.value.taskAssignListener = {
+    header: node.taskAssignListener?.header ?? [],
+    body: node.taskAssignListener?.body ?? []
+  }
+ // 5.3 完成任务
+  configForm.value.taskCompleteListenerEnable = node.taskCompleteListener!.enable
+  configForm.value.taskCompleteListenerPath = node.taskCompleteListener!.path
+  configForm.value.taskCompleteListener = {
+    header: node.taskCompleteListener?.header ?? [],
+    body: node.taskCompleteListener?.body ?? []
+  }
+  // 6. 签名
+  configForm.value.signEnable = node?.signEnable ?? false
+  // 7. 审批意见
+  configForm.value.reasonRequire = node?.reasonRequire ?? false
 }
 
 defineExpose({ openDrawer, showUserTaskNodeConfig }) // 暴露方法给父组件

+ 272 - 0
src/components/SimpleProcessDesignerV2/src/nodes-config/components/Condition.vue

@@ -0,0 +1,272 @@
+<template>
+  <el-form ref="formRef" :model="condition" :rules="formRules" label-position="top">
+    <el-form-item label="配置方式" prop="conditionType">
+      <el-radio-group v-model="condition.conditionType" @change="changeConditionType">
+        <el-radio
+          v-for="(dict, indexConditionType) in conditionConfigTypes"
+          :key="indexConditionType"
+          :value="dict.value"
+          :label="dict.value"
+        >
+          {{ dict.label }}
+        </el-radio>
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item v-if="condition.conditionType === ConditionType.RULE && condition.conditionGroups" label="条件规则">
+      <div class="condition-group-tool">
+        <div class="flex items-center">
+          <div class="mr-4">条件组关系</div>
+          <el-switch
+            v-model="condition.conditionGroups.and"
+            inline-prompt
+            active-text="且"
+            inactive-text="或"
+          />
+        </div>
+      </div>
+      <el-space direction="vertical" :spacer="condition.conditionGroups.and ? '且' : '或'">
+        <el-card
+          class="condition-group"
+          style="width: 530px"
+          v-for="(equation, cIdx) in condition.conditionGroups.conditions"
+          :key="cIdx"
+        >
+          <div
+            class="condition-group-delete"
+            v-if="condition.conditionGroups.conditions.length > 1"
+          >
+            <Icon
+              color="#0089ff"
+              icon="ep:circle-close-filled"
+              :size="18"
+              @click="deleteConditionGroup(condition.conditionGroups.conditions, cIdx)"
+            />
+          </div>
+          <template #header>
+            <div class="flex items-center justify-between">
+              <div>条件组</div>
+              <div class="flex">
+                <div class="mr-4">规则关系</div>
+                <el-switch
+                  v-model="equation.and"
+                  inline-prompt
+                  active-text="且"
+                  inactive-text="或"
+                />
+              </div>
+            </div>
+          </template>
+
+          <div class="flex pt-2" v-for="(rule, rIdx) in equation.rules" :key="rIdx">
+            <div class="mr-2">
+              <el-form-item
+                :prop="`conditionGroups.conditions.${cIdx}.rules.${rIdx}.leftSide`"
+                :rules="{
+                  required: true,
+                  message: '左值不能为空',
+                  trigger: 'change'
+                }"
+              >
+                <el-select style="width: 160px" v-model="rule.leftSide">
+                  <el-option
+                    v-for="(field, fIdx) in fieldOptions"
+                    :key="fIdx"
+                    :label="field.title"
+                    :value="field.field"
+                    :disabled="!field.required"
+                  />
+                </el-select>
+              </el-form-item>
+            </div>
+            <div class="mr-2">
+              <el-select v-model="rule.opCode" style="width: 100px">
+                <el-option
+                  v-for="operator in COMPARISON_OPERATORS"
+                  :key="operator.value"
+                  :label="operator.label"
+                  :value="operator.value"
+                />
+              </el-select>
+            </div>
+            <div class="mr-2">
+              <el-form-item
+                :prop="`conditionGroups.conditions.${cIdx}.rules.${rIdx}.rightSide`"
+                :rules="{
+                  required: true,
+                  message: '右值不能为空',
+                  trigger: 'blur'
+                }"
+              >
+                <el-input v-model="rule.rightSide" style="width: 160px" />
+              </el-form-item>
+            </div>
+            <div class="mr-1 flex items-center" v-if="equation.rules.length > 1">
+              <Icon icon="ep:delete" :size="18" @click="deleteConditionRule(equation, rIdx)" />
+            </div>
+            <div class="flex items-center">
+              <Icon icon="ep:plus" :size="18" @click="addConditionRule(equation, rIdx)" />
+            </div>
+          </div>
+        </el-card>
+      </el-space>
+      <div title="添加条件组" class="mt-4 cursor-pointer">
+        <Icon
+          color="#0089ff"
+          icon="ep:plus"
+          :size="24"
+          @click="addConditionGroup(condition.conditionGroups?.conditions)"
+        />
+      </div>
+    </el-form-item>
+    <el-form-item
+      v-if="condition.conditionType === ConditionType.EXPRESSION"
+      label="条件表达式"
+      prop="conditionExpression"
+    >
+      <el-input
+        type="textarea"
+        v-model="condition.conditionExpression"
+        clearable
+        style="width: 100%"
+      />
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import {
+  COMPARISON_OPERATORS,
+  CONDITION_CONFIG_TYPES,
+  ConditionType,
+  DEFAULT_CONDITION_GROUP_VALUE,
+  ProcessVariableEnum
+} from '../../consts'
+import { BpmModelFormType } from '@/utils/constants'
+import { useFormFields } from '../../node'
+
+const props = defineProps({
+  modelValue: {
+    type: Object,
+    required: true
+  }
+})
+const emit = defineEmits(['update:modelValue'])
+const condition = computed({
+  get() {
+    return props.modelValue
+  },
+  set(newValue) {
+    emit('update:modelValue', newValue)
+  }
+})
+const formType = inject<Ref<number>>('formType') // 表单类型
+const conditionConfigTypes = computed(() => {
+  return CONDITION_CONFIG_TYPES.filter((item) => {
+    // 业务表单暂时去掉条件规则选项
+    if (formType?.value === BpmModelFormType.CUSTOM && item.value === ConditionType.RULE) {
+      return false
+    } else {
+      return true
+    }
+  })
+})
+/** 条件规则可选择的表单字段 */
+const fieldOptions = computed(() => {
+  const fieldsCopy = useFormFields().slice()
+  // 固定添加发起人 ID 字段
+  fieldsCopy.unshift({
+    field: ProcessVariableEnum.START_USER_ID,
+    title: '发起人',
+    required: true
+  })
+  return fieldsCopy
+})
+// 表单校验规则
+const formRules = reactive({
+  conditionType: [{ required: true, message: '配置方式不能为空', trigger: 'blur' }],
+  conditionExpression: [{ required: true, message: '条件表达式不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 切换条件配置方式 */
+const changeConditionType = () => {
+  if (condition.value.conditionType === ConditionType.RULE) {
+    if (!condition.value.conditionGroups) {
+      condition.value.conditionGroups = DEFAULT_CONDITION_GROUP_VALUE
+    }
+  }
+}
+const deleteConditionGroup = (conditions, index) => {
+  conditions.splice(index, 1)
+}
+
+const deleteConditionRule = (condition, index) => {
+  condition.rules.splice(index, 1)
+}
+
+const addConditionRule = (condition, index) => {
+  const rule = {
+    opCode: '==',
+    leftSide: '',
+    rightSide: ''
+  }
+  condition.rules.splice(index + 1, 0, rule)
+}
+
+const addConditionGroup = (conditions) => {
+  const condition = {
+    and: true,
+    rules: [
+      {
+        opCode: '==',
+        leftSide: '',
+        rightSide: ''
+      }
+    ]
+  }
+  conditions.push(condition)
+}
+
+const validate = async () => {
+  if (!formRef) return false
+  return await formRef.value.validate()
+}
+
+defineExpose({ validate })
+</script>
+
+<style lang="scss" scoped>
+.condition-group-tool {
+  display: flex;
+  justify-content: space-between;
+  width: 500px;
+  margin-bottom: 20px;
+}
+
+.condition-group {
+  position: relative;
+
+  &:hover {
+    border-color: #0089ff;
+
+    .condition-group-delete {
+      opacity: 1;
+    }
+  }
+
+  .condition-group-delete {
+    position: absolute;
+    top: 0;
+    left: 0;
+    display: flex;
+    cursor: pointer;
+    opacity: 0;
+  }
+}
+
+::v-deep(.el-card__header) {
+  padding: 8px var(--el-card-padding);
+  border-bottom: 1px solid var(--el-card-border-color);
+  box-sizing: border-box;
+}
+</style>

+ 181 - 0
src/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestParamSetting.vue

@@ -0,0 +1,181 @@
+<template>
+  <el-form-item label="请求头">
+    <div class="flex pt-2" v-for="(item, index) in props.header" :key="index">
+      <div class="mr-2">
+        <el-form-item
+          :prop="`${bind}.header.${index}.key`"
+          :rules="{
+            required: true,
+            message: '参数名不能为空',
+            trigger: 'blur'
+          }"
+        >
+          <el-input class="w-160px" v-model="item.key" />
+        </el-form-item>
+      </div>
+      <div class="mr-2">
+        <el-select class="w-100px!" v-model="item.type">
+          <el-option
+            v-for="types in LISTENER_MAP_TYPES"
+            :key="types.value"
+            :label="types.label"
+            :value="types.value"
+          />
+        </el-select>
+      </div>
+      <div class="mr-2">
+        <el-form-item
+          :prop="`${bind}.header.${index}.value`"
+          :rules="{
+            required: true,
+            message: '参数值不能为空',
+            trigger: 'blur'
+          }"
+        >
+          <el-input
+            v-if="item.type === ListenerParamTypeEnum.FIXED_VALUE"
+            class="w-160px"
+            v-model="item.value"
+          />
+        </el-form-item>
+        <el-form-item
+          :prop="`${bind}.header.${index}.value`"
+          :rules="{
+            required: true,
+            message: '参数值不能为空',
+            trigger: 'change'
+          }"
+        >
+          <el-select
+            v-if="item.type === ListenerParamTypeEnum.FROM_FORM"
+            class="w-160px!"
+            v-model="item.value"
+          >
+            <el-option
+              v-for="(field, fIdx) in formFieldOptions"
+              :key="fIdx"
+              :label="field.title"
+              :value="field.field"
+              :disabled="!field.required"
+            />
+          </el-select>
+        </el-form-item>
+      </div>
+      <div class="mr-1 flex items-center">
+        <Icon icon="ep:delete" :size="18" @click="deleteHttpRequestParam(props.header, index)" />
+      </div>
+    </div>
+    <el-button type="primary" text @click="addHttpRequestParam(props.header)">
+      <Icon icon="ep:plus" class="mr-5px" />添加一行
+    </el-button>
+  </el-form-item>
+  <el-form-item label="请求体">
+    <div class="flex pt-2" v-for="(item, index) in props.body" :key="index">
+      <div class="mr-2">
+        <el-form-item
+          :prop="`${bind}.body.${index}.key`"
+          :rules="{
+            required: true,
+            message: '参数名不能为空',
+            trigger: 'blur'
+          }"
+        >
+          <el-input class="w-160px" v-model="item.key" />
+        </el-form-item>
+      </div>
+      <div class="mr-2">
+        <el-select class="w-100px!" v-model="item.type">
+          <el-option
+            v-for="types in LISTENER_MAP_TYPES"
+            :key="types.value"
+            :label="types.label"
+            :value="types.value"
+          />
+        </el-select>
+      </div>
+      <div class="mr-2">
+        <el-form-item
+          :prop="`${bind}.body.${index}.value`"
+          :rules="{
+            required: true,
+            message: '参数值不能为空',
+            trigger: 'blur'
+          }"
+        >
+          <el-input
+            v-if="item.type === ListenerParamTypeEnum.FIXED_VALUE"
+            class="w-160px"
+            v-model="item.value"
+          />
+        </el-form-item>
+        <el-form-item
+          :prop="`${bind}.body.${index}.value`"
+          :rules="{
+            required: true,
+            message: '参数值不能为空',
+            trigger: 'change'
+          }"
+        >
+          <el-select
+            v-if="item.type === ListenerParamTypeEnum.FROM_FORM"
+            class="w-160px!"
+            v-model="item.value"
+          >
+            <el-option
+              v-for="(field, fIdx) in formFieldOptions"
+              :key="fIdx"
+              :label="field.title"
+              :value="field.field"
+              :disabled="!field.required"
+            />
+          </el-select>
+        </el-form-item>
+      </div>
+      <div class="mr-1 flex items-center">
+        <Icon icon="ep:delete" :size="18" @click="deleteHttpRequestParam(props.body, index)" />
+      </div>
+    </div>
+    <el-button type="primary" text @click="addHttpRequestParam(props.body)">
+      <Icon icon="ep:plus" class="mr-5px" />添加一行
+    </el-button>
+  </el-form-item>
+</template>
+<script setup lang="ts">
+import { ListenerParam, LISTENER_MAP_TYPES, ListenerParamTypeEnum } from '../../consts'
+import { useFormFields } from '../../node'
+defineOptions({
+  name: 'HttpRequestParamSetting'
+})
+
+const props = defineProps({
+  header: {
+    type: Array as () => ListenerParam[],
+    required: false,
+    default: () => []
+  },
+  body: {
+    type: Array as () => ListenerParam[],
+    required: false,
+    default: () => []
+  },
+  bind: {
+    type: String,
+    required: true
+  }
+})
+
+const formFieldOptions = useFormFields()
+
+const addHttpRequestParam = (arr: ListenerParam[]) => {
+  arr.push({
+    key: '',
+    type: ListenerParamTypeEnum.FIXED_VALUE,
+    value: ''
+  })
+}
+const deleteHttpRequestParam = (arr: ListenerParam[], index: number) => {
+  arr.splice(index, 1)
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 88 - 0
src/components/SimpleProcessDesignerV2/src/nodes-config/components/UserTaskListener.vue

@@ -0,0 +1,88 @@
+<template>
+  <el-form ref="listenerFormRef" :model="configForm" label-position="top">
+    <div v-for="(listener, listenerIdx) in taskListener" :key="listenerIdx">
+      <el-divider content-position="left">
+        <el-text tag="b" size="large">{{ listener.name }}</el-text>
+      </el-divider>
+      <el-form-item>
+        <el-switch
+          v-model="configForm[`task${listener.type}ListenerEnable`]"
+          active-text="开启"
+          inactive-text="关闭"
+        />
+      </el-form-item>
+      <div v-if="configForm[`task${listener.type}ListenerEnable`]">
+        <el-form-item>
+          <el-alert
+            title="仅支持 POST 请求,以请求体方式接收参数"
+            type="warning"
+            show-icon
+            :closable="false"
+          />
+        </el-form-item>
+        <el-form-item
+          label="请求地址"
+          :prop="`task${listener.type}ListenerPath`"
+          :rules="{
+            required: true,
+            message: '请求地址不能为空',
+            trigger: 'blur'
+          }"
+        >
+          <el-input v-model="configForm[`task${listener.type}ListenerPath`]" />
+        </el-form-item>
+        <HttpRequestParamSetting
+          :header="configForm[`task${listener.type}Listener`].header"
+          :body="configForm[`task${listener.type}Listener`].body"
+          :bind="`task${listener.type}Listener`"
+        />
+      </div>
+    </div>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import HttpRequestParamSetting from './HttpRequestParamSetting.vue'
+
+const props = defineProps({
+  modelValue: {
+    type: Object,
+    required: true
+  },
+  formFieldOptions: {
+    type: Object,
+    required: true
+  }
+})
+const emit = defineEmits(['update:modelValue'])
+const listenerFormRef = ref()
+const configForm = computed({
+  get() {
+    return props.modelValue
+  },
+  set(newValue) {
+    emit('update:modelValue', newValue)
+  }
+})
+const taskListener = ref([
+  {
+    name: '创建任务',
+    type: 'Create'
+  },
+  {
+    name: '指派任务执行人员',
+    type: 'Assign'
+  },
+  {
+    name: '完成任务',
+    type: 'Complete'
+  }
+])
+
+const validate = async () => {
+  if (!listenerFormRef) return false
+  return await listenerFormRef.value.validate()
+}
+
+defineExpose({ validate })
+</script>

+ 1 - 2
src/components/SimpleProcessDesignerV2/src/nodes/DelayTimerNode.vue

@@ -9,8 +9,7 @@
         ]"
       >
         <div class="node-title-container">
-          <!-- TODO @芋艿 需要更换图标 -->
-          <div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div>
+          <div class="node-title-icon delay-node"><span class="iconfont icon-delay"></span></div>
           <input
             v-if="!readonly && showInput"
             type="text"

+ 1 - 1
src/components/SimpleProcessDesignerV2/src/nodes/EndEventNode.vue

@@ -77,7 +77,7 @@ const props = defineProps({
 const currentNode = useWatchNode(props)
 // 是否只读
 const readonly = inject<Boolean>('readonly')
-const processInstance = inject<Ref<any>>('processInstance')
+const processInstance = inject<Ref<any>>('processInstance', ref({}))
 // 审批信息的弹窗显示,用于只读模式
 const dialogVisible = ref(false) // 弹窗可见性
 const processInstanceInfos = ref<any[]>([]) // 流程的审批信息

+ 7 - 4
src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue

@@ -108,7 +108,7 @@
 <script setup lang="ts">
 import NodeHandler from '../NodeHandler.vue'
 import ProcessNodeTree from '../ProcessNodeTree.vue'
-import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
+import { SimpleFlowNode, NodeType, ConditionType, DEFAULT_CONDITION_GROUP_VALUE, NODE_DEFAULT_TEXT } from '../consts'
 import { getDefaultConditionNodeName } from '../utils'
 import { useTaskStatusClass } from '../node'
 import { generateUUID } from '@/utils'
@@ -149,7 +149,7 @@ const blurEvent = (index: number) => {
   showInputs.value[index] = false
   const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
   conditionNode.name =
-    conditionNode.name || getDefaultConditionNodeName(index, conditionNode.defaultFlow)
+    conditionNode.name || getDefaultConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
 }
 
 // 点击条件名称
@@ -178,8 +178,11 @@ const addCondition = () => {
       type: NodeType.CONDITION_NODE,
       childNode: undefined,
       conditionNodes: [],
-      conditionType: 1,
-      defaultFlow: false
+      conditionSetting: {
+        defaultFlow: false,
+        conditionType: ConditionType.RULE,
+        conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
+      }
     }
     conditionNodes.splice(lastIndex, 0, conditionData)
   }

+ 8 - 5
src/components/SimpleProcessDesignerV2/src/nodes/InclusiveNode.vue

@@ -34,7 +34,7 @@
               ]"
             >
               <div class="branch-node-title-container">
-                <div v-if="showInputs[index]">
+                <div v-if="!readonly && showInputs[index]">
                   <input
                     type="text"
                     class="editable-title-input"
@@ -110,7 +110,7 @@
 <script setup lang="ts">
 import NodeHandler from '../NodeHandler.vue'
 import ProcessNodeTree from '../ProcessNodeTree.vue'
-import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
+import { SimpleFlowNode, NodeType, ConditionType, DEFAULT_CONDITION_GROUP_VALUE, NODE_DEFAULT_TEXT } from '../consts'
 import { useTaskStatusClass } from '../node'
 import { getDefaultInclusiveConditionNodeName } from '../utils'
 import { generateUUID } from '@/utils'
@@ -153,7 +153,7 @@ const blurEvent = (index: number) => {
   showInputs.value[index] = false
   const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
   conditionNode.name =
-    conditionNode.name || getDefaultInclusiveConditionNodeName(index, conditionNode.defaultFlow)
+    conditionNode.name || getDefaultInclusiveConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
 }
 
 // 点击条件名称
@@ -182,8 +182,11 @@ const addCondition = () => {
       type: NodeType.CONDITION_NODE,
       childNode: undefined,
       conditionNodes: [],
-      conditionType: 1,
-      defaultFlow: false
+      conditionSetting: {
+        defaultFlow: false,
+        conditionType: ConditionType.RULE,
+        conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
+      }
     }
     conditionNodes.splice(lastIndex, 0, conditionData)
   }

+ 97 - 0
src/components/SimpleProcessDesignerV2/src/nodes/RouterNode.vue

@@ -0,0 +1,97 @@
+<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">
+          <div class="node-title-icon router-node">
+            <span class="iconfont icon-router"></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.ROUTER_BRANCH_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>
+    <RouterNodeConfig 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 RouterNodeConfig from '../nodes-config/RouterNodeConfig.vue'
+
+defineOptions({
+  name: 'RouterNode'
+})
+
+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.ROUTER_BRANCH_NODE)
+
+const nodeSetting = ref()
+// 打开节点配置
+const openNodeConfig = () => {
+  if (readonly) {
+    return
+  }
+  nodeSetting.value.showRouteNodeConfig(currentNode.value)
+  nodeSetting.value.openDrawer()
+}
+
+// 删除节点。更新当前节点为孩子节点
+const deleteNode = () => {
+  emits('update:flowNode', currentNode.value.childNode)
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 2 - 2
src/components/SimpleProcessDesignerV2/src/nodes/StartUserNode.vue

@@ -13,7 +13,7 @@
             ><span class="iconfont icon-start-user"></span
           ></div>
           <input
-            v-if="showInput"
+            v-if="!readonly && showInput"
             type="text"
             class="editable-title-input"
             @blur="blurEvent()"
@@ -117,7 +117,7 @@ const props = defineProps({
   }
 })
 const readonly = inject<Boolean>('readonly') // 是否只读
-const tasks = inject<Ref<any[]>>('tasks')
+const tasks = inject<Ref<any[]>>('tasks', ref([]))
 // 定义事件,更新父组件。
 const emits = defineEmits<{
   'update:modelValue': [node: SimpleFlowNode | undefined]

+ 97 - 0
src/components/SimpleProcessDesignerV2/src/nodes/TriggerNode.vue

@@ -0,0 +1,97 @@
+<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">
+          <div class="node-title-icon trigger-node">
+            <span class="iconfont icon-trigger"></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.TRIGGER_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>
+    <TriggerNodeConfig 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 TriggerNodeConfig from '../nodes-config/TriggerNodeConfig.vue'
+
+defineOptions({
+  name: 'TriggerNode'
+})
+
+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.TRIGGER_NODE)
+
+const nodeSetting = ref()
+// 打开节点配置
+const openNodeConfig = () => {
+  if (readonly) {
+    return
+  }
+  nodeSetting.value.showTriggerNodeConfig(currentNode.value)
+  nodeSetting.value.openDrawer()
+}
+
+// 删除节点。更新当前节点为孩子节点
+const deleteNode = () => {
+  emits('update:flowNode', currentNode.value.childNode)
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 1 - 1
src/components/SimpleProcessDesignerV2/src/nodes/UserTaskNode.vue

@@ -131,7 +131,7 @@ const emits = defineEmits<{
 
 // 是否只读
 const readonly = inject<Boolean>('readonly')
-const tasks = inject<Ref<any[]>>('tasks')
+const tasks = inject<Ref<any[]>>('tasks', ref([]))
 // 监控节点变化
 const currentNode = useWatchNode(props)
 // 节点名称编辑

binární
src/components/SimpleProcessDesignerV2/theme/iconfont.ttf


binární
src/components/SimpleProcessDesignerV2/theme/iconfont.woff


binární
src/components/SimpleProcessDesignerV2/theme/iconfont.woff2


+ 55 - 16
src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss

@@ -113,18 +113,21 @@
 
 // 节点连线气泡卡片样式
 .handler-item-wrapper {
+  width: 320px;
   display: flex;
+  flex-wrap: wrap;
   cursor: pointer;
 
   .handler-item {
     display: flex;
     flex-direction: column;
     align-items: center;
+    margin-top: 12px;
   }
 
   .handler-item-icon {
-    width: 60px;
-    height: 60px;
+    width: 50px;
+    height: 50px;
     background: #fff;
     border: 1px solid #e2e2e2;
     border-radius: 50%;
@@ -138,13 +141,14 @@
 
     .icon-size {
       font-size: 25px;
-      line-height: 60px;
+      line-height: 50px;
     }
   }
 
   .approve {
     color: #ff943e;
   }
+
   .copy {
     color: #3296fa;
   }
@@ -161,6 +165,18 @@
     color: #345da2;
   }
 
+  .delay {
+    color: #e47470;
+  }
+
+  .trigger {
+    color: #3373d2;
+  }
+
+  .router {
+    color: #ca3a31
+  }
+
   .handler-item-text {
     margin-top: 4px;
     width: 80px;
@@ -266,6 +282,18 @@
           &.start-user {
             color: #676565;
           }
+
+          &.delay-node {
+            color: #e47470;
+          }
+
+          &.trigger-node {
+            color: #3373d2;
+          }
+          
+          &.router-node {
+            color: #ca3a31
+          }
         }
 
         .node-title {
@@ -711,45 +739,56 @@
 
 // iconfont 样式
 @font-face {
-  font-family: 'iconfont'; /* Project id 4495938 */
-  src:
-    url('iconfont.woff2?t=1724339470412') format('woff2'),
-    url('iconfont.woff?t=1724339470412') format('woff'),
-    url('iconfont.ttf?t=1724339470412') format('truetype');
+  font-family: "iconfont"; /* Project id 4495938 */
+  src: url('iconfont.woff2?t=1737639517142') format('woff2'),
+       url('iconfont.woff?t=1737639517142') format('woff'),
+       url('iconfont.ttf?t=1737639517142') format('truetype');
 }
 
 .iconfont {
-  font-family: 'iconfont' !important;
+  font-family: "iconfont" !important;
   font-size: 16px;
   font-style: normal;
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
 }
 
+.icon-trigger:before {
+  content: "\e6d3";
+}
+
+.icon-router:before {
+  content: "\e6b2";
+}
+
+.icon-delay:before {
+  content: "\e600";
+}
+
 .icon-start-user:before {
-  content: '\e679';
+  content: "\e679";
 }
 
 .icon-inclusive:before {
-  content: '\e602';
+  content: "\e602";
 }
 
 .icon-copy:before {
-  content: '\e7eb';
+  content: "\e7eb";
 }
 
 .icon-handle:before {
-  content: '\e61c';
+  content: "\e61c";
 }
 
 .icon-exclusive:before {
-  content: '\e717';
+  content: "\e717";
 }
 
 .icon-approve:before {
-  content: '\e715';
+  content: "\e715";
 }
 
 .icon-parallel:before {
-  content: '\e688';
+  content: "\e688";
 }

+ 2 - 22
src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue

@@ -308,28 +308,6 @@ 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)
@@ -480,6 +458,7 @@ const initModelListeners = () => {
       emit('commandStack-changed', event)
       emit('input', xml)
       emit('change', xml)
+      emit('save', xml)
     } catch (e: any) {
       console.error(`[Process Designer Warn]: ${e.message || e}`)
     }
@@ -568,6 +547,7 @@ const importLocalFile = () => {
   reader.onload = function () {
     let xmlStr = this.result
     createNewDiagram(xmlStr)
+    emit('save', xmlStr)
   }
 }
 /* ------------------------------------------------ refs methods ------------------------------------------------------ */

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

@@ -1438,6 +1438,45 @@
           "isBody": true
         }
       ]
+    },
+    {
+      "name": "SignEnable",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:UserTask"]
+      },
+      "properties": [
+        {
+          "name": "value",
+          "type": "Boolean",
+          "isBody": true
+        }
+      ]
+    },
+    {
+      "name": "SkipExpression",
+      "extends": ["bpmn:UserTask"],
+      "properties": [
+        {
+          "name": "skipExpression",
+          "isAttr": true,
+          "type": "String"
+        }
+      ]
+    },
+    {
+      "name": "ReasonRequire",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:UserTask"]
+      },
+      "properties": [
+        {
+          "name": "value",
+          "type": "Boolean",
+          "isBody": true
+        }
+      ]
     }
   ],
   "emumerations": []

+ 1 - 1
src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="process-panel__container" :style="{ width: `${width}px` }">
+  <div class="process-panel__container" :style="{ width: `${width}px`, maxHeight: '600px' }">
     <el-collapse v-model="activeTab" v-if="isReady">
       <el-collapse-item name="base">
         <!-- class="panel-tab__title" -->

+ 3 - 0
src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue

@@ -152,6 +152,9 @@ watch(
       handleKeyUpdate(props.model.key)
       handleNameUpdate(props.model.name)
     }
+  },
+  {
+    immediate: true
   }
 )
 

+ 33 - 2
src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue

@@ -5,6 +5,7 @@
      4. 操作按钮
      5. 字段权限
      6. 审批类型
+     7. 是否需要签名
 -->
 <template>
   <div>
@@ -161,6 +162,16 @@
         </el-radio-group>
       </div>
     </div>
+
+    <el-divider content-position="left">是否需要签名</el-divider>
+    <el-form-item prop="signEnable">
+      <el-switch v-model="signEnable.value" active-text="是" inactive-text="否" />
+    </el-form-item>
+
+    <el-divider content-position="left">审批意见</el-divider>
+    <el-form-item prop="reasonRequire">
+      <el-switch v-model="reasonRequire.value" active-text="必填" inactive-text="非必填" />
+    </el-form-item>
   </div>
 </template>
 
@@ -218,6 +229,12 @@ const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFie
 // 审批类型
 const approveType = ref({ value: ApproveType.USER })
 
+// 是否需要签名
+const signEnable = ref({ value: false })
+
+// 审批意见
+const reasonRequire = ref({ value: false })
+
 const elExtensionElements = ref()
 const otherExtensions = ref()
 const bpmnElement = ref()
@@ -311,6 +328,16 @@ const resetCustomConfigList = () => {
     })
   }
 
+  // 是否需要签名
+  signEnable.value =
+    elExtensionElements.value.values?.filter((ex) => ex.$type === `${prefix}:SignEnable`)?.[0] ||
+    bpmnInstances().moddle.create(`${prefix}:SignEnable`, { value: false })
+
+  // 审批意见
+  reasonRequire.value =
+    elExtensionElements.value.values?.filter((ex) => ex.$type === `${prefix}:ReasonRequire`)?.[0] ||
+    bpmnInstances().moddle.create(`${prefix}:ReasonRequire`, { value: false })
+
   // 保留剩余扩展元素,便于后面更新该元素对应属性
   otherExtensions.value =
     elExtensionElements.value.values?.filter(
@@ -322,7 +349,9 @@ const resetCustomConfigList = () => {
         ex.$type !== `${prefix}:AssignEmptyUserIds` &&
         ex.$type !== `${prefix}:ButtonsSetting` &&
         ex.$type !== `${prefix}:FieldsPermission` &&
-        ex.$type !== `${prefix}:ApproveType`
+        ex.$type !== `${prefix}:ApproveType` &&
+        ex.$type !== `${prefix}:SignEnable` &&
+        ex.$type !== `${prefix}:ReasonRequire`
     ) ?? []
 
   // 更新元素扩展属性,避免后续报错
@@ -373,7 +402,9 @@ const updateElementExtensions = () => {
       assignEmptyUserIdsEl.value,
       approveType.value,
       ...buttonsSettingEl.value,
-      ...fieldsPermissionEl.value
+      ...fieldsPermissionEl.value,
+      signEnable.value,
+      reasonRequire.value
     ]
   })
   bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {

+ 21 - 12
src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue

@@ -1,6 +1,10 @@
 <template>
   <div class="panel-tab__content">
-    <el-radio-group v-model="approveMethod" @change="onApproveMethodChange">
+    <el-radio-group
+      v-if="type === 'UserTask'"
+      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">
@@ -23,6 +27,9 @@
         </div>
       </div>
     </el-radio-group>
+    <div v-else>
+      除了UserTask以外节点的多实例待实现
+    </div>
     <!-- 与Simple设计器配置合并,保留以前的代码 -->
     <el-form label-width="90px" style="display: none">
       <el-form-item label="快捷配置">
@@ -301,19 +308,21 @@ 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
+  if (props.type === 'UserTask') {
+    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`) ?? []
+    otherExtensions.value =
+      extensionElements.values.filter((ex) => ex.$type !== `${prefix}:ApproveMethod`) ?? []
 
-  if (!approveMethod.value) {
-    approveMethod.value = ApproveMethodType.SEQUENTIAL_APPROVE
-    updateLoopCharacteristics()
+    if (!approveMethod.value) {
+      approveMethod.value = ApproveMethodType.SEQUENTIAL_APPROVE
+      updateLoopCharacteristics()
+    }
   }
 }
 const onApproveMethodChange = () => {

+ 31 - 1
src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue

@@ -192,6 +192,16 @@
       <!-- 选择弹窗 -->
       <ProcessExpressionDialog ref="processExpressionDialogRef" @select="selectProcessExpression" />
     </el-form-item>
+
+    <el-form-item label="跳过表达式" prop="skipExpression">
+      <el-input
+        type="textarea"
+        v-model="userTaskForm.skipExpression"
+        clearable
+        style="width: 100%"
+        @change="updateSkipExpression"
+      />
+    </el-form-item>
   </el-form>
 </template>
 
@@ -220,7 +230,8 @@ const props = defineProps({
 const prefix = inject('prefix')
 const userTaskForm = ref({
   candidateStrategy: undefined, // 分配规则
-  candidateParam: [] // 分配选项
+  candidateParam: [], // 分配选项
+  skipExpression: '' // 跳过表达式
 })
 const bpmnElement = ref()
 const bpmnInstances = () => (window as any)?.bpmnInstances
@@ -311,6 +322,13 @@ const resetTaskForm = () => {
       (ex) => ex.$type !== `${prefix}:CandidateStrategy` && ex.$type !== `${prefix}:CandidateParam`
     ) ?? []
 
+  // 跳过表达式
+  if (businessObject.skipExpression != undefined) {
+    userTaskForm.value.skipExpression = businessObject.skipExpression
+  } else {
+    userTaskForm.value.skipExpression = ''
+  }
+
   // 改用通过extensionElements来存储数据
   return
   if (businessObject.candidateStrategy != undefined) {
@@ -390,6 +408,18 @@ const updateElementTask = () => {
   })
 }
 
+const updateSkipExpression = () => {
+  if (userTaskForm.value.skipExpression && userTaskForm.value.skipExpression !== '') {
+    bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+      skipExpression: userTaskForm.value.skipExpression
+    })
+  } else {
+    bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+      skipExpression: null
+    })
+  }
+}
+
 // 打开监听器弹窗
 const processExpressionDialogRef = ref()
 const openProcessExpressionDialog = async () => {

+ 10 - 10
src/directives/permission/hasPermi.ts

@@ -1,8 +1,9 @@
 import type { App } from 'vue'
-import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import { useUserStore } from '@/store/modules/user'
 
 const { t } = useI18n() // 国际化
 
+/** 判断权限的指令 directive */
 export function hasPermi(app: App<Element>) {
   app.directive('hasPermi', (el, binding) => {
     const { value } = binding
@@ -19,13 +20,12 @@ export function hasPermi(app: App<Element>) {
   })
 }
 
+/** 判断权限的方法 function */
+const userStore = useUserStore()
+const all_permission = '*:*:*'
 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)
-  })
-}
+  return (
+    userStore.permissions.has(all_permission) ||
+    permission.some((permission) => userStore.permissions.has(permission))
+  )
+}

+ 56 - 44
src/layout/components/TagsView/src/TagsView.vue

@@ -12,6 +12,10 @@ import { useDesign } from '@/hooks/web/useDesign'
 import { useTemplateRefsList } from '@vueuse/core'
 import { ElScrollbar } from 'element-plus'
 import { useScrollTo } from '@/hooks/event/useScrollTo'
+import { useTagsView } from '@/hooks/web/useTagsView'
+import { cloneDeep } from 'lodash-es'
+
+defineOptions({ name: 'TagsView' })
 
 const { getPrefixCls } = useDesign()
 
@@ -19,7 +23,9 @@ const prefixCls = getPrefixCls('tags-view')
 
 const { t } = useI18n()
 
-const { currentRoute, push, replace } = useRouter()
+const { currentRoute, push } = useRouter()
+
+const { closeAll, closeLeft, closeRight, closeOther, closeCurrent, refreshPage } = useTagsView()
 
 const permissionStore = usePermissionStore()
 
@@ -31,6 +37,10 @@ const visitedViews = computed(() => tagsViewStore.getVisitedViews)
 
 const affixTagArr = ref<RouteLocationNormalizedLoaded[]>([])
 
+const selectedTag = computed(() => tagsViewStore.getSelectedTag)
+
+const setSelectTag = tagsViewStore.setSelectedTag
+
 const appStore = useAppStore()
 
 const tagsViewImmerse = computed(() => appStore.getTagsViewImmerse)
@@ -45,82 +55,73 @@ const initTags = () => {
   for (const tag of unref(affixTagArr)) {
     // Must have tag name
     if (tag.name) {
-      tagsViewStore.addVisitedView(tag)
+      tagsViewStore.addVisitedView(cloneDeep(tag))
     }
   }
 }
 
-const selectedTag = ref<RouteLocationNormalizedLoaded>()
-
 // 新增tag
 const addTags = () => {
   const { name } = unref(currentRoute)
   if (name) {
-    selectedTag.value = unref(currentRoute)
+    setSelectTag(unref(currentRoute))
     tagsViewStore.addView(unref(currentRoute))
   }
-  return false
 }
 
 // 关闭选中的tag
 const closeSelectedTag = (view: RouteLocationNormalizedLoaded) => {
-  if (view?.meta?.affix) return
-  tagsViewStore.delView(view)
-  if (isActive(view)) {
-    toLastView()
+  closeCurrent(view, () => {
+    if (isActive(view)) {
+      toLastView()
+    }
+  })
+}
+
+// 去最后一个
+const toLastView = () => {
+  const visitedViews = tagsViewStore.getVisitedViews
+  const latestView = visitedViews.slice(-1)[0]
+  if (latestView) {
+    push(latestView)
+  } else {
+    if (
+      unref(currentRoute).path === permissionStore.getAddRouters[0].path ||
+      unref(currentRoute).path === permissionStore.getAddRouters[0].redirect
+    ) {
+      addTags()
+      return
+    }
+    // You can set another route
+    push(permissionStore.getAddRouters[0].path)
   }
 }
 
 // 关闭全部
 const closeAllTags = () => {
-  tagsViewStore.delAllViews()
-  toLastView()
+  closeAll(() => {
+    toLastView()
+  })
 }
 
 // 关闭其它
 const closeOthersTags = () => {
-  tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
+  closeOther()
 }
 
 // 重新加载
 const refreshSelectedTag = async (view?: RouteLocationNormalizedLoaded) => {
-  if (!view) return
-  tagsViewStore.delCachedView()
-  const { path, query } = view
-  await nextTick()
-  replace({
-    path: '/redirect' + path,
-    query: query
-  })
+  refreshPage(view)
 }
 
 // 关闭左侧
 const closeLeftTags = () => {
-  tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
+  closeLeft()
 }
 
 // 关闭右侧
 const closeRightTags = () => {
-  tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
-}
-
-// 跳转到最后一个
-const toLastView = () => {
-  const visitedViews = tagsViewStore.getVisitedViews
-  const latestView = visitedViews.slice(-1)[0]
-  if (latestView) {
-    push(latestView)
-  } else {
-    if (
-      unref(currentRoute).path === permissionStore.getAddRouters[0].path ||
-      unref(currentRoute).path === permissionStore.getAddRouters[0].redirect
-    ) {
-      addTags()
-      return
-    }
-    // TODO: You can set another route
-    push('/')
-  }
+  closeRight()
 }
 
 // 滚动到选中的tag
@@ -209,13 +210,14 @@ const isActive = (route: RouteLocationNormalizedLoaded): boolean => {
 // 所有右键菜单组件的元素
 const itemRefs = useTemplateRefsList<ComponentRef<typeof ContextMenu & ContextMenuExpose>>()
 
-// 右键菜单装填改变的时候
+// 右键菜单状态改变的时候
 const visibleChange = (visible: boolean, tagItem: RouteLocationNormalizedLoaded) => {
   if (visible) {
     for (const v of unref(itemRefs)) {
       const elDropdownMenuRef = v.elDropdownMenuRef
       if (tagItem.fullPath !== v.tagItem.fullPath) {
         elDropdownMenuRef?.handleClose()
+        setSelectTag(tagItem)
       }
     }
   }
@@ -243,7 +245,17 @@ const move = (to: number) => {
   start()
 }
 
-onMounted(() => {
+const canShowIcon = (item: RouteLocationNormalizedLoaded) => {
+  if (
+    (item?.matched?.[1]?.meta?.icon && unref(tagsViewIcon)) ||
+    (item?.meta?.affix && unref(tagsViewIcon) && item?.meta?.icon)
+  ) {
+    return true
+  }
+  return false
+}
+
+onBeforeMount(() => {
   initTags()
   addTags()
 })

+ 2 - 1
src/router/modules/remaining.ts

@@ -344,7 +344,8 @@ const remainingRouter: AppRouteRecordRaw[] = [
         }
       },
       {
-        path: 'manager/model/update/:id',
+        // TODO @zws:1)建议,在加一个路由。然后标题是“复制流程”,这样体验会好点;2)复制出来的数据,在名字前面,加“副本 ”,和钉钉保持一致!
+        path: 'manager/model/:type/:id',
         component: () => import('@/views/bpm/model/form/index.vue'),
         name: 'BpmModelUpdate',
         meta: {

+ 24 - 2
src/store/modules/tagsView.ts

@@ -4,16 +4,19 @@ import { getRawRoute } from '@/utils/routerHelper'
 import { defineStore } from 'pinia'
 import { store } from '../index'
 import { findIndex } from '@/utils'
+import { useUserStoreWithOut } from './user'
 
 export interface TagsViewState {
   visitedViews: RouteLocationNormalizedLoaded[]
   cachedViews: Set<string>
+  selectedTag?: RouteLocationNormalizedLoaded
 }
 
 export const useTagsViewStore = defineStore('tagsView', {
   state: (): TagsViewState => ({
     visitedViews: [],
-    cachedViews: new Set()
+    cachedViews: new Set(),
+    selectedTag: undefined
   }),
   getters: {
     getVisitedViews(): RouteLocationNormalizedLoaded[] {
@@ -21,6 +24,9 @@ export const useTagsViewStore = defineStore('tagsView', {
     },
     getCachedViews(): string[] {
       return Array.from(this.cachedViews)
+    },
+    getSelectedTag(): RouteLocationNormalizedLoaded | undefined {
+      return this.selectedTag
     }
   },
   actions: {
@@ -98,8 +104,12 @@ export const useTagsViewStore = defineStore('tagsView', {
     },
     // 删除所有tag
     delAllVisitedViews() {
+      const userStore = useUserStoreWithOut()
+
       // const affixTags = this.visitedViews.filter((tag) => tag.meta.affix)
-      this.visitedViews = []
+      this.visitedViews = userStore.getUser
+        ? this.visitedViews.filter((tag) => tag?.meta?.affix)
+        : []
     },
     // 删除其他
     delOthersViews(view: RouteLocationNormalizedLoaded) {
@@ -145,6 +155,18 @@ export const useTagsViewStore = defineStore('tagsView', {
           break
         }
       }
+    },
+    // 设置当前选中的 tag
+    setSelectedTag(tag: RouteLocationNormalizedLoaded) {
+      this.selectedTag = tag
+    },
+    setTitle(title: string, path?: string) {
+      for (const v of this.visitedViews) {
+        if (v.path === (path ?? this.selectedTag?.path)) {
+          v.meta.title = title
+          break
+        }
+      }
     }
   },
   persist: false

+ 5 - 5
src/store/modules/user.ts

@@ -15,7 +15,7 @@ interface UserVO {
 
 interface UserInfoVO {
   // USER 缓存
-  permissions: string[]
+  permissions: Set<string>
   roles: string[]
   isSetUser: boolean
   user: UserVO
@@ -23,7 +23,7 @@ interface UserInfoVO {
 
 export const useUserStore = defineStore('admin-user', {
   state: (): UserInfoVO => ({
-    permissions: [],
+    permissions: new Set<string>(),
     roles: [],
     isSetUser: false,
     user: {
@@ -34,7 +34,7 @@ export const useUserStore = defineStore('admin-user', {
     }
   }),
   getters: {
-    getPermissions(): string[] {
+    getPermissions(): Set<string> {
       return this.permissions
     },
     getRoles(): string[] {
@@ -57,7 +57,7 @@ export const useUserStore = defineStore('admin-user', {
       if (!userInfo) {
         userInfo = await getInfo()
       }
-      this.permissions = userInfo.permissions
+      this.permissions = new Set(userInfo.permissions)
       this.roles = userInfo.roles
       this.user = userInfo.user
       this.isSetUser = true
@@ -85,7 +85,7 @@ export const useUserStore = defineStore('admin-user', {
       this.resetState()
     },
     resetState() {
-      this.permissions = []
+      this.permissions = new Set<string>()
       this.roles = []
       this.isSetUser = false
       this.user = {

+ 6 - 0
src/utils/constants.ts

@@ -457,3 +457,9 @@ export const BpmProcessInstanceStatus = {
   REJECT: 3, // 审批不通过
   CANCEL: 4 // 已取消
 }
+
+export const BpmAutoApproveType = {
+  NONE: 0, // 不自动通过
+  APPROVE_ALL: 1, // 仅审批一次,后续重复的审批节点均自动通过
+  APPROVE_SEQUENT: 2, // 仅针对连续审批的节点自动通过
+}

+ 29 - 0
src/utils/download.ts

@@ -33,6 +33,10 @@ const download = {
   markdown: (data: Blob, fileName: string) => {
     download0(data, fileName, 'text/markdown')
   },
+  // 下载 Json 方法
+  json: (data: Blob, fileName: string) => {
+    download0(data, fileName, 'application/json')
+  },
   // 下载图片(允许跨域)
   image: ({
     url,
@@ -65,6 +69,31 @@ const download = {
       a.download = 'image.png'
       a.click()
     }
+  },
+  base64ToFile: (base64: any, fileName: string) => {
+    // 将base64按照 , 进行分割 将前缀  与后续内容分隔开
+    const data = base64.split(',')
+    // 利用正则表达式 从前缀中获取图片的类型信息(image/png、image/jpeg、image/webp等)
+    const type = data[0].match(/:(.*?);/)[1]
+    // 从图片的类型信息中 获取具体的文件格式后缀(png、jpeg、webp)
+    const suffix = type.split('/')[1]
+    // 使用atob()对base64数据进行解码  结果是一个文件数据流 以字符串的格式输出
+    const bstr = window.atob(data[1])
+    // 获取解码结果字符串的长度
+    let n = bstr.length
+    // 根据解码结果字符串的长度创建一个等长的整形数字数组
+    // 但在创建时 所有元素初始值都为 0
+    const u8arr = new Uint8Array(n)
+    // 将整形数组的每个元素填充为解码结果字符串对应位置字符的UTF-16 编码单元
+    while (n--) {
+      // charCodeAt():获取给定索引处字符对应的 UTF-16 代码单元
+      u8arr[n] = bstr.charCodeAt(n)
+    }
+
+    // 将File文件对象返回给方法的调用者
+    return new File([u8arr], `${fileName}.${suffix}`, {
+      type: type
+    })
   }
 }
 

+ 4 - 15
src/utils/permission.ts

@@ -1,4 +1,6 @@
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import {hasPermission} from "@/directives/permission/hasPermi";
+
 
 const { t } = useI18n() // 国际化
 
@@ -7,21 +9,8 @@ const { t } = useI18n() // 国际化
  * @param {Array} value 校验值
  * @returns {Boolean}
  */
-export function checkPermi(value: string[]) {
-  if (value && value instanceof Array && value.length > 0) {
-    const { wsCache } = useCache()
-    const permissionDatas = value
-    const all_permission = '*:*:*'
-    const userInfo = wsCache.get(CACHE_KEY.USER)
-    const permissions = userInfo?.permissions || []
-    const hasPermission = permissions.some((permission: string) => {
-      return all_permission === permission || permissionDatas.includes(permission)
-    })
-    return !!hasPermission
-  } else {
-    console.error(t('permission.hasPermission'))
-    return false
-  }
+export function checkPermi(permission: string[]) {
+  return hasPermission(permission)
 }
 
 /**

+ 161 - 60
src/views/bpm/model/CategoryDraggableModel.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="flex items-center h-50px">
+  <div class="flex items-center h-50px" v-memo="[categoryInfo.name, isCategorySorting]">
     <!-- 头部:分类名 -->
     <div class="flex items-center">
       <el-tooltip content="拖动排序" v-if="isCategorySorting">
@@ -13,7 +13,7 @@
       <div class="color-gray-600 text-16px"> ({{ categoryInfo.modelList?.length || 0 }}) </div>
     </div>
     <!-- 头部:操作 -->
-    <div class="flex-1 flex" v-if="!isCategorySorting">
+    <div class="flex-1 flex" v-show="!isCategorySorting">
       <div
         v-if="categoryInfo.modelList.length > 0"
         class="ml-20px flex items-center"
@@ -69,16 +69,17 @@
   <el-collapse-transition>
     <div v-show="isExpand">
       <el-table
+        v-if="modelList && modelList.length > 0"
         :class="categoryInfo.name"
         ref="tableRef"
-        :header-cell-style="{ backgroundColor: isDark ? '' : '#edeff0', paddingLeft: '10px' }"
-        :cell-style="{ paddingLeft: '10px' }"
-        :row-style="{ height: '68px' }"
         :data="modelList"
         row-key="id"
+        :header-cell-style="tableHeaderStyle"
+        :cell-style="tableCellStyle"
+        :row-style="{ height: '68px' }"
       >
         <el-table-column label="流程名" prop="name" min-width="150">
-          <template #default="scope">
+          <template #default="{ row }">
             <div class="flex items-center">
               <el-tooltip content="拖动排序" v-if="isModelSorting">
                 <Icon
@@ -86,27 +87,25 @@
                   class="drag-icon cursor-move text-#8a909c mr-10px"
                 />
               </el-tooltip>
-              <el-image :src="scope.row.icon" class="h-38px w-38px mr-10px rounded" />
-              {{ scope.row.name }}
+              <el-image v-if="row.icon" :src="row.icon" class="h-38px w-38px mr-10px rounded" />
+              {{ row.name }}
             </div>
           </template>
         </el-table-column>
         <el-table-column label="可见范围" prop="startUserIds" min-width="150">
-          <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 }}
+          <template #default="{ row }">
+            <el-text v-if="!row.startUsers?.length"> 全部可见 </el-text>
+            <el-text v-else-if="row.startUsers.length === 1">
+              {{ 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('、')"
+                :content="row.startUsers.map((user: any) => user.nickname).join('、')"
               >
-                {{ scope.row.startUsers[0].nickname }}等 {{ scope.row.startUsers.length }} 人可见
+                {{ row.startUsers[0].nickname }}等 {{ row.startUsers.length }} 人可见
               </el-tooltip>
             </el-text>
           </template>
@@ -158,17 +157,26 @@
               link
               type="primary"
               @click="openModelForm('update', scope.row.id)"
-              v-hasPermi="['bpm:model:update']"
+              v-if="hasPermiUpdate"
               :disabled="!isManagerUser(scope.row)"
             >
               修改
             </el-button>
+            <el-button
+              link
+              type="primary"
+              @click="openModelForm('copy', scope.row.id)"
+              v-if="hasPermiUpdate"
+              :disabled="!isManagerUser(scope.row)"
+            >
+              复制
+            </el-button>
             <el-button
               link
               class="!ml-5px"
               type="primary"
               @click="handleDeploy(scope.row)"
-              v-hasPermi="['bpm:model:deploy']"
+              v-if="hasPermiDeploy"
               :disabled="!isManagerUser(scope.row)"
             >
               发布
@@ -176,28 +184,33 @@
             <el-dropdown
               class="!align-middle ml-5px"
               @command="(command) => handleModelCommand(command, scope.row)"
-              v-hasPermi="['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete']"
+              v-if="hasPermiMore"
             >
               <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 command="handleDefinitionList" v-if="hasPermiPdQuery">
                     历史
                   </el-dropdown-item>
                   <el-dropdown-item
                     command="handleChangeState"
-                    v-if="checkPermi(['bpm:model:update']) && scope.row.processDefinition"
+                    v-if="hasPermiUpdate && scope.row.processDefinition"
                     :disabled="!isManagerUser(scope.row)"
                   >
                     {{ scope.row.processDefinition.suspensionState === 1 ? '停用' : '启用' }}
                   </el-dropdown-item>
+                  <el-dropdown-item
+                    type="danger"
+                    command="handleClean"
+                    v-if="checkPermi(['bpm:model:clean'])"
+                    :disabled="!isManagerUser(scope.row)"
+                  >
+                    清理
+                  </el-dropdown-item>
                   <el-dropdown-item
                     type="danger"
                     command="handleDelete"
-                    v-if="checkPermi(['bpm:model:delete'])"
+                    v-if="hasPermiDelete"
                     :disabled="!isManagerUser(scope.row)"
                   >
                     删除
@@ -226,16 +239,11 @@
       </div>
     </template>
   </Dialog>
-
-  <!-- 表单弹窗:添加流程模型 -->
-  <ModelForm :categoryId="categoryInfo.code" ref="modelFormRef" @success="emit('success')" />
 </template>
 
 <script lang="ts" setup>
-import ModelForm from './ModelForm.vue'
 import { CategoryApi, CategoryVO } from '@/api/bpm/category'
 import Sortable from 'sortablejs'
-import { propTypes } from '@/utils/propTypes'
 import { formatDate } from '@/utils/formatTime'
 import * as ModelApi from '@/api/bpm/model'
 import * as FormApi from '@/api/bpm/form'
@@ -244,14 +252,49 @@ import { BpmModelFormType } from '@/utils/constants'
 import { checkPermi } from '@/utils/permission'
 import { useUserStoreWithOut } from '@/store/modules/user'
 import { useAppStore } from '@/store/modules/app'
-import { cloneDeep } from 'lodash-es'
+import { cloneDeep, isEqual } from 'lodash-es'
+import { useTagsView } from '@/hooks/web/useTagsView'
+import { useDebounceFn } from '@vueuse/core'
 
 defineOptions({ name: 'BpmModel' })
 
-const props = defineProps({
-  categoryInfo: propTypes.object.def([]), // 分类后的数据
-  isCategorySorting: propTypes.bool.def(false) // 是否分类在排序
-})
+// 优化 Props 类型定义
+interface UserInfo {
+  nickname: string
+  [key: string]: any
+}
+
+interface ProcessDefinition {
+  deploymentTime: string
+  version: number
+  suspensionState: number
+}
+
+interface ModelInfo {
+  id: number
+  name: string
+  icon?: string
+  startUsers?: UserInfo[]
+  processDefinition?: ProcessDefinition
+  formType?: number
+  formId?: number
+  formName?: string
+  formCustomCreatePath?: string
+  managerUserIds?: number[]
+  [key: string]: any
+}
+
+interface CategoryInfoProps {
+  id: number
+  name: string
+  modelList: ModelInfo[]
+}
+
+const props = defineProps<{
+  categoryInfo: CategoryInfoProps
+  isCategorySorting: boolean
+}>()
+
 const emit = defineEmits(['success'])
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
@@ -260,10 +303,37 @@ const userStore = useUserStoreWithOut() // 用户信息缓存
 const isDark = computed(() => useAppStore().getIsDark) // 是否黑暗模式
 
 const isModelSorting = ref(false) // 是否正处于排序状态
-const originalData: any = ref([]) // 原始数据
-const modelList: any = ref([]) // 模型列表
+const originalData = ref<ModelInfo[]>([]) // 原始数据
+const modelList = ref<ModelInfo[]>([]) // 模型列表
 const isExpand = ref(false) // 是否处于展开状态
 
+// 使用 computed 优化表格样式计算
+const tableHeaderStyle = computed(() => ({
+  backgroundColor: isDark.value ? '' : '#edeff0',
+  paddingLeft: '10px'
+}))
+
+const tableCellStyle = computed(() => ({
+  paddingLeft: '10px'
+}))
+
+/** 权限校验:通过 computed 解决列表的卡顿问题 */
+const hasPermiUpdate = computed(() => {
+  return checkPermi(['bpm:model:update'])
+})
+const hasPermiDelete = computed(() => {
+  return checkPermi(['bpm:model:delete'])
+})
+const hasPermiDeploy = computed(() => {
+  return checkPermi(['bpm:model:deploy'])
+})
+const hasPermiMore = computed(() => {
+  return checkPermi(['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete'])
+})
+const hasPermiPdQuery = computed(() => {
+  return checkPermi(['bpm:process-definition:query'])
+})
+
 /** '更多'操作按钮 */
 const handleModelCommand = (command: string, row: any) => {
   switch (command) {
@@ -276,6 +346,9 @@ const handleModelCommand = (command: string, row: any) => {
     case 'handleChangeState':
       handleChangeState(row)
       break
+    case 'handleClean':
+      handleClean(row)
+      break
     default:
       break
   }
@@ -309,6 +382,19 @@ const handleDelete = async (row: any) => {
   } catch {}
 }
 
+/** 清理按钮操作 */
+const handleClean = async (row: any) => {
+  try {
+    // 清理的二次确认
+    await message.confirm('是否确认清理流程名字为"' + row.name + '"的数据项?')
+    // 发起清理
+    await ModelApi.cleanModel(row.id)
+    message.success('清理成功')
+    // 刷新列表
+    emit('success')
+  } catch {}
+}
+
 /** 更新状态操作 */
 const handleChangeState = async (row: any) => {
   const state = row.processDefinition.suspensionState
@@ -405,14 +491,15 @@ const handleModelSortCancel = () => {
 
 /** 创建拖拽实例 */
 const tableRef = ref()
-const initSort = () => {
+const initSort = useDebounceFn(() => {
   const table = document.querySelector(`.${props.categoryInfo.name} .el-table__body-wrapper tbody`)
+  if (!table) return
+
   Sortable.create(table, {
     group: 'shared',
     animation: 150,
     draggable: '.el-table__row',
     handle: '.drag-icon',
-    // 结束拖动事件
     onEnd: ({ newDraggableIndex, oldDraggableIndex }) => {
       if (oldDraggableIndex !== newDraggableIndex) {
         modelList.value.splice(
@@ -423,15 +510,18 @@ const initSort = () => {
       }
     }
   })
-}
+}, 200)
 
 /** 更新 modelList 模型列表 */
-const updateModeList = () => {
-  modelList.value = cloneDeep(props.categoryInfo.modelList)
-  if (props.categoryInfo.modelList.length > 0) {
-    isExpand.value = true
+const updateModeList = useDebounceFn(() => {
+  const newModelList = props.categoryInfo.modelList
+  if (!isEqual(modelList.value, newModelList)) {
+    modelList.value = cloneDeep(newModelList)
+    if (newModelList?.length > 0) {
+      isExpand.value = true
+    }
   }
-}
+}, 100)
 
 /** 重命名弹窗确定 */
 const renameCategoryVisible = ref(false)
@@ -466,26 +556,31 @@ const handleDeleteCategory = async () => {
 }
 
 /** 添加流程模型弹窗 */
-const modelFormRef = ref()
-const openModelForm = (type: string, id?: number) => {
+const tagsView = useTagsView()
+const openModelForm = async (type: string, id?: number) => {
   if (type === 'create') {
-    push({ name: 'BpmModelCreate' })
+    await push({ name: 'BpmModelCreate' })
   } else {
-    push({
+    await push({
       name: 'BpmModelUpdate',
-      params: { id }
+      params: { id, type }
     })
+    // 设置标题
+    if (type === 'copy') {
+      tagsView.setTitle('复制流程')
+    }
   }
 }
 
-watch(() => props.categoryInfo.modelList, updateModeList, { immediate: true })
-watch(
-  () => props.isCategorySorting,
-  (val) => {
-    if (val) isExpand.value = false
-  },
-  { immediate: true }
-)
+watchEffect(() => {
+  if (props.categoryInfo?.modelList) {
+    updateModeList()
+  }
+
+  if (props.isCategorySorting) {
+    isExpand.value = false
+  }
+})
 </script>
 
 <style lang="scss">
@@ -502,10 +597,16 @@ watch(
 }
 </style>
 <style lang="scss" scoped>
-:deep() {
-  .el-table__cell {
+.category-draggable-model {
+  :deep(.el-table__cell) {
     overflow: hidden;
     border-bottom: none !important;
   }
+
+  // 优化表格渲染性能
+  :deep(.el-table__body) {
+    will-change: transform;
+    transform: translateZ(0);
+  }
 }
 </style>

+ 0 - 440
src/views/bpm/model/ModelForm.vue

@@ -1,440 +0,0 @@
-<template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle" width="600">
-    <el-form
-      ref="formRef"
-      v-loading="formLoading"
-      :model="formData"
-      :rules="formRules"
-      label-width="110px"
-    >
-      <el-form-item label="流程标识" prop="key">
-        <el-input v-model="formData.key" :disabled="!!formData.id" placeholder="请输入流标标识" />
-        <el-tooltip
-          v-if="!formData.id"
-          class="item"
-          content="新建后,流程标识不可修改!"
-          effect="light"
-          placement="top"
-        >
-          <i class="el-icon-question" style="padding-left: 5px"></i>
-        </el-tooltip>
-        <el-tooltip v-else class="item" content="流程标识不可修改!" effect="light" placement="top">
-          <i class="el-icon-question" style="padding-left: 5px"></i>
-        </el-tooltip>
-      </el-form-item>
-      <el-form-item label="流程名称" prop="name">
-        <el-input
-          v-model="formData.name"
-          :disabled="!!formData.id"
-          clearable
-          placeholder="请输入流程名称"
-        />
-      </el-form-item>
-      <el-form-item label="流程分类" prop="category">
-        <el-select
-          v-model="formData.category"
-          clearable
-          placeholder="请选择流程分类"
-          style="width: 100%"
-        >
-          <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">
-        <UploadImg v-model="formData.icon" :limit="1" height="64px" width="64px" />
-      </el-form-item>
-      <el-form-item label="流程描述" prop="description">
-        <el-input v-model="formData.description" clearable type="textarea" />
-      </el-form-item>
-      <el-form-item label="流程类型" prop="type">
-        <el-radio-group v-model="formData.type">
-          <el-radio
-            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_TYPE)"
-            :key="dict.value"
-            :label="dict.value"
-          >
-            {{ dict.label }}
-          </el-radio>
-        </el-radio-group>
-      </el-form-item>
-      <el-form-item label="表单类型" prop="formType">
-        <el-radio-group v-model="formData.formType">
-          <el-radio
-            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)"
-            :key="dict.value"
-            :label="dict.value"
-          >
-            {{ dict.label }}
-          </el-radio>
-        </el-radio-group>
-      </el-form-item>
-      <el-form-item v-if="formData.formType === 10" label="流程表单" prop="formId">
-        <el-select v-model="formData.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="formData.formType === 20"
-        label="表单提交路由"
-        prop="formCustomCreatePath"
-      >
-        <el-input
-          v-model="formData.formCustomCreatePath"
-          placeholder="请输入表单提交路由"
-          style="width: 330px"
-        />
-        <el-tooltip
-          class="item"
-          content="自定义表单的提交路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/create.vue"
-          effect="light"
-          placement="top"
-        >
-          <i class="el-icon-question" style="padding-left: 5px"></i>
-        </el-tooltip>
-      </el-form-item>
-      <el-form-item v-if="formData.formType === 20" label="表单查看地址" prop="formCustomViewPath">
-        <el-input
-          v-model="formData.formCustomViewPath"
-          placeholder="请输入表单查看的组件地址"
-          style="width: 330px"
-        />
-        <el-tooltip
-          class="item"
-          content="自定义表单的查看组件地址,使用 Vue 的组件地址,例如说:bpm/oa/leave/detail.vue"
-          effect="light"
-          placement="top"
-        >
-          <i class="el-icon-question" style="padding-left: 5px"></i>
-        </el-tooltip>
-      </el-form-item>
-      <el-form-item label="是否可见" prop="visible">
-        <el-radio-group v-model="formData.visible">
-          <el-radio
-            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
-            :key="dict.value as string"
-            :label="dict.value"
-          >
-            {{ dict.label }}
-          </el-radio>
-        </el-radio-group>
-      </el-form-item>
-      <el-form-item label="谁可以发起" prop="startUserType">
-        <el-select
-          v-model="formData.startUserType"
-          placeholder="请选择谁可以发起"
-          @change="handleStartUserTypeChange"
-        >
-          <el-option label="全员" :value="0" />
-          <el-option label="指定人员" :value="1" />
-          <el-option label="均不可提交" :value="2" />
-        </el-select>
-        <div v-if="formData.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">
-        <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>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-  <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
-</template>
-<script lang="ts" setup>
-import { propTypes } from '@/utils/propTypes'
-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, 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' })
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-const userStore = useUserStoreWithOut() // 用户信息缓存
-const props = defineProps({
-  categoryId: propTypes.number
-})
-const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('') // 弹窗的标题
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
-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 formRules = reactive({
-  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' }],
-  formType: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
-  formId: [{ required: true, message: '流程表单不能为空', trigger: 'blur' }],
-  formCustomCreatePath: [{ required: true, message: '表单提交路由不能为空', trigger: 'blur' }],
-  formCustomViewPath: [{ required: true, message: '表单查看地址不能为空', trigger: 'blur' }],
-  visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
-  managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }]
-})
-const formRef = ref() // 表单 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) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  // 修改时,设置数据
-  if (id) {
-    formLoading.value = true
-    try {
-      formData.value = await ModelApi.getModel(id)
-    } 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)
-  }
-  // 获得流程表单的下拉框的数据
-  formList.value = await FormApi.getFormSimpleList()
-  // 查询流程分类列表
-  categoryList.value = await CategoryApi.getCategorySimpleList()
-  // 查询用户列表
-  userList.value = await UserApi.getSimpleUserList()
-  if (props.categoryId) {
-    formData.value.category = props.categoryId
-  }
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formData.value as unknown as ModelApi.ModelVO
-    if (formType.value === 'create') {
-      await ModelApi.createModel(data)
-      // 提示,引导用户做后续的操作
-      await ElMessageBox.alert(
-        '<strong>新建模型成功!</strong>后续需要执行如下 2 个步骤:' +
-          '<div>1. 点击【设计流程】按钮,绘制流程图</div>' +
-          '<div>2. 点击【发布流程】按钮,完成流程的最终发布</div>' +
-          '另外,每次流程修改后,都需要点击【发布流程】按钮,才能正式生效!!!',
-        '重要提示',
-        {
-          dangerouslyUseHTMLString: true,
-          type: 'success'
-        }
-      )
-    } else {
-      await ModelApi.updateModel(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    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: []
-  }
-  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>

+ 15 - 187
src/views/bpm/model/editor/index.vue

@@ -12,10 +12,12 @@
       :additionalModel="controlForm.additionalModel"
       :model="model"
       @save="save"
+      :process-id="modelKey"
+      :process-name="modelName"
     />
     <!-- 流程属性器,负责编辑每个流程节点的属性 -->
     <MyProcessPenal
-      v-if="isModelerReady && modeler"
+      v-if="modeler"
       key="penal"
       :bpmnModeler="modeler"
       :prefix="controlForm.prefix"
@@ -37,8 +39,8 @@ defineOptions({ name: 'BpmModelEditor' })
 
 const props = defineProps<{
   modelId?: string
-  modelKey?: string
-  modelName?: string
+  modelKey: string
+  modelName: string
   value?: string
 }>()
 
@@ -51,10 +53,13 @@ const formType = ref(20)
 provide('formFields', formFields)
 provide('formType', formType)
 
-const xmlString = ref<string>('') // BPMN XML
+// 注入流程数据
+const xmlString = inject('processData') as Ref
+// 注入模型数据
+const modelData = inject('modelData') as Ref
+
 const modeler = shallowRef() // BPMN Modeler
 const processDesigner = ref()
-const isModelerReady = ref(false)
 const controlForm = ref({
   simulation: true,
   labelEditing: false,
@@ -65,154 +70,26 @@ 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 = async (item) => {
-  try {
-    modeler.value = item
-    // 等待 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 initModeler = async (item: any) => {
+  //先初始化模型数据
+  model.value = modelData.value
+  modeler.value = item
 }
 
 /** 添加/修改模型 */
 const save = async (bpmnXml: string) => {
   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)
-    }
+    emit('success', bpmnXml)
   } catch (error) {
     console.error('保存失败:', error)
     message.error('保存失败')
   }
 }
 
-// 监听 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
@@ -220,55 +97,6 @@ onBeforeUnmount(() => {
     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 }
-  }
-}
-
-/** 获取SVG字符串 */
-const saveSVG = async () => {
-  if (!modeler.value) {
-    return { svg: undefined }
-  }
-  try {
-    return await modeler.value.saveSVG()
-  } catch (error) {
-    console.error('获取SVG失败:', error)
-    return { svg: undefined }
-  }
-}
-
-/** 刷新视图 */
-const refresh = async () => {
-  if (processDesigner.value?.refresh && modeler.value) {
-    try {
-      await modeler.value.importXML(xmlString.value)
-      processDesigner.value.refresh()
-    } catch (error) {
-      console.error('刷新视图失败:', error)
-    }
-  }
-}
-
-// 暴露必要的属性和方法给父组件
-defineExpose({
-  modeler,
-  isModelerReady,
-  saveXML,
-  saveSVG,
-  refresh
-})
 </script>
 <style lang="scss">
 .process-panel__container {

+ 25 - 53
src/views/bpm/model/form/BasicInfo.vue

@@ -62,7 +62,7 @@
       <el-radio-group v-model="modelData.visible">
         <el-radio
           v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
-          :key="dict.value"
+          :key="dict.value as string"
           :value="dict.value"
         >
           {{ dict.label }}
@@ -77,7 +77,6 @@
       >
         <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
@@ -97,21 +96,12 @@
           />
         </div>
         <el-button type="primary" link @click="openStartUserSelect">
-          <Icon icon="ep:plus" />选择人员
+          <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">
+    <el-form-item label="流程管理员" prop="managerUserIds" class="mb-20px">
+      <div class="flex flex-wrap gap-2">
         <div
           v-for="user in selectedManagerUsers"
           :key="user.id"
@@ -142,14 +132,11 @@
 <script lang="ts" setup>
 import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
 import { UserVO } from '@/api/system/user'
+import { CategoryVO } from '@/api/bpm/category'
 
 const props = defineProps({
-  modelValue: {
-    type: Object,
-    required: true
-  },
   categoryList: {
-    type: Array,
+    type: Array as PropType<CategoryVO[]>,
     required: true
   },
   userList: {
@@ -158,8 +145,6 @@ const props = defineProps({
   }
 })
 
-const emit = defineEmits(['update:modelValue'])
-
 const formRef = ref()
 const selectedStartUsers = ref<UserVO[]>([])
 const selectedManagerUsers = ref<UserVO[]>([])
@@ -177,27 +162,30 @@ const rules = {
 }
 
 // 创建本地数据副本
-const modelData = computed({
-  get: () => props.modelValue,
-  set: (val) => emit('update:modelValue', val)
-})
+const modelData = defineModel<any>()
 
 // 初始化选中的用户
 watch(
-  () => props.modelValue,
+  () => modelData.value,
   (newVal) => {
     if (newVal.startUserIds?.length) {
       selectedStartUsers.value = props.userList.filter((user: UserVO) =>
         newVal.startUserIds.includes(user.id)
       ) as UserVO[]
+    } else {
+      selectedStartUsers.value = []
     }
     if (newVal.managerUserIds?.length) {
       selectedManagerUsers.value = props.userList.filter((user: UserVO) =>
         newVal.managerUserIds.includes(user.id)
       ) as UserVO[]
+    } else {
+      selectedManagerUsers.value = []
     }
   },
-  { immediate: true }
+  {
+    immediate: true
+  }
 )
 
 /** 打开发起人选择 */
@@ -215,58 +203,42 @@ const openManagerUserSelect = () => {
 /** 处理用户选择确认 */
 const handleUserSelectConfirm = (_, users: UserVO[]) => {
   if (currentSelectType.value === 'start') {
-    selectedStartUsers.value = users
-    emit('update:modelValue', {
+    modelData.value = {
       ...modelData.value,
       startUserIds: users.map((u) => u.id)
-    })
+    }
   } else {
-    selectedManagerUsers.value = users
-    emit('update:modelValue', {
+    modelData.value = {
       ...modelData.value,
       managerUserIds: users.map((u) => u.id)
-    })
+    }
   }
 }
 
 /** 处理发起人类型变化 */
 const handleStartUserTypeChange = (value: number) => {
   if (value !== 1) {
-    selectedStartUsers.value = []
-    emit('update:modelValue', {
+    modelData.value = {
       ...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 = {
     ...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 = {
     ...modelData.value,
     managerUserIds: modelData.value.managerUserIds.filter((id: number) => id !== user.id)
-  })
+  }
 }
 
 /** 表单校验 */

+ 289 - 0
src/views/bpm/model/form/ExtraSettings.vue

@@ -0,0 +1,289 @@
+<template>
+  <el-form ref="formRef" :model="modelData" label-width="120px" class="mt-20px">
+    <el-form-item class="mb-20px">
+      <template #label>
+        <el-text size="large" tag="b">提交人权限</el-text>
+      </template>
+      <div class="flex flex-col">
+        <el-checkbox v-model="modelData.allowCancelRunningProcess" label="允许撤销审批中的申请" />
+        <div class="ml-22px">
+          <el-text type="info"> 第一个审批节点通过后,提交人仍可撤销申请 </el-text>
+        </div>
+      </div>
+    </el-form-item>
+    <el-form-item v-if="modelData.processIdRule" class="mb-20px">
+      <template #label>
+        <el-text size="large" tag="b">流程编码</el-text>
+      </template>
+      <div class="flex flex-col">
+        <div>
+          <el-input
+            v-model="modelData.processIdRule.prefix"
+            class="w-130px!"
+            placeholder="前缀"
+            :disabled="!modelData.processIdRule.enable"
+          >
+            <template #prepend>
+              <el-checkbox v-model="modelData.processIdRule.enable" />
+            </template>
+          </el-input>
+          <el-select
+            v-model="modelData.processIdRule.infix"
+            class="w-130px! ml-5px"
+            placeholder="中缀"
+            :disabled="!modelData.processIdRule.enable"
+          >
+            <el-option
+              v-for="item in timeOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+          <el-input
+            v-model="modelData.processIdRule.postfix"
+            class="w-80px! ml-5px"
+            placeholder="后缀"
+            :disabled="!modelData.processIdRule.enable"
+          />
+          <el-input-number
+            v-model="modelData.processIdRule.length"
+            class="w-120px! ml-5px"
+            :min="5"
+            :disabled="!modelData.processIdRule.enable"
+          />
+        </div>
+        <div class="ml-22px" v-if="modelData.processIdRule.enable">
+          <el-text type="info"> 编码示例:{{ numberExample }} </el-text>
+        </div>
+      </div>
+    </el-form-item>
+    <el-form-item class="mb-20px">
+      <template #label>
+        <el-text size="large" tag="b">自动去重</el-text>
+      </template>
+      <div class="flex flex-col">
+        <div>
+          <el-text> 同一审批人在流程中重复出现时: </el-text>
+        </div>
+        <el-radio-group v-model="modelData.autoApprovalType">
+          <div class="flex flex-col">
+            <el-radio :value="0">不自动通过</el-radio>
+            <el-radio :value="1">仅审批一次,后续重复的审批节点均自动通过</el-radio>
+            <el-radio :value="2">仅针对连续审批的节点自动通过</el-radio>
+          </div>
+        </el-radio-group>
+      </div>
+    </el-form-item>
+    <el-form-item v-if="modelData.titleSetting" class="mb-20px">
+      <template #label>
+        <el-text size="large" tag="b">标题设置</el-text>
+      </template>
+      <div class="flex flex-col">
+        <el-radio-group v-model="modelData.titleSetting.enable">
+          <div class="flex flex-col">
+            <el-radio :value="false"
+              >系统默认 <el-text type="info"> 展示流程名称 </el-text></el-radio
+            >
+            <el-radio :value="true">
+              自定义标题
+              <el-text>
+                <el-tooltip content="输入字符 '{' 即可插入表单字段" effect="light" placement="top">
+                  <Icon icon="ep:question-filled" class="ml-5px" />
+                </el-tooltip>
+              </el-text>
+            </el-radio>
+          </div>
+        </el-radio-group>
+        <el-mention
+          v-if="modelData.titleSetting.enable"
+          v-model="modelData.titleSetting.title"
+          type="textarea"
+          prefix="{"
+          split="}"
+          whole
+          :options="formFieldOptions4Title"
+          placeholder="请插入表单字段(输入 '{' 可以选择表单字段)或输入文本"
+          class="w-600px!"
+        />
+      </div>
+    </el-form-item>
+    <el-form-item
+      v-if="modelData.summarySetting && modelData.formType === BpmModelFormType.NORMAL"
+      class="mb-20px"
+    >
+      <template #label>
+        <el-text size="large" tag="b">摘要设置</el-text>
+      </template>
+      <div class="flex flex-col">
+        <el-radio-group v-model="modelData.summarySetting.enable">
+          <div class="flex flex-col">
+            <el-radio :value="false">
+              系统默认 <el-text type="info"> 展示表单前 3 个字段 </el-text>
+            </el-radio>
+            <el-radio :value="true"> 自定义摘要 </el-radio>
+          </div>
+        </el-radio-group>
+        <el-select
+          class="w-500px!"
+          v-if="modelData.summarySetting.enable"
+          v-model="modelData.summarySetting.summary"
+          multiple
+          placeholder="请选择要展示的表单字段"
+        >
+          <el-option
+            v-for="item in formFieldOptions4Summary"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </div>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import dayjs from 'dayjs'
+import { BpmAutoApproveType, BpmModelFormType } from '@/utils/constants'
+import * as FormApi from '@/api/bpm/form'
+import { parseFormFields } from '@/components/FormCreate/src/utils'
+import { ProcessVariableEnum } from '@/components/SimpleProcessDesignerV2/src/consts'
+
+const modelData = defineModel<any>()
+
+/** 自定义 ID 流程编码 */
+const timeOptions = ref([
+  {
+    value: '',
+    label: '无'
+  },
+  {
+    value: 'DAY',
+    label: '精确到日'
+  },
+  {
+    value: 'HOUR',
+    label: '精确到时'
+  },
+  {
+    value: 'MINUTE',
+    label: '精确到分'
+  },
+  {
+    value: 'SECOND',
+    label: '精确到秒'
+  }
+])
+const numberExample = computed(() => {
+  if (modelData.value.processIdRule.enable) {
+    let infix = ''
+    switch (modelData.value.processIdRule.infix) {
+      case 'DAY':
+        infix = dayjs().format('YYYYMMDD')
+        break
+      case 'HOUR':
+        infix = dayjs().format('YYYYMMDDHH')
+        break
+      case 'MINUTE':
+        infix = dayjs().format('YYYYMMDDHHmm')
+        break
+      case 'SECOND':
+        infix = dayjs().format('YYYYMMDDHHmmss')
+        break
+      default:
+        break
+    }
+    return (
+      modelData.value.processIdRule.prefix +
+      infix +
+      modelData.value.processIdRule.postfix +
+      '1'.padStart(modelData.value.processIdRule.length - 1, '0')
+    )
+  } else {
+    return ''
+  }
+})
+
+/** 表单选项 */
+const formField = ref<Array<{ field: string; title: string }>>([])
+const formFieldOptions4Title = computed(() => {
+  let cloneFormField = formField.value.map((item) => {
+    return {
+      label: item.title,
+      value: item.field
+    }
+  })
+  // 固定添加发起人 ID 字段
+  cloneFormField.unshift({
+    label: ProcessVariableEnum.PROCESS_DEFINITION_NAME,
+    value: '流程名称'
+  })
+  cloneFormField.unshift({
+    label: ProcessVariableEnum.START_TIME,
+    value: '发起时间'
+  })
+  cloneFormField.unshift({
+    label: ProcessVariableEnum.START_USER_ID,
+    value: '发起人'
+  })
+  return cloneFormField
+})
+const formFieldOptions4Summary = computed(() => {
+  return formField.value.map((item) => {
+    return {
+      label: item.title,
+      value: item.field
+    }
+  })
+})
+
+/** 兼容以前未配置更多设置的流程 */
+const initData = () => {
+  if (!modelData.value.processIdRule) {
+    modelData.value.processIdRule = {
+      enable: false,
+      prefix: '',
+      infix: '',
+      postfix: '',
+      length: 5
+    }
+  }
+  if (!modelData.value.autoApprovalType) {
+    modelData.value.autoApprovalType = BpmAutoApproveType.NONE
+  }
+  if (!modelData.value.titleSetting) {
+    modelData.value.titleSetting = {
+      enable: false,
+      title: ''
+    }
+  }
+  if (!modelData.value.summarySetting) {
+    modelData.value.summarySetting = {
+      enable: false,
+      summary: []
+    }
+  }
+}
+defineExpose({ initData })
+
+/** 监听表单 ID 变化,加载表单数据 */
+watch(
+  () => modelData.value.formId,
+  async (newFormId) => {
+    if (newFormId && modelData.value.formType === BpmModelFormType.NORMAL) {
+      const data = await FormApi.getForm(newFormId)
+      const result: Array<{ field: string; title: string }> = []
+      if (data.fields) {
+        data.fields.forEach((fieldStr: string) => {
+          parseFormFields(JSON.parse(fieldStr), result)
+        })
+      }
+      formField.value = result
+    } else {
+      formField.value = []
+    }
+  },
+  { immediate: true }
+)
+</script>

+ 1 - 10
src/views/bpm/model/form/FormDesign.vue

@@ -70,25 +70,16 @@ 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 modelData = defineModel<any>()
 
 // 表单预览数据
 const formPreview = ref({

+ 6 - 170
src/views/bpm/model/form/ProcessDesign.vue

@@ -6,10 +6,7 @@
       :model-id="modelData.id"
       :model-key="modelData.key"
       :model-name="modelData.name"
-      :value="currentBpmnXml"
-      ref="bpmnEditorRef"
       @success="handleDesignSuccess"
-      @init-finished="handleEditorInit"
     />
   </template>
 
@@ -21,10 +18,7 @@
       :model-key="modelData.key"
       :model-name="modelData.name"
       :start-user-ids="modelData.startUserIds"
-      :value="currentSimpleModel"
-      ref="simpleEditorRef"
       @success="handleDesignSuccess"
-      @init-finished="handleEditorInit"
     />
   </template>
 </template>
@@ -34,137 +28,16 @@ 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()
+const modelData = defineModel<any>()
 
-  // 初始化完成后,设置初始值
-  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 processData = inject('processData') as Ref
 
 /** 表单校验 */
 const validate = async () => {
   try {
     // 获取最新的流程数据
-    const processData = await getProcessData()
-    if (!processData) {
+    if (!processData.value) {
       throw new Error('请设计流程')
     }
     return true
@@ -172,27 +45,19 @@ const validate = async () => {
     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)
+    //更新表单的模型数据部分
+    modelData.value = newModelData
   }
 }
 
@@ -200,36 +65,7 @@ const handleDesignSuccess = async (data?: any) => {
 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
+  validate
 })
 </script>

+ 89 - 120
src/views/bpm/model/form/index.vue

@@ -67,12 +67,12 @@
         </div>
 
         <!-- 第三步:流程设计 -->
-        <ProcessDesign
-          v-if="currentStep === 2"
-          v-model="formData"
-          ref="processDesignRef"
-          @success="handleDesignSuccess"
-        />
+        <ProcessDesign v-if="currentStep === 2" v-model="formData" ref="processDesignRef" />
+
+        <!-- 第四步:更多设置 -->
+        <div v-show="currentStep === 3" class="mx-auto w-700px">
+          <ExtraSettings v-model="formData" ref="extraSettingsRef" />
+        </div>
       </div>
     </div>
   </ContentWrap>
@@ -83,14 +83,15 @@ 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 { CategoryApi, CategoryVO } from '@/api/bpm/category'
 import * as UserApi from '@/api/system/user'
 import { useUserStoreWithOut } from '@/store/modules/user'
-import { BpmModelFormType, BpmModelType } from '@/utils/constants'
+import { BpmModelFormType, BpmModelType, BpmAutoApproveType } from '@/utils/constants'
 import BasicInfo from './BasicInfo.vue'
 import FormDesign from './FormDesign.vue'
 import ProcessDesign from './ProcessDesign.vue'
 import { useTagsViewStore } from '@/store/modules/tagsView'
+import ExtraSettings from './ExtraSettings.vue'
 
 const router = useRouter()
 const { delView } = useTagsViewStore() // 视图操作
@@ -102,6 +103,7 @@ const userStore = useUserStoreWithOut()
 const basicInfoRef = ref()
 const formDesignRef = ref()
 const processDesignRef = ref()
+const extraSettingsRef = ref()
 
 /** 步骤校验函数 */
 const validateBasic = async () => {
@@ -118,11 +120,13 @@ const validateProcess = async () => {
   await processDesignRef.value?.validate()
 }
 
-const currentStep = ref(0) // 步骤控制
+const currentStep = ref(-1) // 步骤控制。-1 用于,一开始全部不展示等当前页面数据初始化完成
+
 const steps = [
   { title: '基本信息', validator: validateBasic },
   { title: '表单设计', validator: validateForm },
-  { title: '流程设计', validator: validateProcess }
+  { title: '流程设计', validator: validateProcess },
+  { title: '更多设置', validator: null }
 ]
 
 // 表单数据
@@ -140,14 +144,36 @@ const formData: any = ref({
   formCustomViewPath: '',
   visible: true,
   startUserType: undefined,
-  managerUserType: undefined,
   startUserIds: [],
-  managerUserIds: []
+  managerUserIds: [],
+  allowCancelRunningProcess: true,
+  processIdRule: {
+    enable: false,
+    prefix: '',
+    infix: '',
+    postfix: '',
+    length: 5
+  },
+  autoApprovalType: BpmAutoApproveType.NONE,
+  titleSetting: {
+    enable: false,
+    title: ''
+  },
+  summarySetting: {
+    enable: false,
+    summary: []
+  }
 })
 
+//流程数据
+const processData = ref<any>()
+
+provide('processData', processData)
+provide('modelData', formData)
+
 // 数据列表
 const formList = ref([])
-const categoryList = ref([])
+const categoryList = ref<CategoryVO[]>([])
 const userList = ref<UserApi.UserVO[]>([])
 
 /** 初始化数据 */
@@ -156,8 +182,16 @@ const initData = async () => {
   if (modelId) {
     // 修改场景
     formData.value = await ModelApi.getModel(modelId)
+    formData.value.startUserType = formData.value.startUserIds?.length > 0 ? 1 : 0
+    // 复制场景
+    if (route.params.type === 'copy') {
+      delete formData.value.id
+      formData.value.name += '副本'
+      formData.value.key += '_copy'
+    }
   } else {
     // 新增场景
+    formData.value.startUserType = 0 // 全体
     formData.value.managerUserIds.push(userStore.getUser.id)
   }
 
@@ -167,59 +201,57 @@ const initData = async () => {
   categoryList.value = await CategoryApi.getCategorySimpleList()
   // 获取用户列表
   userList.value = await UserApi.getSimpleUserList()
+
+  // 最终,设置 currentStep 切换到第一步
+  currentStep.value = 0
+
+  // 兼容,以前未配置更多设置的流程
+  extraSettingsRef.value.initData()
 }
 
+/** 根据类型切换流程数据 */
+watch(
+  async () => formData.value.type,
+  () => {
+    if (formData.value.type === BpmModelType.BPMN) {
+      processData.value = formData.value.bpmnXml
+    } else if (formData.value.type === BpmModelType.SIMPLE) {
+      processData.value = formData.value.simpleModel
+    }
+    console.log('加载流程数据', processData.value)
+  },
+  {
+    immediate: true
+  }
+)
+
 /** 校验所有步骤数据是否完整 */
 const validateAllSteps = async () => {
   try {
     // 基本信息校验
-    await basicInfoRef.value?.validate()
-    if (!formData.value.key || !formData.value.name || !formData.value.category) {
+    try {
+      await validateBasic()
+    } catch (error) {
       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)
-    ) {
+    try {
+      await validateForm()
+    } catch (error) {
       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
-      }
+    // 表单设计校验
+    try {
+      await validateProcess()
+    } catch (error) {
+      currentStep.value = 2
+      throw new Error('请设计流程')
     }
 
     return true
@@ -239,20 +271,6 @@ const handleSave = async () => {
       ...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)
@@ -308,20 +326,6 @@ const handleDeploy = async () => {
       ...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)
@@ -344,60 +348,25 @@ const handleDeploy = async () => {
 /** 步骤切换处理 */
 const handleStepClick = async (index: number) => {
   try {
-    // 如果是切换到第三步(流程设计),需要校验key和name
-    if (index === 2) {
-      if (!formData.value.key || !formData.value.name) {
-        message.warning('请先填写流程标识和流程名称')
-        return
-      }
+    console.log('index', index)
+    if (index !== 0) {
+      await validateBasic()
     }
-
-    // 保存当前步骤的数据
-    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()
-        }
-      }
+    if (index !== 1) {
+      await validateForm()
+    }
+    if (index !== 2) {
+      await validateProcess()
     }
 
     // 切换步骤
     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 = () => {
   // 先删除当前页签

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

@@ -85,8 +85,6 @@
     </div>
   </ContentWrap>
 
-  <!-- 表单弹窗:添加/修改流程 -->
-  <ModelForm ref="formRef" @success="getList" />
   <!-- 表单弹窗:添加分类 -->
   <CategoryForm ref="categoryFormRef" @success="getList" />
   <!-- 弹窗:表单详情 -->
@@ -99,7 +97,6 @@
 import draggable from 'vuedraggable'
 import { CategoryApi } from '@/api/bpm/category'
 import * as ModelApi from '@/api/bpm/model'
-import ModelForm from './ModelForm.vue'
 import CategoryForm from '../category/CategoryForm.vue'
 import { cloneDeep } from 'lodash-es'
 import CategoryDraggableModel from './CategoryDraggableModel.vue'
@@ -123,7 +120,6 @@ const handleQuery = () => {
 }
 
 /** 添加/修改操作 */
-const formRef = ref()
 const openForm = (type: string, id?: number) => {
   if (type === 'create') {
     push({ name: 'BpmModelCreate' })
@@ -206,7 +202,7 @@ const getList = async () => {
 }
 
 /** 初始化 **/
-onMounted(() => {
+onActivated(() => {
   getList()
 })
 </script>

+ 86 - 40
src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue

@@ -44,8 +44,26 @@
               :rows="4"
             />
           </el-form-item>
+          <el-form-item
+            v-if="runningTask.signEnable"
+            label="签名"
+            prop="signPicUrl"
+            ref="approveSignFormRef"
+          >
+            <el-button @click="signRef.open()">点击签名</el-button>
+            <el-image
+              class="w-90px h-40px ml-5px"
+              v-if="approveReasonForm.signPicUrl"
+              :src="approveReasonForm.signPicUrl"
+              :preview-src-list="[approveReasonForm.signPicUrl]"
+            />
+          </el-form-item>
           <el-form-item>
-            <el-button :disabled="formLoading" type="success" @click="handleAudit(true, approveFormRef)">
+            <el-button
+              :disabled="formLoading"
+              type="success"
+              @click="handleAudit(true, approveFormRef)"
+            >
               {{ getButtonDisplayName(OperationButtonType.APPROVE) }}
             </el-button>
             <el-button @click="closePropover('approve', approveFormRef)"> 取消 </el-button>
@@ -86,7 +104,11 @@
             />
           </el-form-item>
           <el-form-item>
-            <el-button :disabled="formLoading" type="danger" @click="handleAudit(false,rejectFormRef)">
+            <el-button
+              :disabled="formLoading"
+              type="danger"
+              @click="handleAudit(false, rejectFormRef)"
+            >
               {{ getButtonDisplayName(OperationButtonType.REJECT) }}
             </el-button>
             <el-button @click="closePropover('reject', rejectFormRef)"> 取消 </el-button>
@@ -471,6 +493,9 @@
       <Icon :size="14" icon="ep:refresh" />&nbsp; 再次提交
     </div>
   </div>
+
+  <!-- 签名弹窗 -->
+  <SignDialog ref="signRef" @success="handleSignFinish" />
 </template>
 <script lang="ts" setup>
 import { useUserStoreWithOut } from '@/store/modules/user'
@@ -479,11 +504,13 @@ import * as TaskApi from '@/api/bpm/task'
 import * as ProcessInstanceApi from '@/api/bpm/processInstance'
 import * as UserApi from '@/api/system/user'
 import {
-  OperationButtonType,
-  OPERATION_BUTTON_NAME
+  OPERATION_BUTTON_NAME,
+  OperationButtonType
 } from '@/components/SimpleProcessDesignerV2/src/consts'
-import { BpmProcessInstanceStatus, BpmModelFormType } from '@/utils/constants'
+import { BpmModelFormType, BpmProcessInstanceStatus } from '@/utils/constants'
 import type { FormInstance, FormRules } from 'element-plus'
+import SignDialog from './SignDialog.vue'
+
 defineOptions({ name: 'ProcessInstanceBtnContainer' })
 
 const router = useRouter() // 路由
@@ -492,12 +519,12 @@ const message = useMessage() // 消息弹窗
 const userId = useUserStoreWithOut().getUser.id // 当前登录的编号
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 
-const props = defineProps< {
-  processInstance: any,  // 流程实例信息
-  processDefinition: any,  // 流程定义信息
-  userOptions: UserApi.UserVO[],
-  normalForm: any, // 流程表单 formCreate
-  normalFormApi: any, // 流程表单 formCreate Api
+const props = defineProps<{
+  processInstance: any // 流程实例信息
+  processDefinition: any // 流程定义信息
+  userOptions: UserApi.UserVO[]
+  normalForm: any // 流程表单 formCreate
+  normalFormApi: any // 流程表单 formCreate Api
   writableFields: string[] // 流程表单可以编辑的字段
 }>()
 
@@ -521,20 +548,29 @@ const approveForm = ref<any>({}) // 审批通过时,额外的补充信息
 const approveFormFApi = ref<any>({}) // approveForms 的 fAPi
 
 // 审批通过意见表单
+const reasonRequire = ref()
 const approveFormRef = ref<FormInstance>()
+const signRef = ref()
+const approveSignFormRef = ref()
 const approveReasonForm = reactive({
-  reason: ''
+  reason: '',
+  signPicUrl: ''
 })
-const approveReasonRule = reactive<FormRules<typeof approveReasonForm>>({
-  reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }],
+const approveReasonRule = computed(() => {
+  return {
+    reason: [{ required: reasonRequire.value, message: '审批意见不能为空', trigger: 'blur' }],
+    signPicUrl: [{ 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 rejectReasonRule = computed(() => {
+  return {
+    reason: [{ required: reasonRequire.value, message: '审批意见不能为空', trigger: 'blur' }]
+  }
 })
 
 // 抄送表单
@@ -555,7 +591,7 @@ const transferForm = reactive({
 })
 const transferFormRule = reactive<FormRules<typeof transferForm>>({
   assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }],
-  reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }],
+  reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }]
 })
 
 // 委派表单
@@ -566,7 +602,7 @@ const delegateForm = reactive({
 })
 const delegateFormRule = reactive<FormRules<typeof delegateForm>>({
   delegateUserId: [{ required: true, message: '接收人不能为空', trigger: 'change' }],
-  reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }],
+  reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }]
 })
 
 // 加签表单
@@ -577,7 +613,7 @@ const addSignForm = reactive({
 })
 const addSignFormRule = reactive<FormRules<typeof addSignForm>>({
   addSignUserIds: [{ required: true, message: '加签处理人不能为空', trigger: 'change' }],
-  reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }],
+  reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }]
 })
 
 // 减签表单
@@ -588,7 +624,7 @@ const deleteSignForm = reactive({
 })
 const deleteSignFormRule = reactive<FormRules<typeof deleteSignForm>>({
   deleteSignTaskId: [{ required: true, message: '减签人员不能为空', trigger: 'change' }],
- reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }],
+  reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }]
 })
 
 // 退回表单
@@ -608,7 +644,7 @@ const cancelForm = reactive({
   cancelReason: ''
 })
 const cancelFormRule = reactive<FormRules<typeof cancelForm>>({
-  cancelReason: [{ required: true, message: '取消理由不能为空', trigger: 'blur' }],
+  cancelReason: [{ required: true, message: '取消理由不能为空', trigger: 'blur' }]
 })
 
 /** 监听 approveFormFApis,实现它对应的 form-create 初始化后,隐藏掉对应的表单提交按钮 */
@@ -627,11 +663,11 @@ watch(
 const openPopover = async (type: string) => {
   if (type === 'approve') {
     // 校验流程表单
-     const valid = await validateNormalForm();
-     if (!valid) {
+    const valid = await validateNormalForm()
+    if (!valid) {
       message.warning('表单校验不通过,请先完善表单!!')
-      return;
-     }
+      return
+    }
   }
   if (type === 'return') {
     // 获取退回节点
@@ -652,7 +688,7 @@ const openPopover = async (type: string) => {
 const closePropover = (type: string, formRef: FormInstance | undefined) => {
   if (formRef) {
     formRef.resetFields()
-  } 
+  }
   popOverVisible.value[type] = false
 }
 
@@ -664,14 +700,18 @@ const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) =>
     if (!formRef) return
     await formRef.validate()
     if (pass) {
-       // 获取修改的流程变量, 暂时只支持流程表单
-       const variables = getUpdatedProcessInstanceVaiables();
+      // 获取修改的流程变量, 暂时只支持流程表单
+      const variables = getUpdatedProcessInstanceVariables()
       // 审批通过数据
       const data = {
         id: runningTask.value.id,
         reason: approveReasonForm.reason,
         variables // 审批通过, 把修改的字段值赋于流程实例变量
       }
+      // 签名
+      if (runningTask.value.signEnable) {
+        data.signPicUrl = approveReasonForm.signPicUrl
+      }
       // 多表单处理,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交
       // TODO 芋艿 任务有多表单这里要如何处理,会和可编辑的字段冲突
       const formCreateApi = approveFormFApi.value
@@ -684,10 +724,10 @@ const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) =>
       popOverVisible.value.approve = false
       message.success('审批通过成功')
     } else {
-       // 审批不通过数据
-       const data = {
+      // 审批不通过数据
+      const data = {
         id: runningTask.value.id,
-        reason: rejectReasonForm.reason,
+        reason: rejectReasonForm.reason
       }
       await TaskApi.rejectTask(data)
       popOverVisible.value.reject = false
@@ -713,7 +753,7 @@ const handleCopy = async () => {
     const data = {
       id: runningTask.value.id,
       reason: copyForm.copyReason,
-      copyUserIds:copyForm.copyUserIds
+      copyUserIds: copyForm.copyUserIds
     }
     await TaskApi.copyTask(data)
     copyFormRef.value.resetFields()
@@ -752,7 +792,6 @@ const handleTransfer = async () => {
 const handleDelegate = async () => {
   formLoading.value = true
   try {
- 
     // 1.1 校验表单
     if (!delegateFormRef.value) return
     await delegateFormRef.value.validate()
@@ -932,6 +971,7 @@ const loadTodoTask = (task: any) => {
   approveForm.value = {}
   approveFormFApi.value = {}
   runningTask.value = task
+  reasonRequire.value = task?.reasonRequire ?? false
   // 处理 approve 表单.
   if (task && task.formId && task.formConf) {
     const tempApproveForm = {}
@@ -949,23 +989,29 @@ const validateNormalForm = async () => {
     try {
       await props.normalFormApi?.validate()
     } catch {
-      valid = false;
+      valid = false
     }
-    return valid;
+    return valid
   } else {
-    return true;
+    return true
   }
 }
+
 /** 从可以编辑的流程表单字段,获取需要修改的流程实例的变量 */
-const getUpdatedProcessInstanceVaiables = ()=> {
+const getUpdatedProcessInstanceVariables = () => {
   const variables = {}
-  props.writableFields.forEach( (field) => {
-    const fieldValue = props.normalFormApi.getValue(field)
-    variables[field] = fieldValue;
+  props.writableFields.forEach((field) => {
+    variables[field] = props.normalFormApi.getValue(field)
   })
   return variables
 }
 
+/** 处理签名完成 */
+const handleSignFinish = (url: string) => {
+  approveReasonForm.signPicUrl = url
+  approveSignFormRef.value.validate('change')
+}
+
 defineExpose({ loadTodoTask })
 </script>
 

+ 15 - 20
src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue

@@ -4,7 +4,6 @@
       :flow-node="simpleModel"
       :tasks="tasks"
       :process-instance="processInstance"
-      class="process-viewer"
     />
   </div>
 </template>
@@ -20,7 +19,7 @@ const props = defineProps({
   modelView: propTypes.object,
   simpleJson: propTypes.string // Simple 模型结构数据 (json 格式)
 })
-const simpleModel = ref()
+const simpleModel = ref<any>({})
 // 用户任务
 const tasks = ref([])
 // 流程实例
@@ -82,7 +81,6 @@ const setSimpleModelNodeTaskStatus = (
     }
     return
   }
-
   // 审批节点
   if (
     simpleModel.type === NodeType.START_USER_NODE ||
@@ -98,31 +96,39 @@ const setSimpleModelNodeTaskStatus = (
     }
     // TODO 是不是还缺一个 cancel 的状态
   }
-
   // 抄送节点
   if (simpleModel.type === NodeType.COPY_TASK_NODE) {
-    // 抄送节点 只有通过和未执行状态
+    // 抄送节点,只有通过和未执行状态
     if (finishedActivityIds.includes(simpleModel.id)) {
       simpleModel.activityStatus = TaskStatusEnum.APPROVE
     } else {
       simpleModel.activityStatus = TaskStatusEnum.NOT_START
     }
   }
-  // 条件节点 对应 SequenceFlow
+  // 延迟器节点
+  if (simpleModel.type === NodeType.DELAY_TIMER_NODE) {
+    // 延迟器节点,只有通过和未执行状态
+    if (finishedActivityIds.includes(simpleModel.id)) {
+      simpleModel.activityStatus = TaskStatusEnum.APPROVE
+    } else {
+      simpleModel.activityStatus = TaskStatusEnum.NOT_START
+    }
+  }
+  // 条件节点对应 SequenceFlow
   if (simpleModel.type === NodeType.CONDITION_NODE) {
-    // 条件节点。只有通过和未执行状态
+    // 条件节点,只有通过和未执行状态
     if (finishedSequenceFlowActivityIds.includes(simpleModel.id)) {
       simpleModel.activityStatus = TaskStatusEnum.APPROVE
     } else {
       simpleModel.activityStatus = TaskStatusEnum.NOT_START
     }
   }
-
   // 网关节点
   if (
     simpleModel.type === NodeType.CONDITION_BRANCH_NODE ||
     simpleModel.type === NodeType.PARALLEL_BRANCH_NODE ||
-    simpleModel.type === NodeType.INCLUSIVE_BRANCH_NODE
+    simpleModel.type === NodeType.INCLUSIVE_BRANCH_NODE ||
+    simpleModel.type === NodeType.ROUTER_BRANCH_NODE
   ) {
     // 网关节点。只有通过和未执行状态
     if (finishedActivityIds.includes(simpleModel.id)) {
@@ -154,15 +160,4 @@ const setSimpleModelNodeTaskStatus = (
 </script>
 
 <style lang="scss" scoped>
-.process-viewer-container {
-  height: 100%;
-  width: 100%;
-  
-  :deep(.process-viewer) {
-    height: 100% !important;
-    min-height: 100%;
-    width: 100%;
-    overflow: auto;
-  }
-}
 </style>

+ 11 - 0
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue

@@ -123,6 +123,17 @@
               >
                 审批意见:{{ task.reason }}
               </div>
+              <div
+                v-if="task.signPicUrl && activity.nodeType === NodeType.USER_TASK_NODE"
+                class="text-#a5a5a5 text-13px mt-1 w-full bg-#f8f8fa p2 rounded-md"
+              >
+                签名:
+                <el-image
+                  class="w-90px h-40px ml-5px"
+                  :src="task.signPicUrl"
+                  :preview-src-list="[task.signPicUrl]"
+                />
+              </div>
             </teleport>
           </div>
           <!-- 情况二:遍历每个审批节点下的【候选的】task 任务。例如说,1)依次审批,2)未来的审批任务等 -->

+ 50 - 0
src/views/bpm/processInstance/detail/SignDialog.vue

@@ -0,0 +1,50 @@
+<template>
+  <el-dialog v-model="signDialogVisible" title="签名" width="935">
+    <div class="position-relative">
+      <Vue3Signature class="b b-solid b-gray" ref="signature" w="900px" h="400px" />
+      <el-button
+        class="pos-absolute bottom-20px right-10px"
+        type="primary"
+        text
+        size="small"
+        @click="signature.clear()"
+      >
+        <Icon icon="ep:delete" class="mr-5px" />
+        清除
+      </el-button>
+    </div>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="signDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submit"> 提交 </el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import Vue3Signature from 'vue3-signature'
+import * as FileApi from '@/api/infra/file'
+import download from '@/utils/download'
+
+const message = useMessage() // 消息弹窗
+const signDialogVisible = ref(false)
+const signature = ref()
+
+const open = async () => {
+  signDialogVisible.value = true
+}
+defineExpose({ open })
+
+const emits = defineEmits(['success'])
+const submit = async () => {
+  message.success('签名上传中请稍等。。。')
+  const res = await FileApi.updateFile({
+    file: download.base64ToFile(signature.value.save('image/png'), '签名')
+  })
+  emits('success', res.data)
+  signDialogVisible.value = false
+}
+</script>
+
+<style scoped></style>

+ 9 - 0
src/views/bpm/processInstance/index.vue

@@ -130,6 +130,15 @@
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
       <el-table-column label="流程名称" align="center" prop="name" min-width="200px" fixed="left" />
+      <el-table-column label="摘要" prop="summary" min-width="180" fixed="left">
+        <template #default="scope">
+          <div class="flex flex-col" v-if="scope.row.summary && scope.row.summary.length > 0">
+            <div v-for="(item, index) in scope.row.summary" :key="index">
+              <el-text type="info"> {{ item.key }} : {{ item.value }} </el-text>
+            </div>
+          </div>
+        </template>
+      </el-table-column>
       <el-table-column
         label="流程分类"
         align="center"

+ 4 - 121
src/views/bpm/simple/SimpleModelDesign.vue

@@ -4,9 +4,7 @@
       :model-id="modelId"
       :model-key="modelKey"
       :model-name="modelName"
-      :value="currentValue"
       @success="handleSuccess"
-      @init-finished="handleInit"
       :start-user-ids="startUserIds"
       ref="designerRef"
     />
@@ -19,137 +17,22 @@ defineOptions({
   name: 'SimpleModelDesign'
 })
 
-const props = defineProps<{
+defineProps<{
   modelId?: string
   modelKey?: string
   modelName?: string
-  value?: string
   startUserIds?: number[]
 }>()
 
-const emit = defineEmits(['success', 'init-finished'])
+const emit = defineEmits(['success'])
 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
+  console.info('handleSuccess', data)
+  if (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>

+ 1 - 0
src/views/bpm/task/copy/index.vue

@@ -44,6 +44,7 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
+      <!-- TODO 芋艿:增加摘要 -->
       <el-table-column align="center" label="流程名" prop="processInstanceName" min-width="180" />
       <el-table-column
         align="center"

+ 14 - 5
src/views/bpm/task/done/index.vue

@@ -64,7 +64,7 @@
             :value="dict.value"
           />
         </el-select>
-      </el-form-item> 
+      </el-form-item>
 
       <!-- 高级筛选 -->
       <el-form-item :style="{ position: 'absolute', right: '0px' }">
@@ -77,9 +77,9 @@
         >
           <template #reference>
             <el-button @click="showPopover = !showPopover" >
-              <Icon icon="ep:plus" class="mr-5px" />高级筛选 
+              <Icon icon="ep:plus" class="mr-5px" />高级筛选
             </el-button>
-            
+
           </template>
           <el-form-item label="流程发起人" class="bold-label" label-position="top" prop="category">
             <el-select
@@ -95,7 +95,7 @@
                 :value="category.code"
               />
             </el-select>
-          </el-form-item>          
+          </el-form-item>
           <el-form-item label="发起时间" class="bold-label" label-position="top" prop="createTime">
             <el-date-picker
               v-model="queryParams.createTime"
@@ -122,6 +122,15 @@
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
       <el-table-column align="center" label="流程" prop="processInstance.name" width="180" />
+      <el-table-column label="摘要" prop="summary" min-width="180">
+        <template #default="scope">
+          <div class="flex flex-col" v-if="scope.row.summary && scope.row.summary.length > 0">
+            <div v-for="(item, index) in scope.row.summary" :key="index">
+              <el-text type="info"> {{ item.key }} : {{ item.value }} </el-text>
+            </div>
+          </div>
+        </template>
+      </el-table-column>
       <el-table-column
         align="center"
         label="发起人"
@@ -195,7 +204,7 @@ const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   name: '',
-  category: undefined,  
+  category: undefined,
   status: undefined,
   createTime: []
 })

+ 12 - 3
src/views/bpm/task/todo/index.vue

@@ -60,9 +60,9 @@
         >
           <template #reference>
             <el-button @click="showPopover = !showPopover" >
-              <Icon icon="ep:plus" class="mr-5px" />高级筛选 
+              <Icon icon="ep:plus" class="mr-5px" />高级筛选
             </el-button>
-            
+
           </template>
           <el-form-item label="流程发起人" class="bold-label" label-position="top" prop="category">
             <el-select
@@ -78,7 +78,7 @@
                 :value="category.code"
               />
             </el-select>
-          </el-form-item>          
+          </el-form-item>
           <el-form-item label="发起时间" class="bold-label" label-position="top" prop="createTime">
             <el-date-picker
               v-model="queryParams.createTime"
@@ -105,6 +105,15 @@
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
       <el-table-column align="center" label="流程" prop="processInstance.name" width="180" />
+      <el-table-column label="摘要" prop="summary" min-width="180">
+        <template #default="scope">
+          <div class="flex flex-col" v-if="scope.row.summary && scope.row.summary.length > 0">
+            <div v-for="(item, index) in scope.row.summary" :key="index">
+              <el-text type="info"> {{ item.key }} : {{ item.value }} </el-text>
+            </div>
+          </div>
+        </template>
+      </el-table-column>
       <el-table-column
         align="center"
         label="发起人"