Răsfoiți Sursa

!595 bpm 仿钉钉/飞书模式
Merge pull request !595 from 芋道源码/feature/bpm

芋道源码 8 luni în urmă
părinte
comite
22199c64fd
95 a modificat fișierele cu 6923 adăugiri și 3987 ștergeri
  1. BIN
      .image/工作流设计器-bpmn.jpg
  2. BIN
      .image/工作流设计器-simple.jpg
  3. 13 9
      README.md
  4. 2 1
      package.json
  5. 89 82
      pnpm-lock.yaml
  6. 0 8
      src/api/bpm/activity/index.ts
  7. 10 0
      src/api/bpm/category/index.ts
  8. 12 2
      src/api/bpm/model/index.ts
  9. 17 13
      src/api/bpm/processInstance/index.ts
  10. 16 15
      src/api/bpm/task/index.ts
  11. 9 3
      src/api/mall/trade/delivery/pickUpStore/index.ts
  12. 2 0
      src/api/pay/app/index.ts
  13. 1 0
      src/assets/svgs/bpm/add-user.svg
  14. 0 0
      src/assets/svgs/bpm/approve.svg
  15. 1 0
      src/assets/svgs/bpm/auditor.svg
  16. 0 0
      src/assets/svgs/bpm/cancel.svg
  17. 1 0
      src/assets/svgs/bpm/condition.svg
  18. 1 0
      src/assets/svgs/bpm/copy.svg
  19. 0 0
      src/assets/svgs/bpm/finish.svg
  20. 1 0
      src/assets/svgs/bpm/parallel.svg
  21. 0 0
      src/assets/svgs/bpm/reject.svg
  22. 0 0
      src/assets/svgs/bpm/running.svg
  23. 1 0
      src/assets/svgs/bpm/simple-process-bg.svg
  24. 1 0
      src/assets/svgs/bpm/starter.svg
  25. 0 1
      src/components/FormCreate/src/components/useApiSelect.tsx
  26. 43 0
      src/components/FormCreate/src/utils/index.ts
  27. 56 10
      src/components/SimpleProcessDesignerV2/src/NodeHandler.vue
  28. 12 1
      src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue
  29. 45 78
      src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue
  30. 140 0
      src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue
  31. 48 0
      src/components/SimpleProcessDesignerV2/src/SimpleProcessViewer.vue
  32. 38 12
      src/components/SimpleProcessDesignerV2/src/consts.ts
  33. 2 1
      src/components/SimpleProcessDesignerV2/src/index.ts
  34. 96 79
      src/components/SimpleProcessDesignerV2/src/node.ts
  35. 1 1
      src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue
  36. 78 11
      src/components/SimpleProcessDesignerV2/src/nodes-config/CopyTaskNodeConfig.vue
  37. 0 1
      src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue
  38. 72 68
      src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue
  39. 26 8
      src/components/SimpleProcessDesignerV2/src/nodes/CopyTaskNode.vue
  40. 90 1
      src/components/SimpleProcessDesignerV2/src/nodes/EndEventNode.vue
  41. 36 14
      src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue
  42. 233 0
      src/components/SimpleProcessDesignerV2/src/nodes/InclusiveNode.vue
  43. 22 19
      src/components/SimpleProcessDesignerV2/src/nodes/ParallelNode.vue
  44. 96 11
      src/components/SimpleProcessDesignerV2/src/nodes/StartUserNode.vue
  45. 99 13
      src/components/SimpleProcessDesignerV2/src/nodes/UserTaskNode.vue
  46. 8 0
      src/components/SimpleProcessDesignerV2/src/utils.ts
  47. 552 516
      src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss
  48. 152 0
      src/components/UserSelectForm/index.vue
  49. 327 612
      src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue
  50. 70 0
      src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json
  51. 5 1
      src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue
  52. 283 0
      src/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue
  53. 3 3
      src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue
  54. 1 1
      src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue
  55. 115 0
      src/components/bpmnProcessDesigner/package/theme/index.scss
  56. 3 4
      src/router/modules/remaining.ts
  57. 8 0
      src/utils/constants.ts
  58. 1 0
      src/utils/formCreate.ts
  59. 3 2
      src/views/bpm/category/CategoryForm.vue
  60. 8 14
      src/views/bpm/definition/index.vue
  61. 5 1
      src/views/bpm/form/editor/index.vue
  62. 2 1
      src/views/bpm/form/index.vue
  63. 532 0
      src/views/bpm/model/CategoryDraggableModel.vue
  64. 7 1
      src/views/bpm/model/ModelForm.vue
  65. 157 340
      src/views/bpm/model/index.vue
  66. 404 0
      src/views/bpm/model/index_old.vue
  67. 259 0
      src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue
  68. 232 190
      src/views/bpm/processInstance/create/index.vue
  69. 267 0
      src/views/bpm/processInstance/create/index_old.vue
  70. 37 30
      src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue
  71. 683 172
      src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue
  72. 168 0
      src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue
  73. 61 133
      src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
  74. 194 135
      src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue
  75. 0 89
      src/views/bpm/processInstance/detail/dialog/TaskDelegateForm.vue
  76. 0 90
      src/views/bpm/processInstance/detail/dialog/TaskReturnForm.vue
  77. 0 99
      src/views/bpm/processInstance/detail/dialog/TaskSignCreateForm.vue
  78. 0 89
      src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue
  79. 0 106
      src/views/bpm/processInstance/detail/dialog/TaskSignList.vue
  80. 0 89
      src/views/bpm/processInstance/detail/dialog/TaskTransferForm.vue
  81. 258 395
      src/views/bpm/processInstance/detail/index.vue
  82. 0 318
      src/views/bpm/processInstance/detail/index_new.vue
  83. 104 53
      src/views/bpm/processInstance/index.vue
  84. 8 2
      src/views/bpm/simple/SimpleModelDesign.vue
  85. 11 3
      src/views/bpm/task/copy/index.vue
  86. 5 1
      src/views/infra/build/index.vue
  87. 132 24
      src/views/mall/trade/delivery/pickUpOrder/index.vue
  88. 143 0
      src/views/mall/trade/delivery/pickUpStore/DeliveryPickUpStoreBindForm.vue
  89. 265 0
      src/views/mall/trade/delivery/pickUpStore/components/StoreStaffTableSelect.vue
  90. 18 1
      src/views/mall/trade/delivery/pickUpStore/index.vue
  91. 14 6
      src/views/mall/trade/order/form/OrderPickUpForm.vue
  92. 1 1
      src/views/mall/trade/order/index.vue
  93. 1 1
      src/views/member/user/detail/UserOrderList.vue
  94. 6 1
      src/views/pay/app/components/AppForm.vue
  95. 0 1
      src/views/pay/app/components/channel/WeixinChannelForm.vue

BIN
.image/工作流设计器-bpmn.jpg


BIN
.image/工作流设计器-simple.jpg


+ 13 - 9
README.md

@@ -120,18 +120,22 @@
 
 ### 工作流程
 
-|     | 功能    | 描述                                     |
-|-----|-------|----------------------------------------|
-| 🚀  | 流程模型  | 配置工作流的流程模型,支持文件导入与在线设计流程图,提供 7 种任务分配规则 |
-| 🚀  | 流程表单  | 拖动表单元素生成相应的工作流表单,覆盖 Element UI 所有的表单组件 |
-| 🚀  | 用户分组  | 自定义用户分组,可用于工作流的审批分组                    |
-| 🚀  | 我的流程  | 查看我发起的工作流程,支持新建、取消流程等操作,高亮流程图、审批时间线    |
-| 🚀  | 待办任务  | 查看自己【未】审批的工作任务,支持通过、不通过、转发、委派、退回等操作    |
-| 🚀  | 已办任务  | 查看自己【已】审批的工作任务,未来会支持回退操作               |
-| 🚀  | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 |
+|    | 功能    | 描述                                      |
+|----|-------|-----------------------------------------|
+| 🚀 | 流程模型  | 配置工作流的流程模型,支持 BPMN 和仿钉钉/飞书设计器           |
+| 🚀 | 流程表单  | 拖动表单元素生成相应的工作流表单,覆盖 Element UI 所有的表单组件  |
+| 🚀 | 用户分组  | 自定义用户分组,可用于工作流的审批分组                     |
+| 🚀 | 我的流程  | 查看我发起的工作流程,支持新建、取消流程等操作,高亮流程图、审批时间线     |
+| 🚀 | 待办任务  | 查看自己【未】审批的工作任务,支持通过、不通过、转派、委派、退回、加减签等操作 |
+| 🚀 | 已办任务  | 查看自己【已】审批的工作任务,支持流程预测,展示未来审批人信息         |
+| 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批  |
 
 ![功能图](/.image/common/bpm-feature.png)
 
+| BPMN 设计器                     | 钉钉/飞书设计器                       |
+|------------------------------|--------------------------------|
+| ![](/.image/工作流设计器-bpmn.jpg) | ![](/.image/工作流设计器-simple.jpg) |
+
 ### 支付系统
 
 |     | 功能   | 描述                        |

+ 2 - 1
package.json

@@ -64,6 +64,7 @@
     "pinia-plugin-persistedstate": "^3.2.1",
     "qrcode": "^1.5.3",
     "qs": "^6.12.0",
+    "sortablejs": "^1.15.3",
     "steady-xml": "^0.1.0",
     "url": "^0.11.3",
     "video.js": "^7.21.5",
@@ -95,7 +96,7 @@
     "@vitejs/plugin-vue": "^5.0.4",
     "@vitejs/plugin-vue-jsx": "^3.1.0",
     "autoprefixer": "^10.4.17",
-    "bpmn-js": "8.9.0",
+    "bpmn-js": "8.10.0",
     "bpmn-js-properties-panel": "0.46.0",
     "consola": "^3.2.3",
     "eslint": "^8.57.0",

+ 89 - 82
pnpm-lock.yaml

@@ -13,10 +13,10 @@ importers:
         version: 2.3.1(vue@3.5.12(typescript@5.3.3))
       '@form-create/designer':
         specifier: ^3.2.6
-        version: 3.2.7(vue@3.5.12(typescript@5.3.3))
+        version: 3.2.8(vue@3.5.12(typescript@5.3.3))
       '@form-create/element-ui':
         specifier: ^3.2.11
-        version: 3.2.11(vue@3.5.12(typescript@5.3.3))
+        version: 3.2.13(vue@3.5.12(typescript@5.3.3))
       '@iconify/iconify':
         specifier: ^3.1.1
         version: 3.1.1
@@ -125,6 +125,9 @@ importers:
       qs:
         specifier: ^6.12.0
         version: 6.12.1
+      sortablejs:
+        specifier: ^1.15.3
+        version: 1.15.3
       steady-xml:
         specifier: ^0.1.0
         version: 0.1.0
@@ -214,11 +217,11 @@ importers:
         specifier: ^10.4.17
         version: 10.4.19(postcss@8.4.38)
       bpmn-js:
-        specifier: 8.9.0
-        version: 8.9.0
+        specifier: 8.10.0
+        version: 8.10.0
       bpmn-js-properties-panel:
         specifier: 0.46.0
-        version: 0.46.0(bpmn-js@8.9.0)
+        version: 0.46.0(bpmn-js@8.10.0)
       consola:
         specifier: ^3.2.3
         version: 3.2.3
@@ -296,7 +299,7 @@ importers:
         version: 0.8.0(rollup@4.17.1)
       unplugin-vue-components:
         specifier: ^0.25.2
-        version: 0.25.2(@babel/parser@7.25.8)(rollup@4.17.1)(vue@3.5.12(typescript@5.3.3))
+        version: 0.25.2(@babel/parser@7.26.2)(rollup@4.17.1)(vue@3.5.12(typescript@5.3.3))
       vite:
         specifier: 5.1.4
         version: 5.1.4(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4)
@@ -451,16 +454,16 @@ packages:
     resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/helper-string-parser@7.25.7':
-    resolution: {integrity: sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==, tarball: https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz}
+  '@babel/helper-string-parser@7.25.9':
+    resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==, tarball: https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz}
     engines: {node: '>=6.9.0'}
 
   '@babel/helper-validator-identifier@7.22.20':
     resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/helper-validator-identifier@7.25.7':
-    resolution: {integrity: sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==, tarball: https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz}
+  '@babel/helper-validator-identifier@7.25.9':
+    resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==, tarball: https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz}
     engines: {node: '>=6.9.0'}
 
   '@babel/helper-validator-option@7.23.5':
@@ -484,8 +487,8 @@ packages:
     engines: {node: '>=6.0.0'}
     hasBin: true
 
-  '@babel/parser@7.25.8':
-    resolution: {integrity: sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==, tarball: https://registry.npmmirror.com/@babel/parser/-/parser-7.25.8.tgz}
+  '@babel/parser@7.26.2':
+    resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==, tarball: https://registry.npmmirror.com/@babel/parser/-/parser-7.26.2.tgz}
     engines: {node: '>=6.0.0'}
     hasBin: true
 
@@ -961,8 +964,8 @@ packages:
     resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/types@7.25.8':
-    resolution: {integrity: sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==, tarball: https://registry.npmmirror.com/@babel/types/-/types-7.25.8.tgz}
+  '@babel/types@7.26.0':
+    resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==, tarball: https://registry.npmmirror.com/@babel/types/-/types-7.26.0.tgz}
     engines: {node: '>=6.9.0'}
 
   '@bpmn-io/diagram-js-ui@0.2.3':
@@ -1070,7 +1073,7 @@ packages:
       postcss-selector-parser: ^6.0.13
 
   '@ctrl/tinycolor@3.6.1':
-    resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==, tarball: https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz}
+    resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==}
     engines: {node: '>=10'}
 
   '@dual-bundle/import-meta-resolve@4.0.0':
@@ -1238,13 +1241,13 @@ packages:
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
 
   '@floating-ui/core@1.6.1':
-    resolution: {integrity: sha512-42UH54oPZHPdRHdw6BgoBD6cg/eVTmVrFcgeRDM3jbO7uxSoipVcmcIGFcA5jmOHO5apcyvBhkSKES3fQJnu7A==, tarball: https://registry.npmmirror.com/@floating-ui/core/-/core-1.6.1.tgz}
+    resolution: {integrity: sha512-42UH54oPZHPdRHdw6BgoBD6cg/eVTmVrFcgeRDM3jbO7uxSoipVcmcIGFcA5jmOHO5apcyvBhkSKES3fQJnu7A==}
 
   '@floating-ui/dom@1.6.4':
-    resolution: {integrity: sha512-0G8R+zOvQsAG1pg2Q99P21jiqxqGBW1iRe/iXHsBRBxnpXKFI8QwbB4x5KmYLggNO5m34IQgOIu9SCRfR/WWiQ==, tarball: https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.6.4.tgz}
+    resolution: {integrity: sha512-0G8R+zOvQsAG1pg2Q99P21jiqxqGBW1iRe/iXHsBRBxnpXKFI8QwbB4x5KmYLggNO5m34IQgOIu9SCRfR/WWiQ==}
 
   '@floating-ui/utils@0.2.2':
-    resolution: {integrity: sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==, tarball: https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.2.tgz}
+    resolution: {integrity: sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==}
 
   '@form-create/component-elm-checkbox@3.2.8':
     resolution: {integrity: sha512-ol/SyzzeDueUTd87MPnYydOe7Sc6cL8S/Uhv5QmWofMY1TuuBet9DPb65JjyS6Lk51/cl3TabvtJj93EAxL6KA==, tarball: https://registry.npmmirror.com/@form-create/component-elm-checkbox/-/component-elm-checkbox-3.2.8.tgz}
@@ -1264,8 +1267,8 @@ packages:
   '@form-create/component-elm-tree@3.2.9':
     resolution: {integrity: sha512-5NG4YeFZ5jzN9Aa0JFuFD8OGKXBqSHSN0KRgxxUgdhzRg8hcRq/JODuN7yYMa7YrBP0ecTKyel8Q4ufR5Ct8iw==, tarball: https://registry.npmmirror.com/@form-create/component-elm-tree/-/component-elm-tree-3.2.9.tgz}
 
-  '@form-create/component-elm-upload@3.2.9':
-    resolution: {integrity: sha512-PdYlUCRs7x/zQjkDkTX9q3116ysKUPZ4R4OwzhSc430JPLSVUCx/CqlhenbAnqZFEj5khwnvppbYSzrTTaDa4A==, tarball: https://registry.npmmirror.com/@form-create/component-elm-upload/-/component-elm-upload-3.2.9.tgz}
+  '@form-create/component-elm-upload@3.2.13':
+    resolution: {integrity: sha512-qngh1Hzb/Oo51gbh3LDiMmUnDaa2+k7sXS4GEZujoDuKCctBjG60y3pi214CmOqBq9PiynM8knf6yQKqpjlRqA==, tarball: https://registry.npmmirror.com/@form-create/component-elm-upload/-/component-elm-upload-3.2.13.tgz}
 
   '@form-create/component-subform@3.1.34':
     resolution: {integrity: sha512-OJcFH/7MTHx7JLEjDK/weS27qfuFWAI+OK+gXTJ2jIt9aZkGWF/EWkjetiJLt5a0KMw4Z15wOS2XCY9pVK9vlA==, tarball: https://registry.npmmirror.com/@form-create/component-subform/-/component-subform-3.1.34.tgz}
@@ -1273,18 +1276,18 @@ packages:
   '@form-create/component-wangeditor@3.1.20':
     resolution: {integrity: sha512-lAjpltmYfr3a2AeXasCehGsZNL/1WB6vWqqV9TIsJ4pleTr0/D/oPwEYQjfv+gG+NoB2Sa25SRGhtlnephjyhg==, tarball: https://registry.npmmirror.com/@form-create/component-wangeditor/-/component-wangeditor-3.1.20.tgz}
 
-  '@form-create/core@3.2.11':
-    resolution: {integrity: sha512-xcaAxFSpAaVRWSpZ3ikrr89OmGidtN1y2YC7mQcQ/Hs7KvdbipH2I27JF5qm98+S7gs/e3Z9jrscngmSwsLw7g==, tarball: https://registry.npmmirror.com/@form-create/core/-/core-3.2.11.tgz}
+  '@form-create/core@3.2.13':
+    resolution: {integrity: sha512-HVLfZ5gf9DRO74OJTw3bt/GwFXhyBWvMmrOG9WkRTEQEMIeGOWudH843iaYp2ljgJN6jrn3RcCfONC9nzAmk8g==, tarball: https://registry.npmmirror.com/@form-create/core/-/core-3.2.13.tgz}
     peerDependencies:
       vue: ^3.1.0
 
-  '@form-create/designer@3.2.7':
-    resolution: {integrity: sha512-jLpX51yXt2SOmsGOiDey5wq6K6gQLfd7CcGtW6zH2tDQTJd4ddS/QstVKmei6ddIwA9GWuk3JWnktGLk4ry2sg==, tarball: https://registry.npmmirror.com/@form-create/designer/-/designer-3.2.7.tgz}
+  '@form-create/designer@3.2.8':
+    resolution: {integrity: sha512-SgrGiWOFaQTARAmysepHDtFyRi97rERrlkv1joz+DCOAzZME3RKRTXVqA7ALzJ2jI3psiCosGAK4rPSLh6EvgA==, tarball: https://registry.npmmirror.com/@form-create/designer/-/designer-3.2.8.tgz}
     peerDependencies:
       vue: ^3.1.5
 
-  '@form-create/element-ui@3.2.11':
-    resolution: {integrity: sha512-cJpKuu5zGNJK5TlsXTLqfc972aAVYk4q2ljn0ERfxM89oRl+2tkatOVr2vPYqGj/Z4Ufpr1R/ZP+RGGl2jVIHQ==, tarball: https://registry.npmmirror.com/@form-create/element-ui/-/element-ui-3.2.11.tgz}
+  '@form-create/element-ui@3.2.13':
+    resolution: {integrity: sha512-b/ilL9/huwQhXhGM3irzKTqlF7n69ld/CPiLQzuzSEUFC4VqBNFcy8kpCQ9/j4+VvuEPCvro1ax3v4V7Mxol9g==, tarball: https://registry.npmmirror.com/@form-create/element-ui/-/element-ui-3.2.13.tgz}
     peerDependencies:
       vue: ^3.1.0
 
@@ -1437,7 +1440,7 @@ packages:
     resolution: {integrity: sha512-s2t+1oVtGDV6KtqfCXtUOhxfeYvOdDF90IVm+nMs/6bUP0HeGZLslguuL/AibpwtfL4FA/oCsIu/RhwapgAdJw==}
 
   '@rollup/plugin-virtual@3.0.2':
-    resolution: {integrity: sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==, tarball: https://registry.npmmirror.com/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz}
+    resolution: {integrity: sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==}
     engines: {node: '>=14.0.0'}
     peerDependencies:
       rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
@@ -1615,7 +1618,7 @@ packages:
     os: [win32]
 
   '@swc/core@1.7.26':
-    resolution: {integrity: sha512-f5uYFf+TmMQyYIoxkn/evWhNGuUzC730dFwAKGwBVHHVoPyak1/GvJUm6i1SKl+2Hrj9oN0i3WSoWWZ4pgI8lw==, tarball: https://registry.npmmirror.com/@swc/core/-/core-1.7.26.tgz}
+    resolution: {integrity: sha512-f5uYFf+TmMQyYIoxkn/evWhNGuUzC730dFwAKGwBVHHVoPyak1/GvJUm6i1SKl+2Hrj9oN0i3WSoWWZ4pgI8lw==}
     engines: {node: '>=10'}
     peerDependencies:
       '@swc/helpers': '*'
@@ -1624,10 +1627,10 @@ packages:
         optional: true
 
   '@swc/counter@0.1.3':
-    resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==, tarball: https://registry.npmmirror.com/@swc/counter/-/counter-0.1.3.tgz}
+    resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
 
   '@swc/types@0.1.12':
-    resolution: {integrity: sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==, tarball: https://registry.npmmirror.com/@swc/types/-/types-0.1.12.tgz}
+    resolution: {integrity: sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==}
 
   '@sxzz/popperjs-es@2.11.7':
     resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==, tarball: https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz}
@@ -1781,7 +1784,7 @@ packages:
     resolution: {integrity: sha512-1CQjuSrgbv1/dhmcfQ83eVyYbvGyqhTvb2Opxr0QCV+iJ4J6/J+XWQ3Om59WiwCd1MN3rDUHasx5XRrpUtewYQ==, tarball: https://registry.npmmirror.com/@types/video.js/-/video.js-7.3.58.tgz}
 
   '@types/web-bluetooth@0.0.16':
-    resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==, tarball: https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz}
+    resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
 
   '@types/web-bluetooth@0.0.20':
     resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
@@ -2127,19 +2130,19 @@ packages:
     resolution: {integrity: sha512-/1vjTol8SXnx6xewDEKfS0Ra//ncg4Hb0DaZiwKf7drgfMsKFExQ+FnnENcN6efPen+1kIzhLQoGSy0eDUVOMg==}
 
   '@vueuse/core@9.13.0':
-    resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==, tarball: https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz}
+    resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
 
   '@vueuse/metadata@10.9.0':
     resolution: {integrity: sha512-iddNbg3yZM0X7qFY2sAotomgdHK7YJ6sKUvQqbvwnf7TmaVPxS4EJydcNsVejNdS8iWCtDk+fYXr7E32nyTnGA==}
 
   '@vueuse/metadata@9.13.0':
-    resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==, tarball: https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz}
+    resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
 
   '@vueuse/shared@10.9.0':
     resolution: {integrity: sha512-Uud2IWncmAfJvRaFYzv5OHDli+FbOzxiVEQdLCKQKLyhz94PIyFC3CHcH7EDMwIn8NPtD06+PNbC/PiO0LGLtw==}
 
   '@vueuse/shared@9.13.0':
-    resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==, tarball: https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz}
+    resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
 
   '@wangeditor/basic-modules@1.1.7':
     resolution: {integrity: sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==}
@@ -2347,7 +2350,7 @@ packages:
     engines: {node: '>=8'}
 
   async-validator@4.2.5:
-    resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==, tarball: https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz}
+    resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
 
   async@3.2.5:
     resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==}
@@ -2432,11 +2435,11 @@ packages:
   bpmn-js-token-simulation@0.10.0:
     resolution: {integrity: sha512-QuZQ/KVXKt9Vl+XENyOBoTW2Aw+uKjuBlKdCJL6El7AyM7DkJ5bZkSYURshId1SkBDdYg2mJ1flSmsrhGuSfwg==}
 
-  bpmn-js@8.9.0:
-    resolution: {integrity: sha512-cthSxiJUpEHspiUKiL0YA8/mRCYngNKwALWieLKPtFo42n+vWTFgmxnASNRwhxpPEbSXjYuTah1lZ0lSyLWPpw==}
+  bpmn-js@8.10.0:
+    resolution: {integrity: sha512-NozeOi01qL0ZdVq8+5hWZcikyEvgrP1yzCBqlhSufJdHFsnEMBCwn2bJJ0B/6JgX+IBwy1sk/Uw+Ds8rQ8vfrw==, tarball: https://registry.npmmirror.com/bpmn-js/-/bpmn-js-8.10.0.tgz}
 
   bpmn-moddle@7.1.3:
-    resolution: {integrity: sha512-ZcBfw0NSOdYTSXFKEn7MOXHItz7VfLZTrFYKO8cK6V8ZzGjCcdiLIOiw7Lctw1PJsihhLiZQS8Htj2xKf+NwCg==}
+    resolution: {integrity: sha512-ZcBfw0NSOdYTSXFKEn7MOXHItz7VfLZTrFYKO8cK6V8ZzGjCcdiLIOiw7Lctw1PJsihhLiZQS8Htj2xKf+NwCg==, tarball: https://registry.npmmirror.com/bpmn-moddle/-/bpmn-moddle-7.1.3.tgz}
 
   brace-expansion@1.1.11:
     resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
@@ -2733,7 +2736,7 @@ packages:
     engines: {node: '>= 6'}
 
   css.escape@1.5.1:
-    resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
+    resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==, tarball: https://registry.npmmirror.com/css.escape/-/css.escape-1.5.1.tgz}
 
   cssesc@3.0.0:
     resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
@@ -2968,7 +2971,7 @@ packages:
     resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==}
 
   diagram-js-direct-editing@1.8.0:
-    resolution: {integrity: sha512-B4Xj+PJfgBjbPEzT3uZQEkZI5xHFB0Izc+7BhDFuHidzrEMzQKZrFGdA3PqfWhReHf3dp+iB6Tt11G9eGNjKMw==}
+    resolution: {integrity: sha512-B4Xj+PJfgBjbPEzT3uZQEkZI5xHFB0Izc+7BhDFuHidzrEMzQKZrFGdA3PqfWhReHf3dp+iB6Tt11G9eGNjKMw==, tarball: https://registry.npmmirror.com/diagram-js-direct-editing/-/diagram-js-direct-editing-1.8.0.tgz}
     peerDependencies:
       diagram-js: '*'
 
@@ -2976,10 +2979,10 @@ packages:
     resolution: {integrity: sha512-LF9BiwjbOPpZd0ez5VSlYRbdbEA59YQX43bWvNDp1rLMv0xwZ5yIg4oaYDK82nIQ0kH1tjvoQRpNevMTCgQVyw==}
 
   diagram-js@7.9.0:
-    resolution: {integrity: sha512-o1yUtX5TXV1pmpevP55gxU/AEG6nCidOXGs/HLuxNXG0zMZ3jQta7kMqRxTK93rNw/XuHmP1eMOwdvdJ2RP5qA==}
+    resolution: {integrity: sha512-o1yUtX5TXV1pmpevP55gxU/AEG6nCidOXGs/HLuxNXG0zMZ3jQta7kMqRxTK93rNw/XuHmP1eMOwdvdJ2RP5qA==, tarball: https://registry.npmmirror.com/diagram-js/-/diagram-js-7.9.0.tgz}
 
   didi@5.2.1:
-    resolution: {integrity: sha512-IKNnajUlD4lWMy/Q9Emkk7H1qnzREgY4UyE3IhmOi/9IKua0JYtYldk928bOdt1yNxN8EiOy1sqtSozEYsmjCg==}
+    resolution: {integrity: sha512-IKNnajUlD4lWMy/Q9Emkk7H1qnzREgY4UyE3IhmOi/9IKua0JYtYldk928bOdt1yNxN8EiOy1sqtSozEYsmjCg==, tarball: https://registry.npmmirror.com/didi/-/didi-5.2.1.tgz}
 
   didi@9.0.2:
     resolution: {integrity: sha512-q2+aj+lnJcUweV7A9pdUrwFr4LHVmRPwTmQLtHPFz4aT7IBoryN6Iy+jmFku+oIzr5ebBkvtBCOb87+dJhb7bg==}
@@ -3075,7 +3078,7 @@ packages:
     resolution: {integrity: sha512-9ItEpeu15hW5m8jKdriL+BQrgwDTXEL9pn4SkillWFu73ZNNNQ2BKKLS+ZHv2vC9UkNhosAeyfxOf/5OSeTCPA==}
 
   element-plus@2.8.4:
-    resolution: {integrity: sha512-ZlVAdUOoJliv4kW3ntWnnSHMT+u/Os7mXJjk2xzOlqNeHaI2/ozlF+R58ZCEak8ZnDi6+5A2viWEYRsq64IuiA==, tarball: https://registry.npmmirror.com/element-plus/-/element-plus-2.8.4.tgz}
+    resolution: {integrity: sha512-ZlVAdUOoJliv4kW3ntWnnSHMT+u/Os7mXJjk2xzOlqNeHaI2/ozlF+R58ZCEak8ZnDi6+5A2viWEYRsq64IuiA==}
     peerDependencies:
       vue: ^3.2.0
 
@@ -3160,7 +3163,7 @@ packages:
     engines: {node: '>=6'}
 
   escape-html@1.0.3:
-    resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==, tarball: https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz}
+    resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
 
   escape-string-regexp@1.0.5:
     resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
@@ -4019,7 +4022,7 @@ packages:
     resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
 
   lodash-unified@1.0.3:
-    resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==, tarball: https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz}
+    resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==}
     peerDependencies:
       '@types/lodash-es': '*'
       lodash: '*'
@@ -4158,7 +4161,7 @@ packages:
     resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
 
   memoize-one@6.0.0:
-    resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==, tarball: https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz}
+    resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
 
   meow@12.1.1:
     resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==}
@@ -4207,7 +4210,7 @@ packages:
     engines: {node: '>=12'}
 
   min-dash@3.8.1:
-    resolution: {integrity: sha512-evumdlmIlg9mbRVPbC4F5FuRhNmcMS5pvuBUbqb1G9v09Ro0ImPEgz5n3khir83lFok1inKqVDjnKEg3GpDxQg==}
+    resolution: {integrity: sha512-evumdlmIlg9mbRVPbC4F5FuRhNmcMS5pvuBUbqb1G9v09Ro0ImPEgz5n3khir83lFok1inKqVDjnKEg3GpDxQg==, tarball: https://registry.npmmirror.com/min-dash/-/min-dash-3.8.1.tgz}
 
   min-dash@4.2.1:
     resolution: {integrity: sha512-to+unsToePnm7cUeR9TrMzFlETHd/UXmU+ELTRfWZj5XGT41KF6X3L233o3E/GdEs3sk2Tbw/lOLD1avmWkg8A==}
@@ -4260,10 +4263,10 @@ packages:
     resolution: {integrity: sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==}
 
   moddle-xml@9.0.6:
-    resolution: {integrity: sha512-tl0reHpsY/aKlLGhXeFlQWlYAQHFxTkFqC8tq8jXRYpQSnLVw13T6swMaourLd7EXqHdWsc+5ggsB+fEep6xZQ==}
+    resolution: {integrity: sha512-tl0reHpsY/aKlLGhXeFlQWlYAQHFxTkFqC8tq8jXRYpQSnLVw13T6swMaourLd7EXqHdWsc+5ggsB+fEep6xZQ==, tarball: https://registry.npmmirror.com/moddle-xml/-/moddle-xml-9.0.6.tgz}
 
   moddle@5.0.4:
-    resolution: {integrity: sha512-Kjb+hjuzO+YlojNGxEUXvdhLYTHTtAABDlDcJTtTcn5MbJF9Zkv4I1Fyvp3Ypmfgg1EfHDZ3PsCQTuML9JD6wg==}
+    resolution: {integrity: sha512-Kjb+hjuzO+YlojNGxEUXvdhLYTHTtAABDlDcJTtTcn5MbJF9Zkv4I1Fyvp3Ypmfgg1EfHDZ3PsCQTuML9JD6wg==, tarball: https://registry.npmmirror.com/moddle/-/moddle-5.0.4.tgz}
 
   mpd-parser@0.22.1:
     resolution: {integrity: sha512-fwBebvpyPUU8bOzvhX0VQZgSohncbgYwUyJJoTSNpmy7ccD2ryiCvM7oRkn/xQH5cv73/xU7rJSNCLjdGFor0Q==}
@@ -4329,7 +4332,7 @@ packages:
     engines: {node: '>=0.10.0'}
 
   normalize-wheel-es@1.2.0:
-    resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==, tarball: https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz}
+    resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==}
 
   npm-run-path@4.0.1:
     resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
@@ -4364,7 +4367,7 @@ packages:
     engines: {node: '>= 0.4'}
 
   object-refs@0.3.0:
-    resolution: {integrity: sha512-eP0ywuoWOaDoiake/6kTJlPJhs+k0qNm4nYRzXLNHj6vh+5M3i9R1epJTdxIPGlhWc4fNRQ7a6XJNCX+/L4FOQ==}
+    resolution: {integrity: sha512-eP0ywuoWOaDoiake/6kTJlPJhs+k0qNm4nYRzXLNHj6vh+5M3i9R1epJTdxIPGlhWc4fNRQ7a6XJNCX+/L4FOQ==, tarball: https://registry.npmmirror.com/object-refs/-/object-refs-0.3.0.tgz}
 
   object-visit@1.0.1:
     resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==}
@@ -4491,8 +4494,8 @@ packages:
   picocolors@1.0.0:
     resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
 
-  picocolors@1.1.0:
-    resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==, tarball: https://registry.npmmirror.com/picocolors/-/picocolors-1.1.0.tgz}
+  picocolors@1.1.1:
+    resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, tarball: https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz}
 
   picomatch@2.3.1:
     resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
@@ -4852,7 +4855,7 @@ packages:
     resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==}
 
   saxen@8.1.2:
-    resolution: {integrity: sha512-xUOiiFbc3Ow7p8KMxwsGICPx46ZQvy3+qfNVhrkwfz3Vvq45eGt98Ft5IQaA1R/7Tb5B5MKh9fUR9x3c3nDTxw==}
+    resolution: {integrity: sha512-xUOiiFbc3Ow7p8KMxwsGICPx46ZQvy3+qfNVhrkwfz3Vvq45eGt98Ft5IQaA1R/7Tb5B5MKh9fUR9x3c3nDTxw==, tarball: https://registry.npmmirror.com/saxen/-/saxen-8.1.2.tgz}
 
   scroll-into-view-if-needed@2.2.31:
     resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==}
@@ -4956,6 +4959,9 @@ packages:
   sortablejs@1.14.0:
     resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==}
 
+  sortablejs@1.15.3:
+    resolution: {integrity: sha512-zdK3/kwwAK1cJgy1rwl1YtNTbRmc8qW/+vgXf75A7NHag5of4pyI6uK86ktmQETyWRH7IGaE73uZOOBcGxgqZg==}
+
   source-map-js@1.2.0:
     resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
     engines: {node: '>=0.10.0'}
@@ -4980,7 +4986,7 @@ packages:
     engines: {node: '>=0.10.0'}
 
   source-map@0.6.1:
-    resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==, tarball: https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz}
+    resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
     engines: {node: '>=0.10.0'}
 
   split-string@3.1.0:
@@ -5167,7 +5173,7 @@ packages:
     resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
 
   tiny-svg@2.2.4:
-    resolution: {integrity: sha512-NOi39lBknf4UdDEahNkbEAJnzhu1ZcN2j75IS2vLRmIhsfxdZpTChfLKBcN1ShplVmPIXJAIafk6YY5/Aa80lQ==}
+    resolution: {integrity: sha512-NOi39lBknf4UdDEahNkbEAJnzhu1ZcN2j75IS2vLRmIhsfxdZpTChfLKBcN1ShplVmPIXJAIafk6YY5/Aa80lQ==, tarball: https://registry.npmmirror.com/tiny-svg/-/tiny-svg-2.2.4.tgz}
 
   tiny-svg@3.0.1:
     resolution: {integrity: sha512-P8T4iwiW1t95vpHVHqrD36Brn7TqFYCPSHIWk9WLJtYK1X4aDd+5cgqcAADIWSjf1/i5idKnpCh9mim8hEdRBg==}
@@ -5376,7 +5382,7 @@ packages:
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
 
   uuid@10.0.0:
-    resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==, tarball: https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz}
+    resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
     hasBin: true
 
   vary@1.1.2:
@@ -5426,7 +5432,7 @@ packages:
       vite: '>=2.0.0'
 
   vite-plugin-top-level-await@1.4.4:
-    resolution: {integrity: sha512-QyxQbvcMkgt+kDb12m2P8Ed35Sp6nXP+l8ptGrnHV9zgYDUpraO0CPdlqLSeBqvY2DToR52nutDG7mIHuysdiw==, tarball: https://registry.npmmirror.com/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.4.4.tgz}
+    resolution: {integrity: sha512-QyxQbvcMkgt+kDb12m2P8Ed35Sp6nXP+l8ptGrnHV9zgYDUpraO0CPdlqLSeBqvY2DToR52nutDG7mIHuysdiw==}
     peerDependencies:
       vite: '>=2.8'
 
@@ -5812,11 +5818,11 @@ snapshots:
 
   '@babel/helper-string-parser@7.24.1': {}
 
-  '@babel/helper-string-parser@7.25.7': {}
+  '@babel/helper-string-parser@7.25.9': {}
 
   '@babel/helper-validator-identifier@7.22.20': {}
 
-  '@babel/helper-validator-identifier@7.25.7': {}
+  '@babel/helper-validator-identifier@7.25.9': {}
 
   '@babel/helper-validator-option@7.23.5': {}
 
@@ -5845,9 +5851,9 @@ snapshots:
     dependencies:
       '@babel/types': 7.24.0
 
-  '@babel/parser@7.25.8':
+  '@babel/parser@7.26.2':
     dependencies:
-      '@babel/types': 7.25.8
+      '@babel/types': 7.26.0
 
   '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.4(@babel/core@7.24.4)':
     dependencies:
@@ -6418,11 +6424,10 @@ snapshots:
       '@babel/helper-validator-identifier': 7.22.20
       to-fast-properties: 2.0.0
 
-  '@babel/types@7.25.8':
+  '@babel/types@7.26.0':
     dependencies:
-      '@babel/helper-string-parser': 7.25.7
-      '@babel/helper-validator-identifier': 7.25.7
-      to-fast-properties: 2.0.0
+      '@babel/helper-string-parser': 7.25.9
+      '@babel/helper-validator-identifier': 7.25.9
 
   '@bpmn-io/diagram-js-ui@0.2.3':
     dependencies:
@@ -6702,7 +6707,7 @@ snapshots:
     dependencies:
       '@form-create/utils': 3.2.0
 
-  '@form-create/component-elm-upload@3.2.9':
+  '@form-create/component-elm-upload@3.2.13':
     dependencies:
       '@form-create/utils': 3.2.0
 
@@ -6712,15 +6717,15 @@ snapshots:
     dependencies:
       wangeditor: 4.7.15
 
-  '@form-create/core@3.2.11(vue@3.5.12(typescript@5.3.3))':
+  '@form-create/core@3.2.13(vue@3.5.12(typescript@5.3.3))':
     dependencies:
       '@form-create/utils': 3.2.0
       vue: 3.5.12(typescript@5.3.3)
 
-  '@form-create/designer@3.2.7(vue@3.5.12(typescript@5.3.3))':
+  '@form-create/designer@3.2.8(vue@3.5.12(typescript@5.3.3))':
     dependencies:
       '@form-create/component-wangeditor': 3.1.20
-      '@form-create/element-ui': 3.2.11(vue@3.5.12(typescript@5.3.3))
+      '@form-create/element-ui': 3.2.13(vue@3.5.12(typescript@5.3.3))
       '@form-create/utils': 3.2.0
       codemirror: 6.65.7
       element-plus: 2.8.4(vue@3.5.12(typescript@5.3.3))
@@ -6729,7 +6734,7 @@ snapshots:
     transitivePeerDependencies:
       - '@vue/composition-api'
 
-  '@form-create/element-ui@3.2.11(vue@3.5.12(typescript@5.3.3))':
+  '@form-create/element-ui@3.2.13(vue@3.5.12(typescript@5.3.3))':
     dependencies:
       '@form-create/component-elm-checkbox': 3.2.8
       '@form-create/component-elm-frame': 3.2.0
@@ -6737,9 +6742,9 @@ snapshots:
       '@form-create/component-elm-radio': 3.2.8
       '@form-create/component-elm-select': 3.2.0
       '@form-create/component-elm-tree': 3.2.9
-      '@form-create/component-elm-upload': 3.2.9
+      '@form-create/component-elm-upload': 3.2.13
       '@form-create/component-subform': 3.1.34
-      '@form-create/core': 3.2.11(vue@3.5.12(typescript@5.3.3))
+      '@form-create/core': 3.2.13(vue@3.5.12(typescript@5.3.3))
       '@form-create/utils': 3.2.0
       vue: 3.5.12(typescript@5.3.3)
 
@@ -7674,7 +7679,7 @@ snapshots:
 
   '@vue/compiler-core@3.5.12':
     dependencies:
-      '@babel/parser': 7.25.8
+      '@babel/parser': 7.26.2
       '@vue/shared': 3.5.12
       entities: 4.5.0
       estree-walker: 2.0.2
@@ -7704,7 +7709,7 @@ snapshots:
 
   '@vue/compiler-sfc@3.5.12':
     dependencies:
-      '@babel/parser': 7.25.8
+      '@babel/parser': 7.26.2
       '@vue/compiler-core': 3.5.12
       '@vue/compiler-dom': 3.5.12
       '@vue/compiler-ssr': 3.5.12
@@ -8109,11 +8114,11 @@ snapshots:
 
   boolbase@1.0.0: {}
 
-  bpmn-js-properties-panel@0.46.0(bpmn-js@8.9.0):
+  bpmn-js-properties-panel@0.46.0(bpmn-js@8.10.0):
     dependencies:
       '@bpmn-io/element-templates-validator': 0.2.0
       '@bpmn-io/extract-process-variables': 0.4.5
-      bpmn-js: 8.9.0
+      bpmn-js: 8.10.0
       ids: 1.0.5
       inherits: 2.0.4
       lodash: 4.17.21
@@ -8128,7 +8133,7 @@ snapshots:
       min-dom: 0.2.0
       svg.js: 2.7.1
 
-  bpmn-js@8.9.0:
+  bpmn-js@8.10.0:
     dependencies:
       bpmn-moddle: 7.1.3
       css.escape: 1.5.1
@@ -10377,7 +10382,7 @@ snapshots:
 
   picocolors@1.0.0: {}
 
-  picocolors@1.1.0: {}
+  picocolors@1.1.1: {}
 
   picomatch@2.3.1: {}
 
@@ -10463,7 +10468,7 @@ snapshots:
   postcss@8.4.47:
     dependencies:
       nanoid: 3.3.7
-      picocolors: 1.1.0
+      picocolors: 1.1.1
       source-map-js: 1.2.1
 
   posthtml-parser@0.2.1:
@@ -10862,6 +10867,8 @@ snapshots:
 
   sortablejs@1.14.0: {}
 
+  sortablejs@1.15.3: {}
+
   source-map-js@1.2.0: {}
 
   source-map-js@1.2.1: {}
@@ -11328,7 +11335,7 @@ snapshots:
     transitivePeerDependencies:
       - rollup
 
-  unplugin-vue-components@0.25.2(@babel/parser@7.25.8)(rollup@4.17.1)(vue@3.5.12(typescript@5.3.3)):
+  unplugin-vue-components@0.25.2(@babel/parser@7.26.2)(rollup@4.17.1)(vue@3.5.12(typescript@5.3.3)):
     dependencies:
       '@antfu/utils': 0.7.7
       '@rollup/pluginutils': 5.1.0(rollup@4.17.1)
@@ -11342,7 +11349,7 @@ snapshots:
       unplugin: 1.10.1
       vue: 3.5.12(typescript@5.3.3)
     optionalDependencies:
-      '@babel/parser': 7.25.8
+      '@babel/parser': 7.26.2
     transitivePeerDependencies:
       - rollup
       - supports-color

+ 0 - 8
src/api/bpm/activity/index.ts

@@ -1,8 +0,0 @@
-import request from '@/config/axios'
-
-export const getActivityList = async (params) => {
-  return await request.get({
-    url: '/bpm/activity/list',
-    params
-  })
-}

+ 10 - 0
src/api/bpm/category/index.ts

@@ -36,6 +36,16 @@ export const CategoryApi = {
     return await request.put({ url: `/bpm/category/update`, data })
   },
 
+  // 批量修改流程分类的排序
+  updateCategorySortBatch: async (ids: number[]) => {
+    return await request.put({
+      url: `/bpm/category/update-sort-batch`,
+      params: {
+        ids: ids.join(',')
+      }
+    })
+  },
+
   // 删除流程分类
   deleteCategory: async (id: number) => {
     return await request.delete({ url: `/bpm/category/delete?id=` + id })

+ 12 - 2
src/api/bpm/model/index.ts

@@ -26,8 +26,8 @@ export type ModelVO = {
   bpmnXml: string
 }
 
-export const getModelPage = async (params) => {
-  return await request.get({ url: '/bpm/model/page', params })
+export const getModelList = async (name: string | undefined) => {
+  return await request.get({ url: '/bpm/model/list', params: { name } })
 }
 
 export const getModel = async (id: string) => {
@@ -38,6 +38,16 @@ export const updateModel = async (data: ModelVO) => {
   return await request.put({ url: '/bpm/model/update', data: data })
 }
 
+// 批量修改流程分类的排序
+export const updateModelSortBatch = async (ids: number[]) => {
+  return await request.put({
+    url: `/bpm/model/update-sort-batch`,
+    params: {
+      ids: ids.join(',')
+    }
+  })
+}
+
 export const updateModelBpmn = async (data: ModelVO) => {
   return await request.put({ url: '/bpm/model/update-bpmn', data: data })
 }

+ 17 - 13
src/api/bpm/processInstance/index.ts

@@ -1,6 +1,6 @@
 import request from '@/config/axios'
 import { ProcessDefinitionVO } from '@/api/bpm/model'
-import { NodeType } from '@/components/SimpleProcessDesignerV2/src/consts'
+import { NodeType, CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts'
 export type Task = {
   id: string
   name: string
@@ -24,30 +24,30 @@ export type ProcessInstanceVO = {
 
 // 用户信息
 export type User = {
-  id: number,
-  nickname: string,
+  id: number
+  nickname: string
   avatar: string
 }
 
 // 审批任务信息
 export type ApprovalTaskInfo = {
-  id: number,
-  ownerUser: User,
-  assigneeUser: User,
-  status: number,
+  id: number
+  ownerUser: User
+  assigneeUser: User
+  status: number
   reason: string
-
 }
 
 // 审批节点信息
 export type ApprovalNodeInfo = {
-  id : number
+  id: number
   name: string
   nodeType: NodeType
+  candidateStrategy?: CandidateStrategy
   status: number
   startTime?: Date
   endTime?: Date
-  candidateUserList?: User[]
+  candidateUsers?: User[]
   tasks: ApprovalTaskInfo[]
 }
 
@@ -88,12 +88,16 @@ export const getProcessInstanceCopyPage = async (params: any) => {
 }
 
 // 获取审批详情
-export const getApprovalDetail = async (processInstanceId?:string, processDefinitionId?:string) => {
-  const param = processInstanceId ? '?processInstanceId='+ processInstanceId : '?processDefinitionId='+ processDefinitionId
-  return await request.get({ url: 'bpm/process-instance/get-approval-detail'+ param })
+export const getApprovalDetail = async (params: any) => {
+  return await request.get({ url: 'bpm/process-instance/get-approval-detail' , params })
 }
 
 // 获取表单字段权限
 export const getFormFieldsPermission = async (params: any) => {
   return await request.get({ url: '/bpm/process-instance/get-form-fields-permission', params })
 }
+
+// 获取流程实例的 BPMN 模型视图
+export const getProcessInstanceBpmnModelView = async (id: string) => {
+  return await request.get({ url: '/bpm/process-instance/get-bpmn-model-view?id=' + id })
+}

+ 16 - 15
src/api/bpm/task/index.ts

@@ -9,10 +9,10 @@ export enum TaskStatusEnum {
    */
   NOT_START = -1,
 
-   /**
+  /**
    * 待审批
    */
-   WAIT = 0,
+  WAIT = 0,
   /**
    * 审批中
    */
@@ -26,7 +26,7 @@ export enum TaskStatusEnum {
    * 审批不通过
    */
   REJECT = 3,
-  
+
   /**
    * 已取消
    */
@@ -35,19 +35,10 @@ export enum TaskStatusEnum {
    * 已退回
    */
   RETURN = 5,
-  /**
-   * 委派中
-   */
-  DELEGATE = 6,
   /**
    * 审批通过中
    */
-  APPROVING = 7,
-
-}
-
-export type TaskVO = {
-  id: number
+  APPROVING = 7
 }
 
 export const getTaskTodoPage = async (params: any) => {
@@ -76,12 +67,12 @@ export const getTaskListByProcessInstanceId = async (processInstanceId: string)
   })
 }
 
-// 获取所有可退的节点
+// 获取所有可退的节点
 export const getTaskListByReturn = async (id: string) => {
   return await request.get({ url: '/bpm/task/list-by-return', params: { id } })
 }
 
-// 退
+// 退
 export const returnTask = async (data: any) => {
   return await request.put({ url: '/bpm/task/return', data })
 }
@@ -106,6 +97,16 @@ export const signDeleteTask = async (data: any) => {
   return await request.delete({ url: '/bpm/task/delete-sign', data })
 }
 
+// 抄送
+export const copyTask = async (data: any) => {
+  return await request.put({ url: '/bpm/task/copy', data })
+}
+
+// 获取我的待办任务
+export const myTodoTask = async (processInstanceId: string) => {
+  return await request.get({ url: '/bpm/task/my-todo?processInstanceId=' + processInstanceId })
+}
+
 // 获取减签任务列表
 export const getChildrenTaskList = async (id: string) => {
   return await request.get({ url: '/bpm/task/list-by-parent-task-id?parentTaskId=' + id })

+ 9 - 3
src/api/mall/trade/delivery/pickUpStore/index.ts

@@ -13,10 +13,11 @@ export interface DeliveryPickUpStoreVO {
   latitude: number
   longitude: number
   status: number
+  verifyUserIds: number[] // 绑定用户编号组数
 }
 
 // 查询自提门店列表
-export const getDeliveryPickUpStorePage = async (params) => {
+export const getDeliveryPickUpStorePage = async (params: any) => {
   return await request.get({ url: '/trade/delivery/pick-up-store/page', params })
 }
 
@@ -26,8 +27,8 @@ export const getDeliveryPickUpStore = async (id: number) => {
 }
 
 // 查询自提门店精简列表
-export const getListAllSimple = async (): Promise<DeliveryPickUpStoreVO[]> => {
-  return await request.get({ url: '/trade/delivery/pick-up-store/list-all-simple' })
+export const getSimpleDeliveryPickUpStoreList = async (): Promise<DeliveryPickUpStoreVO[]> => {
+  return await request.get({ url: '/trade/delivery/pick-up-store/simple-list' })
 }
 
 // 新增自提门店
@@ -44,3 +45,8 @@ export const updateDeliveryPickUpStore = async (data: DeliveryPickUpStoreVO) =>
 export const deleteDeliveryPickUpStore = async (id: number) => {
   return await request.delete({ url: '/trade/delivery/pick-up-store/delete?id=' + id })
 }
+
+// 绑定自提店员
+export const bindStoreStaffId = async (data: any) => {
+  return await request.post({ url: '/trade/delivery/pick-up-store/bind', data })
+}

+ 2 - 0
src/api/pay/app/index.ts

@@ -8,6 +8,7 @@ export interface AppVO {
   remark: string
   payNotifyUrl: string
   refundNotifyUrl: string
+  transferNotifyUrl: string
   merchantId: number
   merchantName: string
   createTime: Date
@@ -19,6 +20,7 @@ export interface AppPageReqVO extends PageParam {
   remark?: string
   payNotifyUrl?: string
   refundNotifyUrl?: string
+  transferNotifyUrl?: string
   merchantName?: string
   createTime?: Date[]
 }

+ 1 - 0
src/assets/svgs/bpm/add-user.svg

@@ -0,0 +1 @@
+<svg t="1731390087280" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4297" width="200" height="200"><path d="M639.9 541.7c76.4-44.2 127.9-126.8 127.9-221.5C767.7 179 653.2 64.5 512 64.5S256.3 179 256.3 320.2c0 89.6 46.1 168.4 115.8 214.1C193.5 593 64.5 761.2 64.5 959.5h63.9c0-211.5 172.1-383.6 383.6-383.6 44.9 0 87.8 8.1 127.9 22.4v-56.6zM320.2 320.2c0-105.8 86-191.8 191.8-191.8s191.8 86 191.8 191.8S617.7 512 512 512s-191.8-86-191.8-191.8zM831.6 767.7V639.9h-63.9v127.8H639.9v63.9h127.8v127.9h63.9V831.6h127.9v-63.9z" fill="#5f6266" p-id="4298"></path></svg>

+ 0 - 0
src/assets/svgs/bpm/audit2.svg → src/assets/svgs/bpm/approve.svg


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

@@ -0,0 +1 @@
+<svg t="1729561718271" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8640" width="200" height="200"><path d="M908.5952 920.4224H164.7616a31.0784 31.0784 0 0 1-30.976-30.976c0-17.0496 13.9264-30.976 30.976-30.976h743.8336c17.0496 0 31.0272 13.9264 31.0272 30.976a31.0784 31.0784 0 0 1-31.0272 30.976z m0-123.9552H164.7616a31.0784 31.0784 0 0 1-30.976-30.976v-154.9824c0-51.1488 41.8304-92.9792 92.9792-92.9792h198.3488c-6.1952-37.1712-24.7808-72.8064-51.1488-103.8336a216.576 216.576 0 0 1-54.2208-144.128c0-58.88 23.2448-114.688 66.6112-156.4672C429.7728 71.168 485.5296 51.0976 545.9968 52.6848c111.5648 4.608 206.08 100.6592 207.616 212.2752 1.536 55.808-20.1216 110.0288-57.344 151.8592-26.3168 27.904-41.8304 61.952-48.0256 100.7104h198.3488c51.2 0 93.0304 41.8304 93.0304 92.9792v154.9824a31.0784 31.0784 0 0 1-31.0272 30.976z m-712.8064-61.952H877.568v-124.0064a31.0784 31.0784 0 0 0-31.0272-30.976h-232.448a31.0784 31.0784 0 0 1-30.976-31.0272c0-65.024 23.2448-127.0784 66.6624-173.568 27.8528-29.3888 41.8304-68.1472 41.8304-108.4416-1.536-80.5888-68.1984-148.7872-148.7872-151.8592a150.528 150.528 0 0 0-113.152 43.3664 153.6 153.6 0 0 0-48.0256 111.616c0 37.1712 13.9776 74.3424 38.7584 102.2464 44.9536 51.1488 69.7344 113.152 69.7344 176.64a31.0784 31.0784 0 0 1-30.976 31.0272h-232.448a31.0784 31.0784 0 0 0-30.976 30.976v123.9552z" fill="#fff" p-id="8641"></path></svg>

Fișier diff suprimat deoarece este prea mare
+ 0 - 0
src/assets/svgs/bpm/cancel.svg


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

@@ -0,0 +1 @@
+<svg t="1729585232424" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1602" width="200" height="200"><path d="M925.5 898.9H804.9c-19 0-34.5-15.4-34.5-34.4V761.3c0-19 15.4-34.4 34.5-34.4h34.5V572.2c0-19-15.4-34.4-34.5-34.4H529.2V727h34.5c19 0 34.5 15.4 34.5 34.4v103.2c0 19-15.4 34.4-34.5 34.4H443.1c-19 0-34.5-15.4-34.5-34.4V761.3c0-19 15.4-34.4 34.5-34.4h34.5V537.8H219.1c-19 0-34.5 15.4-34.5 34.4V727h34.5c19 0 34.5 15.4 34.5 34.4v103.2c0 19-15.4 34.4-34.5 34.4H98.5c-19 0-34.5-15.4-34.5-34.4V761.3c0-19 15.4-34.4 34.5-34.4H133V555c0-38 30.9-68.8 68.9-68.8h275.7V297.1h-34.5c-19 0-34.5-15.4-34.5-34.4V159.5c0-19 15.4-34.4 34.5-34.4h120.6c19 0 34.5 15.4 34.5 34.4v103.2c0 19-15.4 34.4-34.5 34.4h-34.5v189.2h292.9c38.1 0 68.9 30.8 68.9 68.8v172h34.5c19 0 34.5 15.4 34.5 34.4v103.2c0 18.8-15.4 34.2-34.5 34.2z m0 0" p-id="1603" fill="#fff"></path></svg>

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

@@ -0,0 +1 @@
+<svg t="1729649333541" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1644" width="200" height="200"><path d="M647.888 893.84L491.904 571.52l393.888-393.888-237.904 716.208zM872.32 123.232L459.872 535.68 134.96 380.88 872.32 123.232z m90.72-68.32a23.968 23.968 0 0 0-24.784-5.568L64.08 354.816a23.984 23.984 0 0 0-2.4 44.32l381.392 181.728 187.36 387.088a24.048 24.048 0 0 0 23.152 13.504 24.032 24.032 0 0 0 21.232-16.4L968.96 79.552c2.88-8.672 0.592-18.24-5.92-24.64z" fill="#fff" p-id="1645"></path></svg>

Fișier diff suprimat deoarece este prea mare
+ 0 - 0
src/assets/svgs/bpm/finish.svg


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

@@ -0,0 +1 @@
+<svg t="1729585239190" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1755" width="200" height="200"><path d="M901.489435 536.822664v-0.931601l-1.001722-198.240726c-0.100172-19.162936-9.21584-37.474409-25.043042-50.246361-14.024104-11.349507-32.265456-17.60025-51.348255-17.610268l-618.062295-0.18031c-19.142902 0-37.424323 6.280795-51.478478 17.690405-15.827203 12.842072-24.902802 31.2437-24.892785 50.486775v196.798247A114.987635 114.987635 0 1 0 195.295664 536.922836V338.782282c1.15198-1.252152 4.808264-3.596181 10.768509-3.596181l276.725622 0.090155v199.753326a114.987635 114.987635 0 1 0 65.612772 1.412428V335.326342l275.693849 0.080138c6.01033 0 9.626546 2.344029 10.768508 3.596181l1.001722 195.70637a114.987635 114.987635 0 1 0 65.592737 2.113633zM214.979496 645.910158a56.437001 56.437001 0 1 1-56.437001-56.437001 56.507122 56.507122 0 0 1 56.437001 56.437001z m354.689623 0a56.437001 56.437001 0 1 1-56.437001-56.437001 56.507122 56.507122 0 0 1 56.437001 56.437001z m295.507904 56.437001a56.437001 56.437001 0 1 1 56.437001-56.437001 56.507122 56.507122 0 0 1-56.457035 56.437001z" p-id="1756" fill='#fff'></path></svg>

+ 0 - 0
src/assets/svgs/bpm/audit3.svg → src/assets/svgs/bpm/reject.svg


+ 0 - 0
src/assets/svgs/bpm/audit1.svg → src/assets/svgs/bpm/running.svg


+ 1 - 0
src/assets/svgs/bpm/simple-process-bg.svg

@@ -0,0 +1 @@
+<svg width="22" height="22" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#FAFAFA" d="M0 0h22v22H0z"/><circle fill="#919BAE" cx="1" cy="1" r="1"/></g></svg>

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

@@ -0,0 +1 @@
+<svg t="1729561814171" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1359" width="200" height="200"><path d="M674.496 603.456c120.256 0 218.176 90.752 221.44 203.84l0.064 5.888v125.888c0 11.52-9.92 20.928-22.144 20.928h-44.352a21.568 21.568 0 0 1-22.144-20.928v-125.888c0-67.712-56.512-123.264-128-125.76l-4.928-0.064H349.568c-71.488 0-130.176 53.504-132.864 121.152l-0.064 4.672v125.888c0 11.52-9.92 20.928-22.144 20.928h-44.352A21.568 21.568 0 0 1 128 939.072v-125.888c0-113.92 95.872-206.528 215.36-209.664l6.208-0.064h324.928zM497.216 128c122.368 0 221.568 93.888 221.568 209.728s-99.2 209.792-221.568 209.792c-122.304 0-221.44-93.952-221.44-209.728C275.712 221.952 374.848 128 497.152 128z m0 83.904c-73.408 0-132.864 56.32-132.864 125.888 0 69.504 59.52 125.824 132.864 125.824 73.408 0 132.928-56.32 132.928-125.824 0-69.504-59.52-125.888-132.928-125.888z" fill="#fff" p-id="1360"></path></svg>

+ 0 - 1
src/components/FormCreate/src/components/useApiSelect.tsx

@@ -185,7 +185,6 @@ export const useApiSelect = (option: ApiSelectProps) => {
             </el-select>
           )
         }
-        debugger
         return (
           <el-select
             class="w-1/1"

+ 43 - 0
src/components/FormCreate/src/utils/index.ts

@@ -16,3 +16,46 @@ export const localeProps = (t, prefix, rules) => {
     return rule
   })
 }
+
+/**
+ * 解析表单组件的  field, title 等字段(递归,如果组件包含子组件)
+ * 
+ * @param rule  组件的生成规则 https://www.form-create.com/v3/guide/rule
+ * @param fields 解析后表单组件字段
+ * @param parentTitle  如果是子表单,子表单的标题,默认为空
+ */
+export const parseFormFields = (
+  rule: Record<string, any>,
+  fields: Array<Record<string, any>> = [],
+  parentTitle: string = ''
+) => {
+  const { type, field, $required, title: tempTitle, children } = rule
+  if (field && tempTitle) {
+    let title = tempTitle
+    if (parentTitle) {
+      title = `${parentTitle}.${tempTitle}`
+    }
+    let required = false
+    if ($required) {
+      required = true
+    }
+    fields.push({
+      field,
+      title,
+      type,
+      required
+    })
+    // TODO 子表单 需要处理子表单字段
+    // if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
+    //   // 解析子表单的字段
+    //   rule.props.rule.forEach((item) => {
+    //     parseFields(item, fieldsPermission, title)
+    //   })
+    // }
+  }
+  if (children && Array.isArray(children)) {
+    children.forEach((rule) => {
+      parseFormFields(rule, fields)
+    })
+  }
+}

+ 56 - 10
src/components/SimpleProcessDesignerV2/src/NodeHandler.vue

@@ -1,11 +1,12 @@
 <template>
   <div class="node-handler-wrapper">
-    <div class="node-handler" v-if="props.showAdd">
+    <div class="node-handler">
       <el-popover
         trigger="hover"
         v-model:visible="popoverShow"
         placement="right-start"
         width="auto"
+        v-if="!readonly"
       >
         <div class="handler-item-wrapper">
           <div class="handler-item" @click="addNode(NodeType.USER_TASK_NODE)">
@@ -27,11 +28,17 @@
             <div class="handler-item-text">条件分支</div>
           </div>
           <div class="handler-item" @click="addNode(NodeType.PARALLEL_BRANCH_NODE)">
-            <div class="handler-item-icon condition">
+            <div class="handler-item-icon parallel">
               <span class="iconfont icon-size icon-parallel"></span>
             </div>
             <div class="handler-item-text">并行分支</div>
           </div>
+          <div class="handler-item" @click="addNode(NodeType.INCLUSIVE_BRANCH_NODE)">
+            <div class="handler-item-icon inclusive">
+              <span class="iconfont icon-size icon-inclusive"></span>
+            </div>
+            <div class="handler-item-text">包容分支</div>
+          </div>
         </div>
         <template #reference>
           <div class="add-icon"><Icon icon="ep:plus" /></div>
@@ -56,23 +63,36 @@ import { generateUUID } from '@/utils'
 defineOptions({
   name: 'NodeHandler'
 })
-const popoverShow = ref(false)
 
+const message = useMessage() // 消息弹窗
+
+const popoverShow = ref(false)
 const props = defineProps({
   childNode: {
     type: Object as () => SimpleFlowNode,
     default: null
   },
-  showAdd: {
-    // 是否显示添加节点
-    type: Boolean,
-    default: true
+  currentNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
   }
 })
-
 const emits = defineEmits(['update:childNode'])
 
+const readonly = inject<Boolean>('readonly') // 是否只读
+
 const addNode = (type: number) => {
+  // 校验:条件分支、包容分支后面,不允许直接添加并行分支
+  if (
+    type === NodeType.PARALLEL_BRANCH_NODE &&
+    [NodeType.CONDITION_BRANCH_NODE, NodeType.INCLUSIVE_BRANCH_NODE].includes(
+      props.currentNode?.type
+    )
+  ) {
+    message.error('条件分支、包容分支后面,不允许直接添加并行分支')
+    return
+  }
+
   popoverShow.value = false
   if (type === NodeType.USER_TASK_NODE) {
     const id = 'Activity_' + generateUUID()
@@ -122,12 +142,11 @@ const addNode = (type: number) => {
           childNode: undefined,
           conditionType: 1,
           defaultFlow: false
-          
         },
         {
           id: 'Flow_' + generateUUID(),
           name: '其它情况',
-          showText: '其它情况进入此流程',
+          showText: '未满足其它条件时,将进入此分支',
           type: NodeType.CONDITION_NODE,
           childNode: undefined,
           conditionType: undefined,
@@ -162,6 +181,33 @@ const addNode = (type: number) => {
     }
     emits('update:childNode', data)
   }
+  if (type === NodeType.INCLUSIVE_BRANCH_NODE) {
+    const data: SimpleFlowNode = {
+      name: '包容分支',
+      type: NodeType.INCLUSIVE_BRANCH_NODE,
+      id: 'GateWay_' + generateUUID(),
+      childNode: props.childNode,
+      conditionNodes: [
+        {
+          id: 'Flow_' + generateUUID(),
+          name: '包容条件1',
+          showText: '',
+          type: NodeType.CONDITION_NODE,
+          childNode: undefined,
+          defaultFlow: false
+        },
+        {
+          id: 'Flow_' + generateUUID(),
+          name: '其它情况',
+          showText: '未满足其它条件时,将进入此分支',
+          type: NodeType.CONDITION_NODE,
+          childNode: undefined,
+          defaultFlow: true
+        }
+      ]
+    }
+    emits('update:childNode', data)
+  }
 }
 </script>
 

+ 12 - 1
src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue

@@ -31,6 +31,13 @@
     @update:model-value="handleModelValueUpdate"
     @find:parent-node="findFromParentNode"
   />
+  <!-- 包容分支节点 -->
+  <InclusiveNode
+    v-if="currentNode && currentNode.type === NodeType.INCLUSIVE_BRANCH_NODE"
+    :flow-node="currentNode"
+    @update:model-value="handleModelValueUpdate"
+    @find:parent-node="findFromParentNode"
+  />
   <!-- 递归显示孩子节点  -->
   <ProcessNodeTree
     v-if="currentNode && currentNode.childNode"
@@ -40,7 +47,10 @@
   />
 
   <!-- 结束节点 -->
-  <EndEventNode v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE" />
+  <EndEventNode
+    v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE"
+    :flow-node="currentNode"
+  />
 </template>
 <script setup lang="ts">
 import StartUserNode from './nodes/StartUserNode.vue'
@@ -49,6 +59,7 @@ import UserTaskNode from './nodes/UserTaskNode.vue'
 import CopyTaskNode from './nodes/CopyTaskNode.vue'
 import ExclusiveNode from './nodes/ExclusiveNode.vue'
 import ParallelNode from './nodes/ParallelNode.vue'
+import InclusiveNode from './nodes/InclusiveNode.vue'
 import { SimpleFlowNode, NodeType } from './consts'
 import { useWatchNode } from './node'
 defineOptions({

+ 45 - 78
src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue

@@ -1,23 +1,11 @@
 <template>
-  <div class="simple-flow-canvas" v-loading="loading">
-    <div class="simple-flow-container">
-      <div class="top-area-container">
-        <div class="top-actions">
-          <div class="canvas-control">
-            <span class="control-scale-group">
-              <span class="control-scale-button"> <Icon icon="ep:plus" @click="zoomOut()" /></span>
-              <span class="control-scale-label">{{ scaleValue }}%</span>
-              <span class="control-scale-button"><Icon icon="ep:minus" @click="zoomIn()" /></span>
-            </span>
-          </div>
-          <el-button type="primary" @click="saveSimpleFlowModel">保存</el-button>
-          <!-- <el-button type="primary">全局设置</el-button> -->
-        </div>
-      </div>
-      <div class="scale-container" :style="`transform: scale(${scaleValue / 100});`">
-        <ProcessNodeTree v-if="processNodeTree" v-model:flow-node="processNodeTree" />
-      </div>
-    </div>
+  <div v-loading="loading" class="overflow-auto">
+    <SimpleProcessModel
+      v-if="processNodeTree"
+      :flow-node="processNodeTree"
+      :readonly="false"
+      @save="saveSimpleFlowModel"
+    />
     <Dialog v-model="errorDialogVisible" title="保存失败" width="400" :fullscreen="false">
       <div class="mb-2">以下节点内容不完善,请修改后保存</div>
       <div
@@ -35,7 +23,7 @@
 </template>
 
 <script setup lang="ts">
-import ProcessNodeTree from './ProcessNodeTree.vue'
+import SimpleProcessModel from './SimpleProcessModel.vue'
 import { updateBpmSimpleModel, getBpmSimpleModel } from '@/api/bpm/simple'
 import { SimpleFlowNode, NodeType, NodeId, NODE_DEFAULT_TEXT } from './consts'
 import { getModel } from '@/api/bpm/model'
@@ -50,14 +38,16 @@ import * as UserGroupApi from '@/api/bpm/userGroup'
 defineOptions({
   name: 'SimpleProcessDesigner'
 })
-const router = useRouter() // 路由
+const emits = defineEmits(['success']) // 保存成功事件
+
 const props = defineProps({
   modelId: {
     type: String,
     required: true
   }
 })
-const loading = ref(true)
+
+const loading = ref(false)
 const formFields = ref<string[]>([])
 const formType = ref(20)
 const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
@@ -79,28 +69,26 @@ const message = useMessage() // 国际化
 const processNodeTree = ref<SimpleFlowNode | undefined>()
 const errorDialogVisible = ref(false)
 let errorNodes: SimpleFlowNode[] = []
-const saveSimpleFlowModel = async () => {
-  if (!props.modelId) {
-    message.error('缺少模型 modelId 编号')
+const saveSimpleFlowModel = async (simpleModelNode: SimpleFlowNode) => {
+  if (!simpleModelNode) {
+    message.error('模型数据为空')
     return
   }
-  errorNodes = []
-  validateNode(processNodeTree.value, errorNodes)
-  if (errorNodes.length > 0) {
-    errorDialogVisible.value = true
-    return
-  }
-  const data = {
-    id: props.modelId,
-    simpleModel: processNodeTree.value
-  }
-
-  const result = await updateBpmSimpleModel(data)
-  if (result) {
-    message.success('修改成功')
-    close()
-  } else {
-    message.alert('修改失败')
+  try {
+    loading.value = true
+    const data = {
+      id: props.modelId,
+      simpleModel: simpleModelNode
+    }
+    const result = await updateBpmSimpleModel(data)
+    if (result) {
+      message.success('修改成功')
+      emits('success')
+    } else {
+      message.alert('修改失败')
+    }
+  } finally {
+    loading.value = false
   }
 }
 // 校验节点设置。 暂时以 showText 为空 未节点错误配置
@@ -111,58 +99,37 @@ const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNo
       return
     }
     if (type == NodeType.START_USER_NODE) {
+      // 发起人节点暂时不用校验,直接校验孩子节点
       validateNode(node.childNode, errorNodes)
     }
 
-    if (type === NodeType.USER_TASK_NODE) {
-      if (!showText) {
-        errorNodes.push(node)
-      }
-      validateNode(node.childNode, errorNodes)
-    }
-    if (type === NodeType.COPY_TASK_NODE) {
-      if (!showText) {
-        errorNodes.push(node)
-      }
-      validateNode(node.childNode, errorNodes)
-    }
-    if (type === NodeType.CONDITION_NODE) {
+    if (
+      type === NodeType.USER_TASK_NODE ||
+      type === NodeType.COPY_TASK_NODE ||
+      type === NodeType.CONDITION_NODE
+    ) {
       if (!showText) {
         errorNodes.push(node)
       }
       validateNode(node.childNode, errorNodes)
     }
 
-    if (type == NodeType.CONDITION_BRANCH_NODE) {
+    if (
+      type == NodeType.CONDITION_BRANCH_NODE ||
+      type == NodeType.PARALLEL_BRANCH_NODE ||
+      type == NodeType.INCLUSIVE_BRANCH_NODE
+    ) {
+      // 分支节点
+      // 1. 先校验各个分支
       conditionNodes?.forEach((item) => {
         validateNode(item, errorNodes)
       })
+      // 2. 校验孩子节点
       validateNode(node.childNode, errorNodes)
     }
   }
 }
 
-const close = () => {
-  router.push({ path: '/bpm/manager/model' })
-}
-let scaleValue = ref(100)
-const MAX_SCALE_VALUE = 200
-const MIN_SCALE_VALUE = 50
-// 放大
-const zoomOut = () => {
-  if (scaleValue.value == MAX_SCALE_VALUE) {
-    return
-  }
-  scaleValue.value += 10
-}
-// 缩小
-const zoomIn = () => {
-  if (scaleValue.value == MIN_SCALE_VALUE) {
-    return
-  }
-  scaleValue.value -= 10
-}
-
 onMounted(async () => {
   try {
     loading.value = true
@@ -188,7 +155,7 @@ onMounted(async () => {
     // 获取用户组列表
     userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
 
-    // 获取 SIMPLE 设计器模型
+    //获取 SIMPLE 设计器模型
     const result = await getBpmSimpleModel(props.modelId)
     if (result) {
       processNodeTree.value = result

+ 140 - 0
src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue

@@ -0,0 +1,140 @@
+<template>
+  <div class="simple-process-model-container position-relative">
+    <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 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>
+          <el-button size="default" :plain="true" :icon="ZoomIn" @click="zoomIn()" />
+        </el-button-group>
+        <el-button
+          v-if="!readonly"
+          size="default"
+          class="ml-4px"
+          type="primary"
+          :icon="Select"
+          @click="saveSimpleFlowModel"
+          >保存模型</el-button
+        >
+      </el-row>
+    </div>
+    <div class="simple-process-model" :style="`transform: scale(${scaleValue / 100});`">
+      <ProcessNodeTree v-if="processNodeTree" v-model:flow-node="processNodeTree" />
+    </div>
+  </div>
+  <Dialog v-model="errorDialogVisible" title="保存失败" width="400" :fullscreen="false">
+    <div class="mb-2">以下节点内容不完善,请修改后保存</div>
+    <div
+      class="mb-3 b-rounded-1 bg-gray-100 p-2 line-height-normal"
+      v-for="(item, index) in errorNodes"
+      :key="index"
+    >
+      {{ item.name }} : {{ NODE_DEFAULT_TEXT.get(item.type) }}
+    </div>
+    <template #footer>
+      <el-button type="primary" @click="errorDialogVisible = false">知道了</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import ProcessNodeTree from './ProcessNodeTree.vue'
+import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from './consts'
+import { useWatchNode } from './node'
+import { Select, ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue'
+defineOptions({
+  name: 'SimpleProcessModel'
+})
+
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  },
+  readonly: {
+    type: Boolean,
+    required: false,
+    default: true
+  }
+})
+const emits = defineEmits<{
+  'save': [node: SimpleFlowNode | undefined]
+}>()
+
+const processNodeTree = useWatchNode(props)
+
+provide('readonly', props.readonly)
+let scaleValue = ref(100)
+const MAX_SCALE_VALUE = 200
+const MIN_SCALE_VALUE = 50
+// 放大
+const zoomIn = () => {
+  if (scaleValue.value == MAX_SCALE_VALUE) {
+    return
+  }
+  scaleValue.value += 10
+}
+// 缩小
+const zoomOut = () => {
+  if (scaleValue.value == MIN_SCALE_VALUE) {
+    return
+  }
+  scaleValue.value -= 10
+}
+const processReZoom = () => {
+  scaleValue.value = 100
+}
+
+const errorDialogVisible = ref(false)
+let errorNodes: SimpleFlowNode[] = []
+const saveSimpleFlowModel = async () => {
+  errorNodes = []
+  validateNode(processNodeTree.value, errorNodes)
+  if (errorNodes.length > 0) {
+    errorDialogVisible.value = true
+    return
+  }
+  emits('save', processNodeTree.value)
+}
+// 校验节点设置。 暂时以 showText 为空 未节点错误配置
+const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
+  if (node) {
+    const { type, showText, conditionNodes } = node
+    if (type == NodeType.END_EVENT_NODE) {
+      return
+    }
+    if (type == NodeType.START_USER_NODE) {
+      // 发起人节点暂时不用校验,直接校验孩子节点
+      validateNode(node.childNode, errorNodes)
+    }
+
+    if (
+      type === NodeType.USER_TASK_NODE ||
+      type === NodeType.COPY_TASK_NODE ||
+      type === NodeType.CONDITION_NODE
+    ) {
+      if (!showText) {
+        errorNodes.push(node)
+      }
+      validateNode(node.childNode, errorNodes)
+    }
+
+    if (
+      type == NodeType.CONDITION_BRANCH_NODE ||
+      type == NodeType.PARALLEL_BRANCH_NODE ||
+      type == NodeType.INCLUSIVE_BRANCH_NODE
+    ) {
+      // 分支节点
+      // 1. 先校验各个分支
+      conditionNodes?.forEach((item) => {
+        validateNode(item, errorNodes)
+      })
+      // 2. 校验孩子节点
+      validateNode(node.childNode, errorNodes)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 48 - 0
src/components/SimpleProcessDesignerV2/src/SimpleProcessViewer.vue

@@ -0,0 +1,48 @@
+<template>
+  <SimpleProcessModel :flow-node="simpleModel" :readonly="true" />
+</template>
+
+<script setup lang="ts">
+import { useWatchNode } from './node'
+import { SimpleFlowNode } from './consts'
+
+defineOptions({
+  name: 'SimpleProcessViewer'
+})
+
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  },
+  // 流程任务
+  tasks: {
+    type: Array,
+    default: () => [] as any[]
+  },
+  // 流程实例
+  processInstance: {
+    type: Object,
+    default: () => undefined
+  }
+})
+const approveTasks = ref<any[]>(props.tasks)
+const currentProcessInstance = ref(props.processInstance)
+const simpleModel = useWatchNode(props)
+watch(
+  () => props.tasks,
+  (newValue) => {
+    approveTasks.value = newValue
+  }
+)
+watch(
+  () => props.processInstance,
+  (newValue) => {
+    currentProcessInstance.value = newValue
+  }
+)
+
+provide('tasks', approveTasks)
+provide('processInstance', currentProcessInstance)
+</script>
+p

+ 38 - 12
src/components/SimpleProcessDesignerV2/src/consts.ts

@@ -1,6 +1,6 @@
 // @ts-ignore
 import { DictDataVO } from '@/api/system/dict/types'
-
+import { TaskStatusEnum } from '@/api/bpm/task'
 /**
  * 节点类型
  */
@@ -79,7 +79,7 @@ export interface SimpleFlowNode {
   // 审批按钮设置
   buttonsSetting?: any[]
   // 表单权限
-  fieldsPermission?: Array<Record<string, string>>
+  fieldsPermission?: Array<Record<string, any>>
   // 审批任务超时处理
   timeoutHandler?: TimeoutHandler
   // 审批任务拒绝处理
@@ -96,7 +96,8 @@ export interface SimpleFlowNode {
   conditionGroups?: ConditionGroup
   // 是否默认的条件
   defaultFlow?: boolean
-
+  // 活动的状态,用于前端节点状态展示
+  activityStatus?: TaskStatusEnum
 }
 // 候选人策略枚举 ( 用于审批节点。抄送节点 )
 export enum CandidateStrategy {
@@ -144,6 +145,14 @@ export enum CandidateStrategy {
    * 指定用户组
    */
   USER_GROUP = 40,
+  /**
+   * 表单内用户字段
+   */
+  FORM_USER = 50,
+  /**
+   * 表单内部门负责人
+   */
+  FORM_DEPT_LEADER = 51,
   /**
    * 流程表达式
    */
@@ -178,7 +187,7 @@ export enum ApproveMethodType {
 export type RejectHandler = {
   // 审批拒绝类型
   type: RejectHandlerType
-  // 退节点 Id
+  // 退节点 Id
   returnNodeId?: string
 }
 
@@ -360,9 +369,13 @@ export enum OperationButtonType {
    */
   ADD_SIGN = 5,
   /**
-   * 回退
+   * 退回
+   */
+  RETURN = 6,
+  /**
+   * 抄送
    */
-  RETURN = 6
+  COPY = 7
 }
 
 /**
@@ -419,6 +432,8 @@ export const CANDIDATE_STRATEGY: DictDataVO[] = [
   { label: '发起人部门负责人', value: CandidateStrategy.START_USER_DEPT_LEADER },
   { label: '发起人连续部门负责人', value: CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER },
   { label: '用户组', value: CandidateStrategy.USER_GROUP },
+  { label: '表单内用户字段', value: CandidateStrategy.FORM_USER },
+  { label: '表单内部门负责人', value: CandidateStrategy.FORM_DEPT_LEADER },
   { label: '流程表达式', value: CandidateStrategy.EXPRESSION }
 ]
 // 审批节点 的审批类型
@@ -503,16 +518,17 @@ OPERATION_BUTTON_NAME.set(OperationButtonType.REJECT, '拒绝')
 OPERATION_BUTTON_NAME.set(OperationButtonType.TRANSFER, '转办')
 OPERATION_BUTTON_NAME.set(OperationButtonType.DELEGATE, '委派')
 OPERATION_BUTTON_NAME.set(OperationButtonType.ADD_SIGN, '加签')
-OPERATION_BUTTON_NAME.set(OperationButtonType.RETURN, '回退')
+OPERATION_BUTTON_NAME.set(OperationButtonType.RETURN, '退回')
+OPERATION_BUTTON_NAME.set(OperationButtonType.COPY, '抄送')
 
 // 默认的按钮权限设置
 export const DEFAULT_BUTTON_SETTING: ButtonSetting[] = [
   { id: OperationButtonType.APPROVE, displayName: '通过', enable: true },
   { id: OperationButtonType.REJECT, displayName: '拒绝', enable: true },
-  { id: OperationButtonType.TRANSFER, displayName: '转办', enable: false },
-  { id: OperationButtonType.DELEGATE, displayName: '委派', enable: false },
-  { id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: false },
-  { id: OperationButtonType.RETURN, displayName: '回退', enable: false }
+  { id: OperationButtonType.TRANSFER, displayName: '转办', enable: true },
+  { id: OperationButtonType.DELEGATE, displayName: '委派', enable: true },
+  { id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: true },
+  { id: OperationButtonType.RETURN, displayName: '退回', enable: true }
 ]
 
 // 发起人的按钮权限。暂时定死,不可以编辑
@@ -522,7 +538,7 @@ export const START_USER_BUTTON_SETTING: ButtonSetting[] = [
   { id: OperationButtonType.TRANSFER, displayName: '转办', enable: false },
   { id: OperationButtonType.DELEGATE, displayName: '委派', enable: false },
   { id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: false },
-  { id: OperationButtonType.RETURN, displayName: '退', enable: false }
+  { id: OperationButtonType.RETURN, displayName: '退', enable: false }
 ]
 
 export const MULTI_LEVEL_DEPT: DictDataVO = [
@@ -542,3 +558,13 @@ export const MULTI_LEVEL_DEPT: DictDataVO = [
   { label: '第 14 级部门', value: 14 },
   { label: '第 15 级部门', value: 15 }
 ]
+
+/**
+ * 流程实例的变量枚举
+ */
+export enum ProcessVariableEnum {
+  /**
+   * 发起用户 ID
+   */
+  START_USER_ID = 'PROCESS_START_USER_ID'
+}

+ 2 - 1
src/components/SimpleProcessDesignerV2/src/index.ts

@@ -1,4 +1,5 @@
 import SimpleProcessDesigner from './SimpleProcessDesigner.vue'
+import SimpleProcessViewer from './SimpleProcessViewer.vue'
 import '../theme/simple-process-designer.scss'
 
-export { SimpleProcessDesigner }
+export { SimpleProcessDesigner, SimpleProcessViewer}

+ 96 - 79
src/components/SimpleProcessDesignerV2/src/node.ts

@@ -1,4 +1,5 @@
 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'
 import * as PostApi from '@/api/system/post'
@@ -13,8 +14,10 @@ import {
   NODE_DEFAULT_NAME,
   AssignStartUserHandlerType,
   AssignEmptyHandlerType,
-  FieldPermissionType
+  FieldPermissionType,
+  ProcessVariableEnum
 } from './consts'
+import { parseFormFields } from '@/components/FormCreate/src/utils/index'
 export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> {
   const node = ref<SimpleFlowNode>(props.flowNode)
   watch(
@@ -26,12 +29,30 @@ export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlo
   return node
 }
 
+// 解析 formCreate 所有表单字段, 并返回
+const parseFormCreateFields = (formFields?: string[]) => {
+  const result: Array<Record<string, any>> = []
+  if (formFields) {
+    formFields.forEach((fieldStr: string) => {
+      parseFormFields(JSON.parse(fieldStr), result)
+    })
+  }
+  // 固定添加发起人 ID 字段
+  result.unshift({
+    field: ProcessVariableEnum.START_USER_ID,
+    title: '发起人',
+    type: 'UserSelect',
+    required: true
+  })
+  return result
+}
+
 /**
  * @description 表单数据权限配置,用于发起人节点 、审批节点、抄送节点
  */
 export function useFormFieldsPermission(defaultPermission: FieldPermissionType) {
   // 字段权限配置. 需要有 field, title,  permissioin 属性
-  const fieldsPermissionConfig = ref<Array<Record<string, string>>>([])
+  const fieldsPermissionConfig = ref<Array<Record<string, any>>>([])
 
   const formType = inject<Ref<number>>('formType') // 表单类型
 
@@ -44,49 +65,26 @@ export function useFormFieldsPermission(defaultPermission: FieldPermissionType)
   }
   // 默认的表单权限: 获取表单的所有字段,设置字段默认权限为只读
   const getDefaultFieldsPermission = (formFields?: string[]) => {
-    const defaultFieldsPermission: Array<Record<string, string>> = []
+    let defaultFieldsPermission: Array<Record<string, any>> = []
     if (formFields) {
-      formFields.forEach((fieldStr: string) => {
-        parseFieldsSetDefaultPermission(JSON.parse(fieldStr), defaultFieldsPermission)
+      defaultFieldsPermission = parseFormCreateFields(formFields).map((item) => {
+        return {
+          field: item.field,
+          title: item.title,
+          permission: defaultPermission
+        }
       })
     }
     return defaultFieldsPermission
   }
-  // 解析字段。赋给默认权限
-  const parseFieldsSetDefaultPermission = (
-    rule: Record<string, any>,
-    fieldsPermission: Array<Record<string, string>>,
-    parentTitle: string = ''
-  ) => {
-    const { /**type,*/ field, title: tempTitle, children } = rule
-    if (field && tempTitle) {
-      let title = tempTitle
-      if (parentTitle) {
-        title = `${parentTitle}.${tempTitle}`
-      }
-      fieldsPermission.push({
-        field,
-        title,
-        permission: defaultPermission
-      })
-      // TODO 子表单 需要处理子表单字段
-      // if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
-      //   // 解析子表单的字段
-      //   rule.props.rule.forEach((item) => {
-      //     parseFieldsSetDefaultPermission(item, fieldsPermission, title)
-      //   })
-      // }
-    }
-    if (children && Array.isArray(children)) {
-      children.forEach((rule) => {
-        parseFieldsSetDefaultPermission(rule, fieldsPermission)
-      })
-    }
-  }
+
+  // 获取表单的所有字段,作为下拉框选项
+  const formFieldOptions = parseFormCreateFields(unref(formFields))
 
   return {
     formType,
     fieldsPermissionConfig,
+    formFieldOptions,
     getNodeConfigFormFields
   }
 }
@@ -94,50 +92,8 @@ export function useFormFieldsPermission(defaultPermission: FieldPermissionType)
  * @description 获取表单的字段
  */
 export function useFormFields() {
-  // 解析后的表单字段
   const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
-  const parseFormFields = () => {
-    const parsedFormFields: Array<Record<string, string>> = []
-    if (formFields) {
-      formFields.value.forEach((fieldStr: string) => {
-        parseField(JSON.parse(fieldStr), parsedFormFields)
-      })
-    }
-    return parsedFormFields
-  }
-  // 解析字段。
-  const parseField = (
-    rule: Record<string, any>,
-    parsedFormFields: Array<Record<string, string>>,
-    parentTitle: string = ''
-  ) => {
-    const { field, title: tempTitle, children, type } = rule
-    if (field && tempTitle) {
-      let title = tempTitle
-      if (parentTitle) {
-        title = `${parentTitle}.${tempTitle}`
-      }
-      parsedFormFields.push({
-        field,
-        title,
-        type
-      })
-      // TODO 子表单 需要处理子表单字段
-      // if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
-      //   // 解析子表单的字段
-      //   rule.props.rule.forEach((item) => {
-      //     parseFieldsSetDefaultPermission(item, fieldsPermission, title)
-      //   })
-      // }
-    }
-    if (children && Array.isArray(children)) {
-      children.forEach((rule) => {
-        parseField(rule, parsedFormFields)
-      })
-    }
-  }
-
-  return parseFormFields()
+  return parseFormCreateFields(unref(formFields))
 }
 
 export type UserTaskFormType = {
@@ -151,6 +107,8 @@ export type UserTaskFormType = {
   userGroups?: number[] // 用户组
   postIds?: number[] // 岗位
   expression?: string // 流程表达式
+  formUser?: string // 表单内用户字段
+  formDept?: string // 表单内部门字段
   approveRatio?: number
   rejectHandlerType?: RejectHandlerType
   returnNodeId?: string
@@ -173,6 +131,8 @@ export type CopyTaskFormType = {
   userIds?: number[] // 用户
   userGroups?: number[] // 用户组
   postIds?: number[] // 岗位
+  formUser?: string // 表单内用户字段
+  formDept?: string // 表单内部门字段
   expression?: string // 流程表达式
 }
 
@@ -186,6 +146,7 @@ export function useNodeForm(nodeType: NodeType) {
   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 configForm = ref<UserTaskFormType | CopyTaskFormType>()
   if (nodeType === NodeType.USER_TASK_NODE) {
     configForm.value = {
@@ -281,6 +242,18 @@ export function useNodeForm(nodeType: NodeType) {
       }
     }
 
+    // 表单内用户字段
+    if (configForm.value?.candidateStrategy === CandidateStrategy.FORM_USER) {
+      const formFieldOptions = parseFormCreateFields(unref(formFields))
+      const item = formFieldOptions.find((item) => item.field === configForm.value?.formUser)
+      showText = `表单用户:${item?.title}`
+    }
+
+    // 表单内部门负责人
+    if (configForm.value?.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER) {
+      showText = `表单内部门负责人`
+    }
+
     // 发起人自选
     if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER_SELECT) {
       showText = `发起人自选`
@@ -327,6 +300,9 @@ export function useNodeForm(nodeType: NodeType) {
       case CandidateStrategy.USER_GROUP:
         candidateParam = configForm.value.userGroups!.join(',')
         break
+      case CandidateStrategy.FORM_USER:
+        candidateParam = configForm.value.formUser!
+        break
       case CandidateStrategy.EXPRESSION:
         candidateParam = configForm.value.expression!
         break
@@ -346,6 +322,13 @@ export function useNodeForm(nodeType: NodeType) {
         candidateParam = deptIds.concat('|' + configForm.value.deptLevel + '')
         break
       }
+      // 表单内部门的负责人
+      case CandidateStrategy.FORM_DEPT_LEADER: {
+        // 候选人参数格式: | 分隔 。左边为表单内部门字段。 右边为部门层级
+        const deptFieldOnForm = configForm.value.formDept!
+        candidateParam = deptFieldOnForm.concat('|' + configForm.value.deptLevel + '')
+        break
+      }
       default:
         break
     }
@@ -375,6 +358,9 @@ export function useNodeForm(nodeType: NodeType) {
       case CandidateStrategy.USER_GROUP:
         configForm.value.userGroups = candidateParam.split(',').map((item) => +item)
         break
+      case CandidateStrategy.FORM_USER:
+        configForm.value.formUser = candidateParam
+        break
       case CandidateStrategy.EXPRESSION:
         configForm.value.expression = candidateParam
         break
@@ -395,6 +381,14 @@ export function useNodeForm(nodeType: NodeType) {
         configForm.value.deptLevel = +paramArray[1]
         break
       }
+      // 表单内的部门负责人
+      case CandidateStrategy.FORM_DEPT_LEADER: {
+        // 候选人参数格式: | 分隔 。左边为表单内的部门字段。 右边为部门层级
+        const paramArray = candidateParam.split('|')
+        configForm.value.formDept = paramArray[0]
+        configForm.value.deptLevel = +paramArray[1]
+        break
+      }
       default:
         break
     }
@@ -476,3 +470,26 @@ export function useNodeName2(node: Ref<SimpleFlowNode>, nodeType: NodeType) {
     blurEvent
   }
 }
+
+/**
+ * @description 根据节点任务状态,获取节点任务状态样式
+ */
+export function useTaskStatusClass(taskStatus: TaskStatusEnum | undefined): string {
+  if (!taskStatus) {
+    return ''
+  }
+  if (taskStatus === TaskStatusEnum.APPROVE) {
+    return 'status-pass'
+  }
+  if (taskStatus === TaskStatusEnum.RUNNING) {
+    return 'status-running'
+  }
+  if (taskStatus === TaskStatusEnum.REJECT) {
+    return 'status-reject'
+  }
+  if (taskStatus === TaskStatusEnum.CANCEL) {
+    return 'status-cancel'
+  }
+
+  return ''
+}

+ 1 - 1
src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue

@@ -26,7 +26,7 @@
       </div>
     </template>
     <div>
-      <div class="mb-3 font-size-16px" v-if="currentNode.defaultFlow">其它条件不满足进入此分支(该分支不可编辑和删除)</div>
+      <div class="mb-3 font-size-16px" v-if="currentNode.defaultFlow">未满足其它条件时,将进入此分支(该分支不可编辑和删除)</div>
       <div v-else>
         <el-form
           ref="formRef"

+ 78 - 11
src/components/SimpleProcessDesignerV2/src/nodes-config/CopyTaskNodeConfig.vue

@@ -60,7 +60,8 @@
             <el-form-item
               v-if="
                 configForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER ||
-                configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER
+                configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER ||
+                configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
               "
               label="指定部门"
               prop="deptIds"
@@ -122,7 +123,57 @@
                 />
               </el-select>
             </el-form-item>
-
+            <el-form-item
+              v-if="configForm.candidateStrategy === CandidateStrategy.FORM_USER"
+              label="表单内用户字段"
+              prop="formUser"
+            >
+              <el-select v-model="configForm.formUser" clearable style="width: 100%">
+                <el-option
+                  v-for="(item, idx) in userFieldOnFormOptions"
+                  :key="idx"
+                  :label="item.title"
+                  :value="item.field"
+                  :disabled ="!item.required"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item
+              v-if="configForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER"
+              label="表单内部门字段"
+              prop="formDept"
+            >
+              <el-select v-model="configForm.formDept" clearable style="width: 100%">
+                <el-option
+                  v-for="(item, idx) in deptFieldOnFormOptions"
+                  :key="idx"
+                  :label="item.title"
+                  :value="item.field"
+                  :disabled ="!item.required"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item
+              v-if="
+                configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
+                configForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
+                configForm.candidateStrategy ==
+                  CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER ||
+                configForm.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER
+              "
+              :label="deptLevelLabel!"
+              prop="deptLevel"
+              span="24"
+            >
+              <el-select v-model="configForm.deptLevel" clearable>
+                <el-option
+                  v-for="(item, index) in MULTI_LEVEL_DEPT"
+                  :key="index"
+                  :label="item.label"
+                  :value="item.value"
+                />
+              </el-select>
+            </el-form-item>
             <el-form-item
               v-if="configForm.candidateStrategy === CandidateStrategy.EXPRESSION"
               label="流程表达式"
@@ -201,7 +252,8 @@ import {
   CandidateStrategy,
   NodeType,
   CANDIDATE_STRATEGY,
-  FieldPermissionType
+  FieldPermissionType,
+  MULTI_LEVEL_DEPT
 } from '../consts'
 import {
   useWatchNode,
@@ -221,6 +273,15 @@ const props = defineProps({
     required: true
   }
 })
+const deptLevelLabel = computed(() => {
+  let label = '部门负责人来源'
+  if (configForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) {
+    label = label + '(指定部门向上)'
+  } else {
+    label = label + '(发起人部门向上)'
+  }
+  return label
+})
 // 抽屉配置
 const { settingVisible, closeDrawer, openDrawer } = useDrawer()
 // 当前节点
@@ -230,9 +291,16 @@ const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.COPY_
 // 激活的 Tab 标签页
 const activeTabName = ref('user')
 // 表单字段权限配置
-const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
-  FieldPermissionType.READ
-)
+const { formType, fieldsPermissionConfig, formFieldOptions, getNodeConfigFormFields } =
+  useFormFieldsPermission(FieldPermissionType.READ)
+// 表单内用户字段选项, 必须是必填和用户选择器
+const userFieldOnFormOptions = computed(() => {
+  return formFieldOptions.filter((item) => item.type === 'UserSelect')
+})
+// 表单内部门字段选项, 必须是必填和部门选择器
+const deptFieldOnFormOptions = computed(() => {
+  return formFieldOptions.filter((item) => item.type === 'DeptSelect')
+})
 // 抄送人表单配置
 const formRef = ref() // 表单 Ref
 // 表单校验规则
@@ -243,6 +311,8 @@ const formRules = reactive({
   deptIds: [{ required: true, message: '部门不能为空', trigger: 'change' }],
   userGroups: [{ required: true, message: '用户组不能为空', trigger: 'change' }],
   postIds: [{ required: true, message: '岗位不能为空', trigger: 'change' }],
+  formUser: [{ required: true, message: '表单内用户字段不能为空', trigger: 'change' }],
+  formDept: [{ required: true, message: '表单内部门字段不能为空', trigger: 'change' }],
   expression: [{ required: true, message: '流程表达式不能为空', trigger: 'blur' }]
 })
 
@@ -260,11 +330,7 @@ const {
 const configForm = tempConfigForm as Ref<CopyTaskFormType>
 // 抄送人策略, 去掉发起人自选 和 发起人自己
 const copyUserStrategies = computed(() => {
-  return CANDIDATE_STRATEGY.filter(
-    (item) =>
-      item.value !== CandidateStrategy.START_USER_SELECT &&
-      item.value !== CandidateStrategy.START_USER
-  )
+  return CANDIDATE_STRATEGY.filter((item) => item.value !== CandidateStrategy.START_USER)
 })
 // 改变抄送人设置策略
 const changeCandidateStrategy = () => {
@@ -274,6 +340,7 @@ const changeCandidateStrategy = () => {
   configForm.value.postIds = []
   configForm.value.userGroups = []
   configForm.value.deptLevel = 1
+  configForm.value.formUser = ''
 }
 // 保存配置
 const saveConfig = async () => {

+ 0 - 1
src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue

@@ -119,7 +119,6 @@ const saveConfig = async () => {
   currentNode.value.fieldsPermission = fieldsPermissionConfig.value
   // 设置发起人的按钮权限
   currentNode.value.buttonsSetting = START_USER_BUTTON_SETTING
-  console.log('currentNode.value.buttonsSetting==>', currentNode.value.buttonsSetting)
   settingVisible.value = false
   return true
 }

+ 72 - 68
src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue

@@ -56,7 +56,6 @@
                 </el-radio>
               </el-radio-group>
             </el-form-item>
-
             <el-form-item
               v-if="configForm.candidateStrategy == CandidateStrategy.ROLE"
               label="指定角色"
@@ -94,25 +93,6 @@
                 show-checkbox
               />
             </el-form-item>
-            <el-form-item
-              v-if="
-                configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
-                configForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
-                configForm.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
-              "
-              :label="deptLevelLabel!"
-              prop="deptLevel"
-              span="24"
-            >
-              <el-select v-model="configForm.deptLevel" clearable>
-                <el-option
-                  v-for="(item, index) in MULTI_LEVEL_DEPT"
-                  :key="index"
-                  :label="item.label"
-                  :value="item.value"
-                />
-              </el-select>
-            </el-form-item>
             <el-form-item
               v-if="configForm.candidateStrategy == CandidateStrategy.POST"
               label="指定岗位"
@@ -134,13 +114,7 @@
               prop="userIds"
               span="24"
             >
-              <el-select
-                v-model="configForm.userIds"
-                clearable
-                multiple
-                style="width: 100%"
-                @change="changedCandidateUsers"
-              >
+              <el-select v-model="configForm.userIds" clearable multiple style="width: 100%">
                 <el-option
                   v-for="item in userOptions"
                   :key="item.id"
@@ -163,6 +137,57 @@
                 />
               </el-select>
             </el-form-item>
+            <el-form-item
+              v-if="configForm.candidateStrategy === CandidateStrategy.FORM_USER"
+              label="表单内用户字段"
+              prop="formUser"
+            >
+              <el-select v-model="configForm.formUser" clearable style="width: 100%">
+                <el-option
+                  v-for="(item, idx) in userFieldOnFormOptions"
+                  :key="idx"
+                  :label="item.title"
+                  :value="item.field"
+                  :disabled ="!item.required"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item
+              v-if="configForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER"
+              label="表单内部门字段"
+              prop="formDept"
+            >
+              <el-select v-model="configForm.formDept" clearable style="width: 100%">
+                <el-option
+                  v-for="(item, idx) in deptFieldOnFormOptions"
+                  :key="idx"
+                  :label="item.title"
+                  :value="item.field"
+                  :disabled ="!item.required"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item
+              v-if="
+                configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
+                configForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
+                configForm.candidateStrategy ==
+                  CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER ||
+                configForm.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER
+              "
+              :label="deptLevelLabel!"
+              prop="deptLevel"
+              span="24"
+            >
+              <el-select v-model="configForm.deptLevel" clearable>
+                <el-option
+                  v-for="(item, index) in MULTI_LEVEL_DEPT"
+                  :key="index"
+                  :label="item.label"
+                  :value="item.value"
+                />
+              </el-select>
+            </el-form-item>
             <!-- TODO @jason:后续要支持选择已经存好的表达式 -->
             <el-form-item
               v-if="configForm.candidateStrategy === CandidateStrategy.EXPRESSION"
@@ -184,14 +209,7 @@
                     :key="index"
                     class="flex items-center"
                   >
-                    <el-radio
-                      :value="item.value"
-                      :label="item.value"
-                      :disabled="
-                        item.value !== ApproveMethodType.RANDOM_SELECT_ONE_APPROVE &&
-                        notAllowedMultiApprovers
-                      "
-                    >
+                    <el-radio :value="item.value" :label="item.value">
                       {{ item.label }}
                     </el-radio>
                     <el-form-item prop="approveRatio">
@@ -481,6 +499,8 @@ const deptLevelLabel = computed(() => {
   let label = '部门负责人来源'
   if (configForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) {
     label = label + '(指定部门向上)'
+  } else if (configForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER) {
+    label = label + '(表单内部门向上)'
   } else {
     label = label + '(发起人部门向上)'
   }
@@ -495,9 +515,16 @@ const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.USER_
 // 激活的 Tab 标签页
 const activeTabName = ref('user')
 // 表单字段权限设置
-const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
-  FieldPermissionType.READ
-)
+const { formType, fieldsPermissionConfig, formFieldOptions, getNodeConfigFormFields } =
+  useFormFieldsPermission(FieldPermissionType.READ)
+// 表单内用户字段选项, 必须是必填和用户选择器
+const userFieldOnFormOptions = computed(() => {
+  return formFieldOptions.filter((item) => item.type === 'UserSelect')
+})
+// 表单内部门字段选项, 必须是必填和部门选择器
+const deptFieldOnFormOptions = computed(() => {
+  return formFieldOptions.filter((item) => item.type === 'DeptSelect')
+})
 // 操作按钮设置
 const { buttonsSetting, btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } =
   useButtonsSetting()
@@ -511,6 +538,8 @@ const formRules = reactive({
   roleIds: [{ required: true, message: '角色不能为空', trigger: 'change' }],
   deptIds: [{ required: true, message: '部门不能为空', trigger: 'change' }],
   userGroups: [{ required: true, message: '用户组不能为空', trigger: 'change' }],
+  formUser: [{ required: true, message: '表单内用户字段不能为空', trigger: 'change' }],
+  formDept: [{ required: true, message: '表单内部门字段不能为空', trigger: 'change' }],
   postIds: [{ required: true, message: '岗位不能为空', trigger: 'change' }],
   expression: [{ required: true, message: '流程表达式不能为空', trigger: 'blur' }],
   approveMethod: [{ required: true, message: '多人审批方式不能为空', trigger: 'change' }],
@@ -537,8 +566,7 @@ const {
   getShowText
 } = useNodeForm(NodeType.USER_TASK_NODE)
 const configForm = tempConfigForm as Ref<UserTaskFormType>
-// 不允许多人审批
-const notAllowedMultiApprovers = ref(false)
+
 // 改变审批人设置策略
 const changeCandidateStrategy = () => {
   configForm.value.userIds = []
@@ -547,30 +575,11 @@ const changeCandidateStrategy = () => {
   configForm.value.postIds = []
   configForm.value.userGroups = []
   configForm.value.deptLevel = 1
+  configForm.value.formUser = ''
+  configForm.value.formDept = ''
   configForm.value.approveMethod = ApproveMethodType.SEQUENTIAL_APPROVE
-  if (
-    configForm.value.candidateStrategy === CandidateStrategy.START_USER ||
-    configForm.value.candidateStrategy === CandidateStrategy.USER
-  ) {
-    notAllowedMultiApprovers.value = true
-  } else {
-    notAllowedMultiApprovers.value = false
-  }
-}
-// 改变审批候选人
-const changedCandidateUsers = () => {
-  if (
-    configForm.value.userIds &&
-    configForm.value.userIds?.length <= 1 &&
-    configForm.value.candidateStrategy === CandidateStrategy.USER
-  ) {
-    configForm.value.approveMethod = ApproveMethodType.RANDOM_SELECT_ONE_APPROVE
-    configForm.value.rejectHandlerType = RejectHandlerType.FINISH_PROCESS
-    notAllowedMultiApprovers.value = true
-  } else {
-    notAllowedMultiApprovers.value = false
-  }
 }
+
 // 审批方式改变
 const approveMethodChanged = () => {
   configForm.value.rejectHandlerType = RejectHandlerType.FINISH_PROCESS
@@ -579,7 +588,7 @@ const approveMethodChanged = () => {
   }
   formRef.value.clearValidate('approveRatio')
 }
-// 审批拒绝 可退的节点
+// 审批拒绝 可退的节点
 const returnTaskList = ref<SimpleFlowNode[]>([])
 // 审批人超时未处理设置
 const {
@@ -666,11 +675,6 @@ const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
   configForm.value.candidateStrategy = node.candidateStrategy!
   // 解析候选人参数
   parseCandidateParam(node.candidateStrategy!, node?.candidateParam)
-  if (configForm.value.userIds && configForm.value.userIds.length > 1) {
-    notAllowedMultiApprovers.value = true
-  } else {
-    notAllowedMultiApprovers.value = false
-  }
   // 2.2 设置审批方式
   configForm.value.approveMethod = node.approveMethod!
   if (node.approveMethod == ApproveMethodType.APPROVE_BY_RATIO) {

+ 26 - 8
src/components/SimpleProcessDesignerV2/src/nodes/CopyTaskNode.vue

@@ -1,11 +1,17 @@
 <template>
   <div class="node-wrapper">
     <div class="node-container">
-      <div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
+      <div
+        class="node-box"
+        :class="[
+          { 'node-config-error': !currentNode.showText },
+          `${useTaskStatusClass(currentNode?.activityStatus)}`
+        ]"
+      >
         <div class="node-title-container">
           <div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div>
           <input
-            v-if="showInput"
+            v-if="!readonly && showInput"
             type="text"
             class="editable-title-input"
             @blur="blurEvent()"
@@ -24,9 +30,9 @@
           <div class="node-text" v-else>
             {{ NODE_DEFAULT_TEXT.get(NodeType.COPY_TASK_NODE) }}
           </div>
-          <Icon icon="ep:arrow-right-bold" />
+          <Icon v-if="!readonly" icon="ep:arrow-right-bold" />
         </div>
-        <div class="node-toolbar">
+        <div v-if="!readonly" class="node-toolbar">
           <div class="toolbar-icon"
             ><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
           /></div>
@@ -34,15 +40,23 @@
       </div>
 
       <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
-      <NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
+      <NodeHandler
+        v-if="currentNode"
+        v-model:child-node="currentNode.childNode"
+        :current-node="currentNode"
+      />
     </div>
-    <CopyTaskNodeConfig v-if="currentNode" ref="nodeSetting" :flow-node="currentNode" />
+    <CopyTaskNodeConfig
+      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 } from '../node'
+import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node'
 import CopyTaskNodeConfig from '../nodes-config/CopyTaskNodeConfig.vue'
 defineOptions({
   name: 'CopyTaskNode'
@@ -57,7 +71,8 @@ const props = defineProps({
 const emits = defineEmits<{
   'update:flowNode': [node: SimpleFlowNode | undefined]
 }>()
-
+// 是否只读
+const readonly = inject<Boolean>('readonly')
 // 监控节点的变化
 const currentNode = useWatchNode(props)
 // 节点名称编辑
@@ -66,6 +81,9 @@ const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.
 const nodeSetting = ref()
 // 打开节点配置
 const openNodeConfig = () => {
+  if (readonly) {
+    return
+  }
   nodeSetting.value.showCopyTaskNodeConfig(currentNode.value)
   nodeSetting.value.openDrawer()
 }

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

@@ -1,13 +1,102 @@
 <template>
   <div class="end-node-wrapper">
-    <div class="end-node-box">
+    <div class="end-node-box cursor-pointer" :class="`${useTaskStatusClass(currentNode?.activityStatus)}`" @click="nodeClick">
       <span class="node-fixed-name" title="结束">结束</span>
     </div>
   </div>
+  <el-dialog title="审批信息" v-model="dialogVisible" width="1000px" append-to-body>
+      <el-row>
+        <el-table
+          :data="processInstanceInfos"
+          size="small"
+          border
+          header-cell-class-name="table-header-gray"
+        >
+          <el-table-column
+            label="序号"
+            header-align="center"
+            align="center"
+            type="index"
+            width="50"
+          />
+          <el-table-column
+            label="发起人"
+            prop="assigneeUser.nickname"
+            min-width="100"
+            align="center"
+          />
+          <el-table-column label="部门" min-width="100" align="center">
+            <template #default="scope">
+              {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
+            </template>
+          </el-table-column>
+          <el-table-column
+            :formatter="dateFormatter"
+            align="center"
+            label="开始时间"
+            prop="createTime"
+            min-width="140"
+          />
+          <el-table-column
+            :formatter="dateFormatter"
+            align="center"
+            label="结束时间"
+            prop="endTime"
+            min-width="140"
+          />
+          <el-table-column align="center" label="审批状态" prop="status" min-width="90">
+            <template #default="scope">
+              <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
+            </template>
+          </el-table-column>
+         
+          <el-table-column align="center" label="耗时" prop="durationInMillis" width="100">
+            <template #default="scope">
+              {{ formatPast2(scope.row.durationInMillis) }}
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-row>
+    </el-dialog>
 </template>
 <script setup lang="ts">
+import { SimpleFlowNode } from '../consts'
+import { useWatchNode, useTaskStatusClass } from '../node'
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
+import { DICT_TYPE } from '@/utils/dict'
 defineOptions({
   name: 'EndEventNode'
 })
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    default: () => null
+  }
+})
+// 监控节点变化
+const currentNode = useWatchNode(props)
+// 是否只读
+const readonly = inject<Boolean>('readonly')
+const processInstance = inject<Ref<any>>('processInstance')
+// 审批信息的弹窗显示,用于只读模式
+const dialogVisible = ref(false) // 弹窗可见性
+const processInstanceInfos = ref<any[]>([]) // 流程的审批信息
+
+const nodeClick = () => {
+  if (readonly) { 
+    if(processInstance && processInstance.value){
+      processInstanceInfos.value = [
+      {
+        assigneeUser: processInstance.value.startUser,
+        createTime: processInstance.value.startTime,
+        endTime: processInstance.value.endTime,
+        status: processInstance.value.status,
+        durationInMillis: processInstance.value.durationInMillis
+      }
+    ]
+      dialogVisible.value = true
+    }
+  }
+}
 </script>
 <style lang="scss" scoped></style>

+ 36 - 14
src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue

@@ -1,7 +1,17 @@
 <template>
   <div class="branch-node-wrapper">
     <div class="branch-node-container">
-      <div class="branch-node-add" @click="addCondition">添加条件</div>
+      <div
+        v-if="readonly"
+        class="branch-node-readonly"
+        :class="`${useTaskStatusClass(currentNode?.activityStatus)}`"
+      >
+        <span class="iconfont icon-exclusive icon-size condition"></span>
+      </div>
+      <el-button v-else class="branch-node-add" color="#67c23a" @click="addCondition" plain
+        >添加条件</el-button
+      >
+
       <div
         class="branch-node-item"
         v-for="(item, index) in currentNode.conditionNodes"
@@ -17,9 +27,15 @@
         </template>
         <div class="node-wrapper">
           <div class="node-container">
-            <div class="node-box" :class="{ 'node-config-error': !item.showText }">
+            <div
+              class="node-box"
+              :class="[
+                { 'node-config-error': !item.showText },
+                `${useTaskStatusClass(item.activityStatus)}`
+              ]"
+            >
               <div class="branch-node-title-container">
-                <div v-if="showInputs[index]">
+                <div v-if="!readonly && showInputs[index]">
                   <input
                     type="text"
                     class="input-max-width editable-title-input"
@@ -39,7 +55,10 @@
                   {{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
                 </div>
               </div>
-              <div class="node-toolbar" v-if="index + 1 !== currentNode.conditionNodes?.length">
+              <div
+                class="node-toolbar"
+                v-if="!readonly && index + 1 !== currentNode.conditionNodes?.length"
+              >
                 <div class="toolbar-icon">
                   <Icon
                     color="#0089ff"
@@ -65,7 +84,7 @@
                 <Icon icon="ep:arrow-right" />
               </div>
             </div>
-            <NodeHandler v-model:child-node="item.childNode" />
+            <NodeHandler v-model:child-node="item.childNode" :current-node="item" />
           </div>
         </div>
         <ConditionNodeConfig :node-index="index" :condition-node="item" :ref="item.id" />
@@ -78,7 +97,11 @@
         />
       </div>
     </div>
-    <NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
+    <NodeHandler
+      v-if="currentNode"
+      v-model:child-node="currentNode.childNode"
+      :current-node="currentNode"
+    />
   </div>
 </template>
 
@@ -87,6 +110,7 @@ import NodeHandler from '../NodeHandler.vue'
 import ProcessNodeTree from '../ProcessNodeTree.vue'
 import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
 import { getDefaultConditionNodeName } from '../utils'
+import { useTaskStatusClass } from '../node'
 import { generateUUID } from '@/utils'
 import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
 const { proxy } = getCurrentInstance() as any
@@ -94,10 +118,6 @@ defineOptions({
   name: 'ExclusiveNode'
 })
 const props = defineProps({
-  // parentNode : {
-  //   type: Object as () => SimpleFlowNode,
-  //   required: true
-  // },
   flowNode: {
     type: Object as () => SimpleFlowNode,
     required: true
@@ -113,10 +133,9 @@ const emits = defineEmits<{
     nodeType: number
   ]
 }>()
-
+// 是否只读
+const readonly = inject<Boolean>('readonly')
 const currentNode = ref<SimpleFlowNode>(props.flowNode)
-// const conditionNodes = computed(() => currentNode.value.conditionNodes);
-
 watch(
   () => props.flowNode,
   (newValue) => {
@@ -139,6 +158,9 @@ const clickEvent = (index: number) => {
 }
 
 const conditionNodeConfig = (nodeId: string) => {
+  if (readonly) {
+    return
+  }
   const conditionNode = proxy.$refs[nodeId][0]
   conditionNode.open()
 }
@@ -193,7 +215,7 @@ const recursiveFindParentNode = (
   node: SimpleFlowNode,
   nodeType: number
 ) => {
-  if (!node || node.type === NodeType.START_EVENT_NODE) {
+  if (!node || node.type === NodeType.START_USER_NODE) {
     return
   }
   if (node.type === nodeType) {

+ 233 - 0
src/components/SimpleProcessDesignerV2/src/nodes/InclusiveNode.vue

@@ -0,0 +1,233 @@
+<template>
+  <div class="branch-node-wrapper">
+    <div class="branch-node-container">
+      <div
+        v-if="readonly"
+        class="branch-node-readonly"
+        :class="`${useTaskStatusClass(currentNode?.activityStatus)}`"
+      >
+        <span class="iconfont icon-inclusive icon-size inclusive"></span>
+      </div>
+      <el-button v-else class="branch-node-add" color="#345da2" @click="addCondition" plain
+        >添加条件</el-button
+      >
+      <div
+        class="branch-node-item"
+        v-for="(item, index) in currentNode.conditionNodes"
+        :key="index"
+      >
+        <template v-if="index == 0">
+          <div class="branch-line-first-top"> </div>
+          <div class="branch-line-first-bottom"></div>
+        </template>
+        <template v-if="index + 1 == currentNode.conditionNodes?.length">
+          <div class="branch-line-last-top"></div>
+          <div class="branch-line-last-bottom"></div>
+        </template>
+        <div class="node-wrapper">
+          <div class="node-container">
+            <div
+              class="node-box"
+              :class="[
+                { 'node-config-error': !item.showText },
+                `${useTaskStatusClass(item.activityStatus)}`
+              ]"
+            >
+              <div class="branch-node-title-container">
+                <div v-if="showInputs[index]">
+                  <input
+                    type="text"
+                    class="editable-title-input"
+                    @blur="blurEvent(index)"
+                    v-mountedFocus
+                    v-model="item.name"
+                  />
+                </div>
+                <div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div>
+              </div>
+              <div class="branch-node-content" @click="conditionNodeConfig(item.id)">
+                <div class="branch-node-text" :title="item.showText" v-if="item.showText">
+                  {{ item.showText }}
+                </div>
+                <div class="branch-node-text" v-else>
+                  {{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
+                </div>
+              </div>
+              <div
+                class="node-toolbar"
+                v-if="!readonly && index + 1 !== currentNode.conditionNodes?.length"
+              >
+                <div class="toolbar-icon">
+                  <Icon
+                    color="#0089ff"
+                    icon="ep:circle-close-filled"
+                    :size="18"
+                    @click="deleteCondition(index)"
+                  />
+                </div>
+              </div>
+              <div
+                class="branch-node-move move-node-left"
+                v-if="!readonly && index != 0 && index + 1 !== currentNode.conditionNodes?.length"
+                @click="moveNode(index, -1)"
+              >
+                <Icon icon="ep:arrow-left" />
+              </div>
+
+              <div
+                class="branch-node-move move-node-right"
+                v-if="
+                  !readonly &&
+                  currentNode.conditionNodes &&
+                  index < currentNode.conditionNodes.length - 2
+                "
+                @click="moveNode(index, 1)"
+              >
+                <Icon icon="ep:arrow-right" />
+              </div>
+            </div>
+            <NodeHandler v-model:child-node="item.childNode" :current-node="item" />
+          </div>
+        </div>
+        <ConditionNodeConfig :node-index="index" :condition-node="item" :ref="item.id" />
+        <!-- 递归显示子节点  -->
+        <ProcessNodeTree
+          v-if="item && item.childNode"
+          :parent-node="item"
+          v-model:flow-node="item.childNode"
+          @find:recursive-find-parent-node="recursiveFindParentNode"
+        />
+      </div>
+    </div>
+    <NodeHandler
+      v-if="currentNode"
+      v-model:child-node="currentNode.childNode"
+      :current-node="currentNode"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import NodeHandler from '../NodeHandler.vue'
+import ProcessNodeTree from '../ProcessNodeTree.vue'
+import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
+import { useTaskStatusClass } from '../node'
+import { getDefaultInclusiveConditionNodeName } from '../utils'
+import { generateUUID } from '@/utils'
+import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
+const { proxy } = getCurrentInstance() as any
+defineOptions({
+  name: 'InclusiveNode'
+})
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  }
+})
+// 定义事件,更新父组件
+const emits = defineEmits<{
+  'update:modelValue': [node: SimpleFlowNode | undefined]
+  'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number]
+  'find:recursiveFindParentNode': [
+    nodeList: SimpleFlowNode[],
+    curentNode: SimpleFlowNode,
+    nodeType: number
+  ]
+}>()
+// 是否只读
+const readonly = inject<Boolean>('readonly')
+
+const currentNode = ref<SimpleFlowNode>(props.flowNode)
+
+watch(
+  () => props.flowNode,
+  (newValue) => {
+    currentNode.value = newValue
+  }
+)
+
+const showInputs = ref<boolean[]>([])
+// 失去焦点
+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)
+}
+
+// 点击条件名称
+const clickEvent = (index: number) => {
+  showInputs.value[index] = true
+}
+
+const conditionNodeConfig = (nodeId: string) => {
+  if (readonly) {
+    return
+  }
+  const conditionNode = proxy.$refs[nodeId][0]
+  conditionNode.open()
+}
+
+// 新增条件
+const addCondition = () => {
+  const conditionNodes = currentNode.value.conditionNodes
+  if (conditionNodes) {
+    const len = conditionNodes.length
+    let lastIndex = len - 1
+    const conditionData: SimpleFlowNode = {
+      id: 'Flow_' + generateUUID(),
+      name: '包容条件' + len,
+      showText: '',
+      type: NodeType.CONDITION_NODE,
+      childNode: undefined,
+      conditionNodes: [],
+      conditionType: 1,
+      defaultFlow: false
+    }
+    conditionNodes.splice(lastIndex, 0, conditionData)
+  }
+}
+
+// 删除条件
+const deleteCondition = (index: number) => {
+  const conditionNodes = currentNode.value.conditionNodes
+  if (conditionNodes) {
+    conditionNodes.splice(index, 1)
+    if (conditionNodes.length == 1) {
+      const childNode = currentNode.value.childNode
+      // 更新此节点为后续孩子节点
+      emits('update:modelValue', childNode)
+    }
+  }
+}
+
+// 移动节点
+const moveNode = (index: number, to: number) => {
+  // -1 :向左  1: 向右
+  if (currentNode.value.conditionNodes) {
+    currentNode.value.conditionNodes[index] = currentNode.value.conditionNodes.splice(
+      index + to,
+      1,
+      currentNode.value.conditionNodes[index]
+    )[0]
+  }
+}
+// 递归从父节点中查询匹配的节点
+const recursiveFindParentNode = (
+  nodeList: SimpleFlowNode[],
+  node: SimpleFlowNode,
+  nodeType: number
+) => {
+  if (!node || node.type === NodeType.START_USER_NODE) {
+    return
+  }
+  if (node.type === nodeType) {
+    nodeList.push(node)
+  }
+  // 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点条件分支节点(NodeType.INCLUSIVE_BRANCH_NODE) 继续查找
+  emits('find:parentNode', nodeList, nodeType)
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 22 - 19
src/components/SimpleProcessDesignerV2/src/nodes/ParallelNode.vue

@@ -1,7 +1,16 @@
 <template>
   <div class="branch-node-wrapper">
     <div class="branch-node-container">
-      <div class="branch-node-add" @click="addCondition">添加分支</div>
+      <div
+        v-if="readonly"
+        class="branch-node-readonly"
+        :class="`${useTaskStatusClass(currentNode?.activityStatus)}`"
+      >
+        <span class="iconfont icon-parallel icon-size parallel"></span>
+      </div>
+      <el-button v-else class="branch-node-add" color="#626aef" @click="addCondition" plain
+        >添加分支</el-button
+      >
       <div
         class="branch-node-item"
         v-for="(item, index) in currentNode.conditionNodes"
@@ -17,7 +26,7 @@
         </template>
         <div class="node-wrapper">
           <div class="node-container">
-            <div class="node-box">
+            <div class="node-box" :class="`${useTaskStatusClass(item.activityStatus)}`">
               <div class="branch-node-title-container">
                 <div v-if="showInputs[index]">
                   <input
@@ -39,7 +48,7 @@
                   {{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
                 </div>
               </div>
-              <div class="node-toolbar">
+              <div v-if="!readonly" class="node-toolbar">
                 <div class="toolbar-icon">
                   <Icon
                     color="#0089ff"
@@ -49,20 +58,8 @@
                   />
                 </div>
               </div>
-              <!-- <div 
-                class="branch-node-move move-node-left"
-                v-if="index != 0 && index + 1 !== currentNode.conditionNodes?.length" @click="moveNode(index, -1)">
-                <Icon icon="ep:arrow-left" />
-              </div> -->
-
-              <!-- <div 
-                class="branch-node-move move-node-right"
-                v-if="currentNode.conditionNodes && index < currentNode.conditionNodes.length - 2"
-                @click="moveNode(index, 1)">
-                <Icon icon="ep:arrow-right" />
-              </div> -->
             </div>
-            <NodeHandler v-model:child-node="item.childNode" />
+            <NodeHandler v-model:child-node="item.childNode" :current-node="item" />
           </div>
         </div>
         <!-- 递归显示子节点  -->
@@ -74,7 +71,11 @@
         />
       </div>
     </div>
-    <NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
+    <NodeHandler
+      v-if="currentNode"
+      v-model:child-node="currentNode.childNode"
+      :current-node="currentNode"
+    />
   </div>
 </template>
 
@@ -82,8 +83,8 @@
 import NodeHandler from '../NodeHandler.vue'
 import ProcessNodeTree from '../ProcessNodeTree.vue'
 import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
+import { useTaskStatusClass } from '../node'
 import { generateUUID } from '@/utils'
-
 const { proxy } = getCurrentInstance() as any
 defineOptions({
   name: 'ParallelNode'
@@ -106,6 +107,8 @@ const emits = defineEmits<{
 }>()
 
 const currentNode = ref<SimpleFlowNode>(props.flowNode)
+// 是否只读
+const readonly = inject<Boolean>('readonly')
 
 watch(
   () => props.flowNode,
@@ -169,7 +172,7 @@ const recursiveFindParentNode = (
   node: SimpleFlowNode,
   nodeType: number
 ) => {
-  if (!node || node.type === NodeType.START_EVENT_NODE) {
+  if (!node || node.type === NodeType.START_USER_NODE) {
     return
   }
   if (node.type === nodeType) {

+ 96 - 11
src/components/SimpleProcessDesignerV2/src/nodes/StartUserNode.vue

@@ -1,7 +1,13 @@
 <template>
   <div class="node-wrapper">
     <div class="node-container">
-      <div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
+      <div
+        class="node-box"
+        :class="[
+          { 'node-config-error': !currentNode.showText },
+          `${useTaskStatusClass(currentNode?.activityStatus)}`
+        ]"
+      >
         <div class="node-title-container">
           <div class="node-title-icon start-user"
             ><span class="iconfont icon-start-user"></span
@@ -19,27 +25,88 @@
             {{ currentNode.name }}
           </div>
         </div>
-        <div class="node-content" @click="openNodeConfig">
+        <div class="node-content" @click="nodeClick">
           <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.START_USER_NODE) }}
           </div>
-          <Icon icon="ep:arrow-right-bold" />
+          <Icon icon="ep:arrow-right-bold" v-if="!readonly" />
         </div>
       </div>
       <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
-      <NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
+      <NodeHandler
+        v-if="currentNode"
+        v-model:child-node="currentNode.childNode"
+        :current-node="currentNode"
+      />
     </div>
   </div>
-  <StartUserNodeConfig v-if="currentNode" ref="nodeSetting" :flow-node="currentNode" />
+  <StartUserNodeConfig v-if="!readonly && currentNode" ref="nodeSetting" :flow-node="currentNode" />
+  <!-- 审批记录 -->
+  <el-dialog
+    :title="dialogTitle || '审批记录'"
+    v-model="dialogVisible"
+    width="1000px"
+    append-to-body
+  >
+    <el-row>
+      <el-table :data="selectTasks" size="small" border header-cell-class-name="table-header-gray">
+        <el-table-column
+          label="序号"
+          header-align="center"
+          align="center"
+          type="index"
+          width="50"
+        />
+        <el-table-column label="审批人" min-width="100" align="center">
+          <template #default="scope">
+            {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
+          </template>
+        </el-table-column>
+
+        <el-table-column label="部门" min-width="100" align="center">
+          <template #default="scope">
+            {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          :formatter="dateFormatter"
+          align="center"
+          label="开始时间"
+          prop="createTime"
+          min-width="140"
+        />
+        <el-table-column
+          :formatter="dateFormatter"
+          align="center"
+          label="结束时间"
+          prop="endTime"
+          min-width="140"
+        />
+        <el-table-column align="center" label="审批状态" prop="status" min-width="90">
+          <template #default="scope">
+            <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
+          </template>
+        </el-table-column>
+        <el-table-column align="center" label="审批建议" prop="reason" min-width="120" />
+        <el-table-column align="center" label="耗时" prop="durationInMillis" width="100">
+          <template #default="scope">
+            {{ formatPast2(scope.row.durationInMillis) }}
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-row>
+  </el-dialog>
 </template>
 <script setup lang="ts">
 import NodeHandler from '../NodeHandler.vue'
-import { useWatchNode, useNodeName2 } from '../node'
+import { useWatchNode, useNodeName2, useTaskStatusClass } from '../node'
 import { SimpleFlowNode, NODE_DEFAULT_TEXT, NodeType } from '../consts'
 import StartUserNodeConfig from '../nodes-config/StartUserNodeConfig.vue'
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
+import { DICT_TYPE } from '@/utils/dict'
 defineOptions({
   name: 'StartEventNode'
 })
@@ -49,6 +116,8 @@ const props = defineProps({
     default: () => null
   }
 })
+const readonly = inject<Boolean>('readonly') // 是否只读
+const tasks = inject<Ref<any[]>>('tasks')
 // 定义事件,更新父组件。
 const emits = defineEmits<{
   'update:modelValue': [node: SimpleFlowNode | undefined]
@@ -59,11 +128,27 @@ const currentNode = useWatchNode(props)
 const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
 
 const nodeSetting = ref()
-// 打开节点配置
-const openNodeConfig = () => {
-  // 把当前节点传递给配置组件
-  nodeSetting.value.showStartUserNodeConfig(currentNode.value)
-  nodeSetting.value.openDrawer()
+//
+const nodeClick = () => {
+  if (readonly) {
+    // 只读模式,弹窗显示任务信息
+    if (tasks && tasks.value) {
+      dialogTitle.value = currentNode.value.name
+      selectTasks.value = tasks.value.filter(
+        (item: any) => item?.taskDefinitionKey === currentNode.value.id
+      )
+      dialogVisible.value = true
+    }
+  } else {
+    // 编辑模式,打开节点配置、把当前节点传递给配置组件
+    nodeSetting.value.showStartUserNodeConfig(currentNode.value)
+    nodeSetting.value.openDrawer()
+  }
 }
+
+// 任务的弹窗显示,用于只读模式
+const dialogVisible = ref(false) // 弹窗可见性
+const dialogTitle = ref<string | undefined>(undefined) // 弹窗标题
+const selectTasks = ref<any[] | undefined>([]) // 选中的任务数组
 </script>
 <style lang="scss" scoped></style>

+ 99 - 13
src/components/SimpleProcessDesignerV2/src/nodes/UserTaskNode.vue

@@ -1,11 +1,17 @@
 <template>
   <div class="node-wrapper">
     <div class="node-container">
-      <div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
+      <div
+        class="node-box"
+        :class="[
+          { 'node-config-error': !currentNode.showText },
+          `${useTaskStatusClass(currentNode?.activityStatus)}`
+        ]"
+      >
         <div class="node-title-container">
           <div class="node-title-icon user-task"><span class="iconfont icon-approve"></span></div>
           <input
-            v-if="showInput"
+            v-if="!readonly && showInput"
             type="text"
             class="editable-title-input"
             @blur="blurEvent()"
@@ -17,23 +23,27 @@
             {{ currentNode.name }}
           </div>
         </div>
-        <div class="node-content" @click="openNodeConfig">
+        <div class="node-content" @click="nodeClick">
           <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.USER_TASK_NODE) }}
           </div>
-          <Icon icon="ep:arrow-right-bold" />
+          <Icon icon="ep:arrow-right-bold" v-if="!readonly" />
         </div>
-        <div class="node-toolbar">
+        <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" />
+      <NodeHandler
+        v-if="currentNode"
+        v-model:child-node="currentNode.childNode"
+        :current-node="currentNode"
+      />
     </div>
   </div>
   <UserTaskNodeConfig
@@ -42,12 +52,69 @@
     :flow-node="currentNode"
     @find:return-task-nodes="findReturnTaskNodes"
   />
+  <!-- 审批记录 -->
+  <el-dialog
+    :title="dialogTitle || '审批记录'"
+    v-model="dialogVisible"
+    width="1000px"
+    append-to-body
+  >
+    <el-row>
+      <el-table :data="selectTasks" size="small" border header-cell-class-name="table-header-gray">
+        <el-table-column
+          label="序号"
+          header-align="center"
+          align="center"
+          type="index"
+          width="50"
+        />
+        <el-table-column label="审批人" min-width="100" align="center">
+          <template #default="scope">
+            {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
+          </template>
+        </el-table-column>
+
+        <el-table-column label="部门" min-width="100" align="center">
+          <template #default="scope">
+            {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          :formatter="dateFormatter"
+          align="center"
+          label="开始时间"
+          prop="createTime"
+          min-width="140"
+        />
+        <el-table-column
+          :formatter="dateFormatter"
+          align="center"
+          label="结束时间"
+          prop="endTime"
+          min-width="140"
+        />
+        <el-table-column align="center" label="审批状态" prop="status" min-width="90">
+          <template #default="scope">
+            <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
+          </template>
+        </el-table-column>
+        <el-table-column align="center" label="审批建议" prop="reason" min-width="120" />
+        <el-table-column align="center" label="耗时" prop="durationInMillis" width="100">
+          <template #default="scope">
+            {{ formatPast2(scope.row.durationInMillis) }}
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-row>
+  </el-dialog>
 </template>
 <script setup lang="ts">
 import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
-import { useWatchNode, useNodeName2 } from '../node'
+import { useWatchNode, useNodeName2, useTaskStatusClass } from '../node'
 import NodeHandler from '../NodeHandler.vue'
 import UserTaskNodeConfig from '../nodes-config/UserTaskNodeConfig.vue'
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
+import { DICT_TYPE } from '@/utils/dict'
 defineOptions({
   name: 'UserTaskNode'
 })
@@ -61,22 +128,36 @@ const emits = defineEmits<{
   'update:flowNode': [node: SimpleFlowNode | undefined]
   'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: NodeType]
 }>()
+
+// 是否只读
+const readonly = inject<Boolean>('readonly')
+const tasks = inject<Ref<any[]>>('tasks')
 // 监控节点变化
 const currentNode = useWatchNode(props)
 // 节点名称编辑
 const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
 const nodeSetting = ref()
-// 打开节点配置
-const openNodeConfig = () => {
-  // 把当前节点传递给配置组件
-  nodeSetting.value.showUserTaskNodeConfig(currentNode.value)
-  nodeSetting.value.openDrawer()
+
+const nodeClick = () => {
+  if (readonly) {
+    if (tasks && tasks.value) {
+      dialogTitle.value = currentNode.value.name
+      // 只读模式,弹窗显示任务信息
+      selectTasks.value = tasks.value.filter(
+        (item: any) => item?.taskDefinitionKey === currentNode.value.id
+      )
+      dialogVisible.value = true
+    }
+  } else {
+    // 编辑模式,打开节点配置、把当前节点传递给配置组件
+    nodeSetting.value.showUserTaskNodeConfig(currentNode.value)
+    nodeSetting.value.openDrawer()
+  }
 }
 
 const deleteNode = () => {
   emits('update:flowNode', currentNode.value.childNode)
 }
-
 // 查找可以驳回用户节点
 const findReturnTaskNodes = (
   matchNodeList: SimpleFlowNode[] // 匹配的节点
@@ -84,5 +165,10 @@ const findReturnTaskNodes = (
   // 从父节点查找
   emits('find:parentNode', matchNodeList, NodeType.USER_TASK_NODE)
 }
+
+// 任务的弹窗显示,用于只读模式
+const dialogVisible = ref(false) // 弹窗可见性
+const dialogTitle = ref<string | undefined>(undefined) // 弹窗标题
+const selectTasks = ref<any[] | undefined>([]) // 选中的任务数组
 </script>
 <style lang="scss" scoped></style>

+ 8 - 0
src/components/SimpleProcessDesignerV2/src/utils.ts

@@ -8,6 +8,14 @@ export const getDefaultConditionNodeName = (index: number, defaultFlow: boolean
   return '条件' + (index + 1)
 }
 
+// 获取包容分支条件节点默认的名称
+export const getDefaultInclusiveConditionNodeName = (index: number, defaultFlow: boolean | undefined): string => {
+  if (defaultFlow) {
+    return '其它情况'
+  }
+  return '包容条件' + (index + 1)
+}
+
 export const convertTimeUnit = (strTimeUnit: string) => {
   if (strTimeUnit === 'M') {
     return TimeUnitType.MINUTE

+ 552 - 516
src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss

@@ -1,512 +1,3 @@
-.simple-flow-canvas {
-  position: absolute;
-  inset: 0;
-  z-index: 1;
-  overflow: auto;
-  background-color: #fafafa;
-  user-select: none;
-
-  .simple-flow-container {
-    position: relative;
-    display: flex;
-    flex-direction: column;
-    justify-content: center;
-    align-items: center;
-
-    .top-area-container {
-      position: sticky;
-      inset: 0;
-      display: flex;
-      width: 100%;
-      height: 42px;
-      z-index: 1;
-      // padding: 4px 0;
-      background-color: #fff;
-      justify-content: flex-end;
-      align-items: center;
-
-      .top-actions {
-        display: flex;
-        margin: 4px;
-        margin-right: 8px;
-        align-items: center;
-
-        .canvas-control {
-          font-size: 16px;
-
-          .control-scale-group {
-            display: inline-flex;
-            align-items: center;
-            margin-right: 8px;
-
-            .control-scale-button {
-              display: inline-flex;
-              width: 28px;
-              height: 28px;
-              padding: 2px;
-              text-align: center;
-              cursor: pointer;
-              justify-content: center;
-              align-items: center;
-            }
-
-            .control-scale-label {
-              margin: 0 4px;
-              font-size: 14px;
-            }
-          }
-        }
-      }
-    }
-
-    .scale-container {
-      display: flex;
-      flex-direction: column;
-      justify-content: center;
-      align-items: center;
-      margin-top: 16px;
-      background-color: #fafafa;
-      transform-origin: 50% 0 0;
-      transform: scale(1);
-      transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
-
-      // 节点容器 定义节点宽度
-      .node-container {
-        width: 200px;
-      }
-      // 节点
-      .node-box {
-        position: relative;
-        display: flex;
-        min-height: 70px;
-        padding: 5px 10px 8px;
-        cursor: pointer;
-        background-color: #fff;
-        flex-direction: column;
-        border: 2px solid transparent;
-        // border-color: #0089ff;
-        border-radius: 8px;
-        // border-color: #0089ff;
-        box-shadow: 0 1px 4px 0 rgba(10, 30, 65, 0.16);
-        transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
-
-        &:hover {
-          border-color: #0089ff;
-          .node-toolbar {
-            opacity: 1;
-          }
-
-          .branch-node-move {
-            display: flex;
-          }
-        }
-
-        // 普通节点标题
-        .node-title-container {
-          display: flex;
-          padding: 4px;
-          cursor: pointer;
-          border-radius: 4px 4px 0 0;
-          align-items: center;
-
-          .node-title-icon {
-            display: flex;
-            align-items: center;
-
-            &.user-task {
-              color: #ff943e;
-            }
-            &.copy-task {
-              color: #3296fa;
-            }
-            &.start-user {
-              color: #676565;
-            }
-          }
-
-          .node-title {
-            margin-left: 4px;
-            font-size: 14px;
-            font-weight: 600;
-            white-space: nowrap;
-            overflow: hidden;
-            text-overflow: ellipsis;
-            color: #1f1f1f;
-            line-height: 18px;
-            &:hover {
-              border-bottom: 1px dashed #f60;
-            }
-          }
-        }
-
-        // 条件节点标题
-        .branch-node-title-container {
-          display: flex;
-          padding: 4px 0;
-          cursor: pointer;
-          border-radius: 4px 4px 0 0;
-          align-items: center;
-          justify-content: space-between;
-
-          .input-max-width {
-            max-width: 115px !important;
-          }
-
-          .branch-title {
-            font-size: 13px;
-            font-weight: 600;
-            white-space: nowrap;
-            overflow: hidden;
-            text-overflow: ellipsis;
-            color: #f60;
-            &:hover {
-              border-bottom: 1px dashed #000;
-            }
-          }
-
-          .branch-priority {
-            min-width: 50px;
-            font-size: 13px;
-          }
-        }
-
-        .node-content {
-          display: flex;
-          min-height: 32px;
-          padding: 4px 8px;
-          margin-top: 4px;
-          line-height: 32px;
-          justify-content: space-between;
-          align-items: center;
-          color: #111f2c;
-          background: rgba(0, 0, 0, 0.03);
-          border-radius: 4px;
-
-          .node-text {
-            display: -webkit-box;
-            overflow: hidden;
-            font-size: 14px;
-            line-height: 24px;
-            text-overflow: ellipsis;
-            word-break: break-all;
-            -webkit-line-clamp: 2; /* 这将限制文本显示为两行 */
-            -webkit-box-orient: vertical;
-          }
-        }
-
-        //条件节点内容
-        .branch-node-content {
-          display: flex;
-          min-height: 32px;
-          padding: 4px 8px;
-          margin-top: 4px;
-          line-height: 32px;
-          align-items: center;
-          color: #111f2c;
-          border-radius: 4px;
-
-          .branch-node-text {
-            overflow: hidden;
-            font-size: 14px;
-            line-height: 24px;
-            text-overflow: ellipsis;
-            word-break: break-all;
-            -webkit-line-clamp: 2; /* 这将限制文本显示为两行 */
-            -webkit-box-orient: vertical;
-          }
-        }
-
-        // 节点操作 :删除
-        .node-toolbar {
-          opacity: 0;
-          position: absolute;
-          top: -20px;
-          right: 0px;
-          display: flex;
-
-          .toolbar-icon {
-            text-align: center;
-            vertical-align: middle;
-          }
-        }
-
-        // 条件节点左右移动
-        .branch-node-move {
-          position: absolute;
-          width: 10px;
-          cursor: pointer;
-          display: none;
-          align-items: center;
-          height: 100%;
-          justify-content: center;
-        }
-
-        .move-node-left {
-          left: -2px;
-          top: 0px;
-          background: rgba(126, 134, 142, 0.08);
-          border-top-left-radius: 8px;
-          border-bottom-left-radius: 8px;
-        }
-
-        .move-node-right {
-          right: -2px;
-          top: 0px;
-          background: rgba(126, 134, 142, 0.08);
-          border-top-right-radius: 6px;
-          border-bottom-right-radius: 6px;
-        }
-      }
-
-      .node-config-error {
-        border-color: #ff5219 !important;
-      }
-      // 普通节点包装
-      .node-wrapper {
-        display: flex;
-        flex-direction: column;
-        justify-content: center;
-        align-items: center;
-      }
-      // 节点连线处理
-      .node-handler-wrapper {
-        position: relative;
-        display: flex;
-        height: 70px;
-        align-items: center;
-        user-select: none;
-        justify-content: center;
-        flex-direction: column;
-
-        &::before {
-          position: absolute;
-          top: 0;
-          right: 0;
-          left: 0;
-          // bottom: 5px;
-          bottom: 0px;
-          z-index: 0;
-          width: 2px;
-          height: 100%;
-          // height: calc(100% - 5px);
-          margin: auto;
-          background-color: #dedede;
-          content: '';
-        }
-
-        .node-handler {
-          .add-icon {
-            position: relative;
-            top: -5px;
-            display: flex;
-            align-items: center;
-            justify-content: center;
-            cursor: pointer;
-            width: 25px;
-            height: 25px;
-            color: #fff;
-            background-color: #0089ff;
-            border-radius: 50%;
-
-            &:hover {
-              transform: scale(1.1);
-            }
-          }
-        }
-
-        .node-handler-arrow {
-          position: absolute;
-          bottom: 0;
-          left: 50%;
-          display: flex;
-          transform: translateX(-50%);
-        }
-      }
-
-      // 条件节点包装
-      .branch-node-wrapper {
-        position: relative;
-        display: flex;
-        flex-direction: column;
-        justify-content: center;
-        align-items: center;
-        margin-top: 16px;
-
-        .branch-node-container {
-          position: relative;
-          display: flex;
-
-          &::before {
-            position: absolute;
-            height: 100%;
-            width: 4px;
-            background-color: #fafafa;
-            content: '';
-            left: 50%;
-            transform: translate(-50%);
-          }
-
-          .branch-node-add {
-            position: absolute;
-            top: -18px;
-            left: 50%;
-            z-index: 1;
-            height: 36px;
-            padding: 0 10px;
-            font-size: 12px;
-            line-height: 36px;
-            color: #222;
-            cursor: pointer;
-            background: #fff;
-            border: 2px solid #dedede;
-            border-radius: 18px;
-            transform: translateX(-50%);
-            transform-origin: center center;
-            transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
-          }
-
-          .branch-node-item {
-            position: relative;
-            display: flex;
-            flex-direction: column;
-            align-items: center;
-            min-width: 280px;
-            padding: 40px 40px 0;
-            background: transparent;
-            border-top: 2px solid #dedede;
-            border-bottom: 2px solid #dedede;
-
-            &::before {
-              position: absolute;
-              width: 2px;
-              height: 100%;
-              margin: auto;
-              inset: 0;
-              background-color: #dedede;
-              content: '';
-            }
-          }
-          // 覆盖条件节点第一个节点左上角的线
-          .branch-line-first-top {
-            position: absolute;
-            top: -5px;
-            left: -1px;
-            width: 50%;
-            height: 7px;
-            background-color: #fafafa;
-            content: '';
-          }
-          // 覆盖条件节点第一个节点左下角的线
-          .branch-line-first-bottom {
-            position: absolute;
-            bottom: -5px;
-            left: -1px;
-            width: 50%;
-            height: 7px;
-            background-color: #fafafa;
-            content: '';
-          }
-          // 覆盖条件节点最后一个节点右上角的线
-          .branch-line-last-top {
-            position: absolute;
-            top: -5px;
-            right: -1px;
-            width: 50%;
-            height: 7px;
-            background-color: #fafafa;
-            content: '';
-          }
-          // 覆盖条件节点最后一个节点右下角的线
-          .branch-line-last-bottom {
-            position: absolute;
-            right: -1px;
-            bottom: -5px;
-            width: 50%;
-            height: 7px;
-            background-color: #fafafa;
-            content: '';
-          }
-        }
-      }
-
-      .node-fixed-name {
-        display: inline-block;
-        width: auto;
-        padding: 0 4px;
-        overflow: hidden;
-        text-align: center;
-        text-overflow: ellipsis;
-        white-space: nowrap;
-      }
-      // 开始节点包装
-      .start-node-wrapper {
-        position: relative;
-        margin-top: 16px;
-
-        .start-node-container {
-          display: flex;
-          flex-direction: column;
-          justify-content: center;
-          align-items: center;
-
-          .start-node-box {
-            display: flex;
-            justify-content: center;
-            align-items: center;
-            width: 90px;
-            height: 36px;
-            padding: 3px 4px;
-            color: #212121;
-            cursor: pointer;
-            // background: #2c2c2c;
-            background: #fafafa;
-            border-radius: 30px;
-            box-shadow: 0 1px 5px 0 rgba(10, 30, 65, 0.08);
-            box-sizing: border-box;
-          }
-        }
-      }
-
-      // 结束节点包装
-      .end-node-wrapper {
-        margin-bottom: 16px;
-
-        .end-node-box {
-          display: flex;
-          justify-content: center;
-          align-items: center;
-          width: 80px;
-          height: 36px;
-          color: #212121;
-          // background: #6e6e6e;
-          background: #fafafa;
-          border-radius: 30px;
-          box-shadow: 0 1px 5px 0 rgba(10, 30, 65, 0.08);
-          box-sizing: border-box;
-        }
-      }
-
-      // 可编辑的 title 输入框
-      .editable-title-input {
-        height: 20px;
-        max-width: 145px;
-        line-height: 20px;
-        font-size: 12px;
-        margin-left: 4px;
-        border: 1px solid #d9d9d9;
-        border-radius: 4px;
-        transition: all 0.3s;
-
-        &:focus {
-          border-color: #40a9ff;
-          outline: 0;
-          box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
-        }
-      }
-    }
-  }
-}
-
 // 配置节点头部
 .config-header {
   display: flex;
@@ -626,16 +117,17 @@
   cursor: pointer;
 
   .handler-item {
-    margin-right: 8px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
   }
 
   .handler-item-icon {
-    width: 80px;
-    height: 80px;
+    width: 60px;
+    height: 60px;
     background: #fff;
     border: 1px solid #e2e2e2;
     border-radius: 50%;
-    transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
     user-select: none;
     text-align: center;
 
@@ -645,8 +137,8 @@
     }
 
     .icon-size {
-      font-size: 35px;
-      line-height: 80px;
+      font-size: 25px;
+      line-height: 60px;
     }
   }
 
@@ -658,13 +150,557 @@
   }
 
   .condition {
-    color: #15bc83;
+    color: #67c23a;
+  }
+
+  .parallel {
+    color: #626aef;
+  }
+
+  .inclusive {
+    color: #345da2;
   }
 
   .handler-item-text {
     margin-top: 4px;
     width: 80px;
     text-align: center;
+    font-size: 13px;
+  }
+}
+// Simple 流程模型样式
+.simple-process-model-container {
+  height: 100%;
+  padding-top: 32px;
+  background-color: #fafafa;
+  .simple-process-model {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    transform-origin: 50% 0 0;
+    overflow: auto;
+    transform: scale(1);
+    transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+    background: url(@/assets/svgs/bpm/simple-process-bg.svg) 0 0 repeat;
+    // 节点容器 定义节点宽度
+    .node-container {
+      width: 200px;
+    }
+    // 节点
+    .node-box {
+      position: relative;
+      display: flex;
+      min-height: 70px;
+      padding: 5px 10px 8px;
+      cursor: pointer;
+      background-color: #fff;
+      flex-direction: column;
+      border: 2px solid transparent;
+      border-radius: 8px;
+      box-shadow: 0 1px 4px 0 rgb(10 30 65 / 16%);
+      transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
+
+      &.status-pass {
+        background-color: #a9da90;
+        border-color: #67c23a;
+      }
+
+      &.status-pass:hover {
+        border-color: #67c23a;
+      }
+
+      &.status-running {
+        background-color: #e7f0fe;
+        border-color: #5a9cf8;
+      }
+
+      &.status-running:hover {
+        border-color: #5a9cf8;
+      }
+
+      &.status-reject {
+        background-color: #f6e5e5;
+        border-color: #e47470;
+      }
+
+      &.status-reject:hover {
+        border-color: #e47470;
+      }
+
+      &:hover {
+        border-color: #0089ff;
+
+        .node-toolbar {
+          opacity: 1;
+        }
+
+        .branch-node-move {
+          display: flex;
+        }
+      }
+
+      // 普通节点标题
+      .node-title-container {
+        display: flex;
+        padding: 4px;
+        cursor: pointer;
+        border-radius: 4px 4px 0 0;
+        align-items: center;
+
+        .node-title-icon {
+          display: flex;
+          align-items: center;
+
+          &.user-task {
+            color: #ff943e;
+          }
+
+          &.copy-task {
+            color: #3296fa;
+          }
+
+          &.start-user {
+            color: #676565;
+          }
+        }
+
+        .node-title {
+          margin-left: 4px;
+          overflow: hidden;
+          font-size: 14px;
+          font-weight: 600;
+          line-height: 18px;
+          color: #1f1f1f;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+
+          &:hover {
+            border-bottom: 1px dashed #f60;
+          }
+        }
+      }
+
+      // 条件节点标题
+      .branch-node-title-container {
+        display: flex;
+        padding: 4px 0;
+        cursor: pointer;
+        border-radius: 4px 4px 0 0;
+        align-items: center;
+        justify-content: space-between;
+
+        .input-max-width {
+          max-width: 115px !important;
+        }
+
+        .branch-title {
+          overflow: hidden;
+          font-size: 13px;
+          font-weight: 600;
+          color: #f60;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+
+          &:hover {
+            border-bottom: 1px dashed #000;
+          }
+        }
+
+        .branch-priority {
+          min-width: 50px;
+          font-size: 12px;
+        }
+      }
+
+      .node-content {
+        display: flex;
+        min-height: 32px;
+        padding: 4px 8px;
+        margin-top: 4px;
+        line-height: 32px;
+        justify-content: space-between;
+        align-items: center;
+        color: #111f2c;
+        background: rgb(0 0 0 / 3%);
+        border-radius: 4px;
+
+        .node-text {
+          display: -webkit-box;
+          overflow: hidden;
+          font-size: 14px;
+          line-height: 24px;
+          text-overflow: ellipsis;
+          word-break: break-all;
+          -webkit-line-clamp: 2; /* 这将限制文本显示为两行 */
+          -webkit-box-orient: vertical;
+        }
+      }
+
+      //条件节点内容
+      .branch-node-content {
+        display: flex;
+        min-height: 32px;
+        padding: 4px 0;
+        margin-top: 4px;
+        line-height: 32px;
+        align-items: center;
+        color: #111f2c;
+        border-radius: 4px;
+
+        .branch-node-text {
+          overflow: hidden;
+          font-size: 12px;
+          line-height: 24px;
+          text-overflow: ellipsis;
+          word-break: break-all;
+          -webkit-line-clamp: 2; /* 这将限制文本显示为两行 */
+          -webkit-box-orient: vertical;
+        }
+      }
+
+      // 节点操作 :删除
+      .node-toolbar {
+        position: absolute;
+        top: -20px;
+        right: 0;
+        display: flex;
+        opacity: 0;
+
+        .toolbar-icon {
+          text-align: center;
+          vertical-align: middle;
+        }
+      }
+
+      // 条件节点左右移动
+      .branch-node-move {
+        position: absolute;
+        display: none;
+        width: 10px;
+        height: 100%;
+        cursor: pointer;
+        align-items: center;
+        justify-content: center;
+      }
+
+      .move-node-left {
+        top: 0;
+        left: -2px;
+        background: rgb(126 134 142 / 8%);
+        border-bottom-left-radius: 8px;
+        border-top-left-radius: 8px;
+      }
+
+      .move-node-right {
+        top: 0;
+        right: -2px;
+        background: rgb(126 134 142 / 8%);
+        border-top-right-radius: 6px;
+        border-bottom-right-radius: 6px;
+      }
+    }
+
+    .node-config-error {
+      border-color: #ff5219 !important;
+    }
+    // 普通节点包装
+    .node-wrapper {
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+    }
+    // 节点连线处理
+    .node-handler-wrapper {
+      position: relative;
+      display: flex;
+      height: 70px;
+      align-items: center;
+      user-select: none;
+      justify-content: center;
+      flex-direction: column;
+
+      &::before {
+        position: absolute;
+        top: 0;
+        z-index: 0;
+        width: 2px;
+        height: 100%;
+        margin: auto;
+        background-color: #dedede;
+        content: '';
+      }
+
+      .node-handler {
+        .add-icon {
+          position: relative;
+          top: -5px;
+          display: flex;
+          width: 25px;
+          height: 25px;
+          color: #fff;
+          cursor: pointer;
+          background-color: #0089ff;
+          border-radius: 50%;
+          align-items: center;
+          justify-content: center;
+
+          &:hover {
+            transform: scale(1.1);
+          }
+        }
+      }
+
+      .node-handler-arrow {
+        position: absolute;
+        bottom: 0;
+        left: 50%;
+        display: flex;
+        transform: translateX(-50%);
+      }
+    }
+
+    // 条件节点包装
+    .branch-node-wrapper {
+      position: relative;
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      margin-top: 16px;
+
+      .branch-node-container {
+        position: relative;
+        display: flex;
+
+        &::before {
+          position: absolute;
+          left: 50%;
+          width: 4px;
+          height: 100%;
+          background-color: #fafafa;
+          content: '';
+          transform: translate(-50%);
+        }
+
+        .branch-node-add {
+          position: absolute;
+          top: -18px;
+          left: 50%;
+          z-index: 1;
+          height: 36px;
+          padding: 0 10px;
+          font-size: 12px;
+          line-height: 36px;
+          border: 2px solid #dedede;
+          border-radius: 18px;
+          transform: translateX(-50%);
+          transform-origin: center center;
+        }
+
+        .branch-node-readonly {
+          position: absolute;
+          top: -18px;
+          left: 50%;
+          z-index: 1;
+          display: flex;
+          width: 36px;
+          height: 36px;
+          background-color: #fff;
+          border: 2px solid #dedede;
+          border-radius: 50%;
+          transform: translateX(-50%);
+          align-items: center;
+          justify-content: center;
+          transform-origin: center center;
+
+          &.status-pass {
+            background-color: #e9f4e2;
+            border-color: #6bb63c;
+          }
+
+          &.status-pass:hover {
+            border-color: #6bb63c;
+          }
+
+          .icon-size {
+            font-size: 22px;
+            &.condition {
+              color: #67c23a;
+            }
+            &.parallel {
+              color: #626aef;
+            }
+            &.inclusive {
+              color: #345da2;
+            }
+          }
+        }
+
+        .branch-node-item {
+          position: relative;
+          display: flex;
+          flex-direction: column;
+          align-items: center;
+          min-width: 280px;
+          padding: 40px 40px 0;
+          background: transparent;
+          border-top: 2px solid #dedede;
+          border-bottom: 2px solid #dedede;
+
+          &::before {
+            position: absolute;
+            width: 2px;
+            height: 100%;
+            margin: auto;
+            inset: 0;
+            background-color: #dedede;
+            content: '';
+          }
+        }
+        // 覆盖条件节点第一个节点左上角的线
+        .branch-line-first-top {
+          position: absolute;
+          top: -5px;
+          left: -1px;
+          width: 50%;
+          height: 7px;
+          background-color: #fafafa;
+          content: '';
+        }
+        // 覆盖条件节点第一个节点左下角的线
+        .branch-line-first-bottom {
+          position: absolute;
+          bottom: -5px;
+          left: -1px;
+          width: 50%;
+          height: 7px;
+          background-color: #fafafa;
+          content: '';
+        }
+        // 覆盖条件节点最后一个节点右上角的线
+        .branch-line-last-top {
+          position: absolute;
+          top: -5px;
+          right: -1px;
+          width: 50%;
+          height: 7px;
+          background-color: #fafafa;
+          content: '';
+        }
+        // 覆盖条件节点最后一个节点右下角的线
+        .branch-line-last-bottom {
+          position: absolute;
+          right: -1px;
+          bottom: -5px;
+          width: 50%;
+          height: 7px;
+          background-color: #fafafa;
+          content: '';
+        }
+      }
+    }
+
+    .node-fixed-name {
+      display: inline-block;
+      width: auto;
+      padding: 0 4px;
+      overflow: hidden;
+      text-align: center;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+    // 开始节点包装
+    .start-node-wrapper {
+      position: relative;
+      margin-top: 16px;
+
+      .start-node-container {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        align-items: center;
+
+        .start-node-box {
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          width: 90px;
+          height: 36px;
+          padding: 3px 4px;
+          color: #212121;
+          cursor: pointer;
+          background: #fafafa;
+          border-radius: 30px;
+          box-shadow: 0 1px 5px 0 rgb(10 30 65 / 8%);
+          box-sizing: border-box;
+        }
+      }
+    }
+
+    // 结束节点包装
+    .end-node-wrapper {
+      margin-bottom: 16px;
+
+      .end-node-box {
+        display: flex;
+        width: 80px;
+        height: 36px;
+        color: #212121;
+        border: 2px solid #fafafa;
+        border-radius: 30px;
+        box-shadow: 0 1px 5px 0 rgb(10 30 65 / 8%);
+        box-sizing: border-box;
+        justify-content: center;
+        align-items: center;
+
+        &.status-pass {
+          background-color: #a9da90;
+          border-color: #6bb63c;
+        }
+
+        &.status-pass:hover {
+          border-color: #6bb63c;
+        }
+
+        &.status-reject {
+          background-color: #f6e5e5;
+          border-color: #e47470;
+        }
+
+        &.status-reject:hover {
+          border-color: #e47470;
+        }
+
+        &.status-cancel {
+          background-color: #eaeaeb;
+          border-color: #919398;
+        }
+
+        &.status-cancel:hover {
+          border-color: #919398;
+        }
+      }
+    }
+
+    // 可编辑的 title 输入框
+    .editable-title-input {
+      height: 20px;
+      max-width: 145px;
+      margin-left: 4px;
+      font-size: 12px;
+      line-height: 20px;
+      border: 1px solid #d9d9d9;
+      border-radius: 4px;
+      transition: all 0.3s;
+
+      &:focus {
+        border-color: #40a9ff;
+        outline: 0;
+        box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
+      }
+    }
   }
 }
 

+ 152 - 0
src/components/UserSelectForm/index.vue

@@ -0,0 +1,152 @@
+<template>
+  <Dialog v-model="dialogVisible" title="人员选择" width="800">
+    <el-row class="gap2" v-loading="formLoading">
+      <el-col :span="6">
+        <ContentWrap class="h-1/1">
+          <el-tree
+            ref="treeRef"
+            :data="deptTree"
+            :expand-on-click-node="false"
+            :props="defaultProps"
+            default-expand-all
+            highlight-current
+            node-key="id"
+            @node-click="handleNodeClick"
+          />
+        </ContentWrap>
+      </el-col>
+      <el-col :span="17">
+        <el-transfer
+          v-model="selectedUserIdList"
+          :titles="['未选', '已选']"
+          filterable
+          filter-placeholder="搜索成员"
+          :data="transferUserList"
+          :props="{ label: 'nickname', key: 'id' }"
+        />
+      </el-col>
+    </el-row>
+    <template #footer>
+      <el-button
+        :disabled="formLoading || !selectedUserIdList?.length"
+        type="primary"
+        @click="submitForm"
+      >
+        确 定
+      </el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import { defaultProps, findTreeNode, handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+import * as UserApi from '@/api/system/user'
+
+defineOptions({ name: 'UserSelectForm' })
+const emit = defineEmits<{
+  confirm: [id: any, userList: any[]]
+}>()
+const { t } = useI18n() // 国际
+const message = useMessage() // 消息弹窗
+const deptTree = ref<Tree[]>([]) // 部门树形结构化
+const userList = ref<UserApi.UserVO[]>([]) // 所有用户列表
+const filteredUserList = ref<UserApi.UserVO[]>([]) // 当前部门过滤后的用户列表
+const selectedUserIdList: any = ref([]) // 选中的用户列表
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中
+const activityId = ref()
+
+/** 计算属性:合并已选择的用户和当前部门过滤后的用户 */
+const transferUserList = computed(() => {
+  // 1.1 获取所有已选择的用户
+  const selectedUsers = userList.value.filter((user: any) =>
+    selectedUserIdList.value.includes(user.id)
+  )
+
+  // 1.2 获取当前部门过滤后的未选择用户
+  const filteredUnselectedUsers = filteredUserList.value.filter(
+    (user: any) => !selectedUserIdList.value.includes(user.id)
+  )
+
+  // 2. 合并并去重
+  return [...selectedUsers, ...filteredUnselectedUsers]
+})
+
+/** 打开弹窗 */
+const open = async (id: number, selectedList?: any[]) => {
+  activityId.value = id
+  resetForm()
+
+  // 加载部门、用户列表
+  deptTree.value = handleTree(await DeptApi.getSimpleDeptList())
+  userList.value = await UserApi.getSimpleUserList()
+
+  // 初始状态下,过滤列表等于所有用户列表
+  filteredUserList.value = [...userList.value]
+  selectedUserIdList.value = selectedList?.map((item: any) => item.id) || []
+  dialogVisible.value = true
+}
+
+/** 获取部门过滤后的用户列表 */
+const getUserList = async (deptId?: number) => {
+  formLoading.value = true
+  try {
+    // @ts-ignore
+    // TODO @芋艿:替换到 simple List 暂不支持 deptId 过滤
+    // TODO @Zqqq:这个,可以使用前端过滤么?通过 deptList 获取到 deptId 子节点,然后去 userList
+    const data = await UserApi.getUserPage({ pageSize: 100, pageNo: 1, deptId })
+    // 更新过滤后的用户列表
+    filteredUserList.value = data.list
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 提交选择 */
+const submitForm = async () => {
+  try {
+    message.success(t('common.updateSuccess'))
+    dialogVisible.value = false
+    // 从所有用户列表中筛选出已选择的用户
+    const emitUserList = userList.value.filter((user: any) =>
+      selectedUserIdList.value.includes(user.id)
+    )
+    // 发送操作成功的事件
+    emit('confirm', activityId.value, emitUserList)
+  } finally {
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  deptTree.value = []
+  userList.value = []
+  filteredUserList.value = []
+  selectedUserIdList.value = []
+}
+
+/** 处理部门被点击 */
+const handleNodeClick = (row: { [key: string]: any }) => {
+  getUserList(row.id)
+}
+
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+</script>
+
+<style lang="scss" scoped>
+:deep() {
+  .el-transfer {
+    display: flex;
+  }
+  .el-transfer__buttons {
+    display: flex !important;
+    flex-direction: column-reverse;
+    justify-content: center;
+    gap: 20px;
+    .el-transfer__button:nth-child(2) {
+      margin: 0;
+    }
+  }
+}
+</style>

+ 327 - 612
src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue

@@ -1,664 +1,379 @@
 <template>
-  <div class="my-process-designer">
-    <div class="my-process-designer__container">
-      <div class="my-process-designer__canvas" style="height: 760px" ref="bpmnCanvas"></div>
+  <div class="process-viewer">
+    <div style="height: 100%" ref="processCanvas" v-show="!isLoading"> </div>
+    <!-- 自定义箭头样式,用于已完成状态下流程连线箭头 -->
+    <defs ref="customDefs">
+      <marker
+        id="sequenceflow-end-white-success"
+        viewBox="0 0 20 20"
+        refX="11"
+        refY="10"
+        markerWidth="10"
+        markerHeight="10"
+        orient="auto"
+      >
+        <path
+          class="success-arrow"
+          d="M 1 5 L 11 10 L 1 15 Z"
+          style="stroke-width: 1px; stroke-linecap: round; stroke-dasharray: 10000, 1"
+        />
+      </marker>
+      <marker
+        id="conditional-flow-marker-white-success"
+        viewBox="0 0 20 20"
+        refX="-1"
+        refY="10"
+        markerWidth="10"
+        markerHeight="10"
+        orient="auto"
+      >
+        <path
+          class="success-conditional"
+          d="M 0 10 L 8 6 L 16 10 L 8 14 Z"
+          style="stroke-width: 1px; stroke-linecap: round; stroke-dasharray: 10000, 1"
+        />
+      </marker>
+    </defs>
+
+    <!-- 审批记录 -->
+    <el-dialog :title="dialogTitle || '审批记录'" v-model="dialogVisible" width="1000px">
+      <el-row>
+        <el-table
+          :data="selectTasks"
+          size="small"
+          border
+          header-cell-class-name="table-header-gray"
+        >
+          <el-table-column
+            label="序号"
+            header-align="center"
+            align="center"
+            type="index"
+            width="50"
+          />
+          <el-table-column
+            label="审批人"
+            min-width="100"
+            align="center"
+            v-if="selectActivityType === 'bpmn:UserTask'"
+          >
+            <template #default="scope">
+              {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
+            </template>
+          </el-table-column>
+          <el-table-column
+            label="发起人"
+            prop="assigneeUser.nickname"
+            min-width="100"
+            align="center"
+            v-else
+          />
+          <el-table-column label="部门" min-width="100" align="center">
+            <template #default="scope">
+              {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
+            </template>
+          </el-table-column>
+          <el-table-column
+            :formatter="dateFormatter"
+            align="center"
+            label="开始时间"
+            prop="createTime"
+            min-width="140"
+          />
+          <el-table-column
+            :formatter="dateFormatter"
+            align="center"
+            label="结束时间"
+            prop="endTime"
+            min-width="140"
+          />
+          <el-table-column align="center" label="审批状态" prop="status" min-width="90">
+            <template #default="scope">
+              <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
+            </template>
+          </el-table-column>
+          <el-table-column
+            align="center"
+            label="审批建议"
+            prop="reason"
+            min-width="120"
+            v-if="selectActivityType === 'bpmn:UserTask'"
+          />
+          <el-table-column align="center" label="耗时" prop="durationInMillis" width="100">
+            <template #default="scope">
+              {{ formatPast2(scope.row.durationInMillis) }}
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-row>
+    </el-dialog>
+
+    <!-- Zoom:放大、缩小 -->
+    <div style="position: absolute; top: 0; left: 0; width: 100%">
+      <el-row type="flex" justify="end">
+        <el-button-group key="scale-control" size="default">
+          <el-button
+            size="default"
+            :plain="true"
+            :disabled="defaultZoom <= 0.3"
+            :icon="ZoomOut"
+            @click="processZoomOut()"
+          />
+          <el-button size="default" style="width: 90px">
+            {{ Math.floor(defaultZoom * 10 * 10) + '%' }}
+          </el-button>
+          <el-button
+            size="default"
+            :plain="true"
+            :disabled="defaultZoom >= 3.9"
+            :icon="ZoomIn"
+            @click="processZoomIn()"
+          />
+          <el-button size="default" :icon="ScaleToOriginal" @click="processReZoom()" />
+        </el-button-group>
+      </el-row>
     </div>
   </div>
 </template>
 
 <script lang="ts" setup>
+import '../theme/index.scss'
 import BpmnViewer from 'bpmn-js/lib/Viewer'
-import DefaultEmptyXML from './plugins/defaultEmpty'
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { formatDate } from '@/utils/formatTime'
-import { isEmpty } from '@/utils/is'
-
-defineOptions({ name: 'MyProcessViewer' })
+import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas'
+import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue'
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
+import { BpmProcessInstanceStatus } from '@/utils/constants'
 
 const props = defineProps({
-  value: {
-    // BPMN XML 字符串
-    type: String,
-    default: ''
-  },
-  prefix: {
-    // 使用哪个引擎
+  xml: {
     type: String,
-    default: 'camunda'
+    required: true
   },
-  activityData: {
-    // 活动的数据。传递时,可高亮流程
-    type: Array,
-    default: () => []
-  },
-  processInstanceData: {
-    // 流程实例的数据。传递时,可展示流程发起人等信息
+  view: {
     type: Object,
-    default: () => {}
-  },
-  taskData: {
-    // 任务实例的数据。传递时,可展示 UserTask 审核相关的信息
-    type: Array,
-    default: () => []
+    require: true
   }
 })
 
-provide('configGlobal', props)
-
-const emit = defineEmits(['destroy'])
+const processCanvas = ref()
+const bpmnViewer = ref<BpmnViewer | null>(null)
+const customDefs = ref()
+const defaultZoom = ref(1) // 默认缩放比例
+const isLoading = ref(false) // 是否加载中
 
-let bpmnModeler
+const processInstance = ref<any>({}) // 流程实例
+const tasks = ref([]) // 流程任务
 
-const xml = ref('')
-const activityLists = ref<any[]>([])
-const processInstance = ref<any>(undefined)
-const taskList = ref<any[]>([])
-const bpmnCanvas = ref()
-// const element = ref()
-const elementOverlayIds = ref<any>(null)
-const overlays = ref<any>(null)
+const dialogVisible = ref(false) // 弹窗可见性
+const dialogTitle = ref<string | undefined>(undefined) // 弹窗标题
+const selectActivityType = ref<string | undefined>(undefined) // 选中 Task 的活动编号
+const selectTasks = ref<any[]>([]) // 选中的任务数组
 
-const initBpmnModeler = () => {
-  if (bpmnModeler) return
-  bpmnModeler = new BpmnViewer({
-    container: bpmnCanvas.value,
-    bpmnRenderer: {}
-  })
+/** Zoom:恢复 */
+const processReZoom = () => {
+  defaultZoom.value = 1
+  bpmnViewer.value?.get('canvas').zoom('fit-viewport', 'auto')
 }
 
-/* 创建新的流程图 */
-const createNewDiagram = async (xml) => {
-  // 将字符串转换成图显示出来
-  let newId = `Process_${new Date().getTime()}`
-  let newName = `业务流程_${new Date().getTime()}`
-  let xmlString = xml || DefaultEmptyXML(newId, newName, props.prefix)
-  try {
-    let { warnings } = await bpmnModeler.importXML(xmlString)
-    if (warnings && warnings.length) {
-      warnings.forEach((warn) => console.warn(warn))
-    }
-    // 高亮流程图
-    await highlightDiagram()
-    const canvas = bpmnModeler.get('canvas')
-    canvas.zoom('fit-viewport', 'auto')
-  } catch (e) {
-    console.error(e)
-    // console.error(`[Process Designer Warn]: ${e?.message || e}`);
+/** Zoom:放大 */
+const processZoomIn = (zoomStep = 0.1) => {
+  let newZoom = Math.floor(defaultZoom.value * 100 + zoomStep * 100) / 100
+  if (newZoom > 4) {
+    throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4')
   }
+  defaultZoom.value = newZoom
+  bpmnViewer.value?.get('canvas').zoom(defaultZoom.value)
 }
 
-/* 高亮流程图 */
-// TODO 芋艿:如果多个 endActivity 的话,目前的逻辑可能有一定的问题。https://www.jdon.com/workflow/multi-events.html
-const highlightDiagram = async () => {
-  const activityList = activityLists.value
-  if (activityList.length === 0) {
-    return
-  }
-  // 参考自 https://gitee.com/tony2y/RuoYi-flowable/blob/master/ruoyi-ui/src/components/Process/index.vue#L222 实现
-  // 再次基础上,增加不同审批结果的颜色等等
-  let canvas = bpmnModeler.get('canvas')
-  let todoActivity: any = activityList.find((m: any) => !m.endTime) // 找到待办的任务
-  let endActivity: any = activityList[activityList.length - 1] // 获得最后一个任务
-  let findProcessTask = false //是否已经高亮了进行中的任务
-  //进行中高亮之后的任务 key 集合,用于过滤掉 taskList 进行中后面的任务,避免进行中后面的数据 Hover 还有数据
-  let removeTaskDefinitionKeyList = []
-  // debugger
-  bpmnModeler.getDefinitions().rootElements[0].flowElements?.forEach((n: any) => {
-    let activity: any = activityList.find((m: any) => m.key === n.id) // 找到对应的活动
-    if (!activity) {
-      return
-    }
-    if (n.$type === 'bpmn:UserTask') {
-      // 用户任务
-      // 处理用户任务的高亮
-      const task: any = taskList.value.find((m: any) => m.id === activity.taskId) // 找到活动对应的 taskId
-      if (!task) {
-        return
-      }
-      // 进行中的任务已经高亮过了,则不高亮后面的任务了
-      if (findProcessTask) {
-        removeTaskDefinitionKeyList.push(n.id)
-        return
-      }
-      // 高亮任务
-      canvas.addMarker(n.id, getResultCss(task.status))
-      //标记是否高亮了进行中任务
-      if (task.status === 1) {
-        findProcessTask = true
-      }
-      // 如果非通过,就不走后面的线条了
-      if (task.status !== 2) {
-        return
-      }
-      // 处理 outgoing 出线
-      const outgoing = getActivityOutgoing(activity)
-      outgoing?.forEach((nn: any) => {
-        // debugger
-        let targetActivity: any = activityList.find((m: any) => m.key === nn.targetRef.id)
-        // 如果目标活动存在,则根据该活动是否结束,进行【bpmn:SequenceFlow】连线的高亮设置
-        if (targetActivity) {
-          canvas.addMarker(nn.id, targetActivity.endTime ? 'highlight' : 'highlight-todo')
-        } else if (nn.targetRef.$type === 'bpmn:ExclusiveGateway') {
-          // TODO 芋艿:这个流程,暂时没走到过
-          canvas.addMarker(nn.id, activity.endTime ? 'highlight' : 'highlight-todo')
-          canvas.addMarker(nn.targetRef.id, activity.endTime ? 'highlight' : 'highlight-todo')
-        } else if (nn.targetRef.$type === 'bpmn:EndEvent') {
-          // TODO 芋艿:这个流程,暂时没走到过
-          if (!todoActivity && endActivity.key === n.id) {
-            canvas.addMarker(nn.id, 'highlight')
-            canvas.addMarker(nn.targetRef.id, 'highlight')
-          }
-          if (!activity.endTime) {
-            canvas.addMarker(nn.id, 'highlight-todo')
-            canvas.addMarker(nn.targetRef.id, 'highlight-todo')
-          }
-        }
-      })
-    } else if (n.$type === 'bpmn:ExclusiveGateway') {
-      // 排它网关
-      // 设置【bpmn:ExclusiveGateway】排它网关的高亮
-      canvas.addMarker(n.id, getActivityHighlightCss(activity))
-      // 查找需要高亮的连线
-      let matchNN: any = undefined
-      let matchActivity: any = undefined
-      n.outgoing?.forEach((nn: any) => {
-        let targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id)
-        if (!targetActivity) {
-          return
-        }
-        // 特殊判断 endEvent 类型的原因,ExclusiveGateway 可能后续连有 2 个路径:
-        //  1. 一个是 UserTask => EndEvent
-        //  2. 一个是 EndEvent
-        // 在选择路径 1 时,其实 EndEvent 可能也存在,导致 1 和 2 都高亮,显然是不正确的。
-        // 所以,在 matchActivity 为 EndEvent 时,需要进行覆盖~~
-        if (!matchActivity || matchActivity.type === 'endEvent') {
-          matchNN = nn
-          matchActivity = targetActivity
-        }
-      })
-      if (matchNN && matchActivity) {
-        canvas.addMarker(matchNN.id, getActivityHighlightCss(matchActivity))
-      }
-    } else if (n.$type === 'bpmn:ParallelGateway') {
-      // 并行网关
-      // 设置【bpmn:ParallelGateway】并行网关的高亮
-      canvas.addMarker(n.id, getActivityHighlightCss(activity))
-      n.outgoing?.forEach((nn: any) => {
-        // 获得连线是否有指向目标。如果有,则进行高亮
-        const targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id)
-        if (targetActivity) {
-          canvas.addMarker(nn.id, getActivityHighlightCss(targetActivity)) // 高亮【bpmn:SequenceFlow】连线
-          // 高亮【...】目标。其中 ... 可以是 bpm:UserTask、也可以是其它的。当然,如果是 bpm:UserTask 的话,其实不做高亮也没问题,因为上面有逻辑做了这块。
-          canvas.addMarker(nn.targetRef.id, getActivityHighlightCss(targetActivity))
-        }
-      })
-    } else if (n.$type === 'bpmn:StartEvent') {
-      // 开始节点
-      canvas.addMarker(n.id, 'highlight')
-      n.outgoing?.forEach((nn) => {
-        // outgoing 例如说【bpmn:SequenceFlow】连线
-        // 获得连线是否有指向目标。如果有,则进行高亮
-        let targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id)
-        if (targetActivity) {
-          canvas.addMarker(nn.id, 'highlight') // 高亮【bpmn:SequenceFlow】连线
-          canvas.addMarker(n.id, 'highlight') // 高亮【bpmn:StartEvent】开始节点(自己)
-        }
-      })
-    } else if (n.$type === 'bpmn:EndEvent') {
-      // 结束节点
-      if (!processInstance.value || processInstance.value.status === 1) {
-        return
-      }
-      canvas.addMarker(n.id, getResultCss(processInstance.value.status))
-    } else if (n.$type === 'bpmn:ServiceTask') {
-      //服务任务
-      if (activity.startTime > 0 && activity.endTime === 0) {
-        //进入执行,标识进行色
-        canvas.addMarker(n.id, getResultCss(1))
-      }
-      if (activity.endTime > 0) {
-        // 执行完成,节点标识完成色, 所有outgoing标识完成色。
-        canvas.addMarker(n.id, getResultCss(2))
-        const outgoing = getActivityOutgoing(activity)
-        outgoing?.forEach((out) => {
-          canvas.addMarker(out.id, getResultCss(2))
-        })
-      }
-    } else if (n.$type === 'bpmn:SequenceFlow') {
-      let targetActivity = activityList.find((m: any) => m.key === n.targetRef.id)
-      if (targetActivity) {
-        canvas.addMarker(n.id, getActivityHighlightCss(targetActivity))
-      }
-    }
-  })
-  if (!isEmpty(removeTaskDefinitionKeyList)) {
-    taskList.value = taskList.value.filter(
-      (item) => !removeTaskDefinitionKeyList.includes(item.taskDefinitionKey)
-    )
+/** Zoom:缩小 */
+const processZoomOut = (zoomStep = 0.1) => {
+  let newZoom = Math.floor(defaultZoom.value * 100 - zoomStep * 100) / 100
+  if (newZoom < 0.2) {
+    throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2')
   }
+  defaultZoom.value = newZoom
+  bpmnViewer.value?.get('canvas').zoom(defaultZoom.value)
 }
 
-const getActivityHighlightCss = (activity) => {
-  return activity.endTime ? 'highlight' : 'highlight-todo'
-}
-
-const getResultCss = (status) => {
-  if (status === 1) {
-    // 审批中
-    return 'highlight-todo'
-  } else if (status === 2) {
-    // 已通过
-    return 'highlight'
-  } else if (status === 3) {
-    // 不通过
-    return 'highlight-reject'
-  } else if (status === 4) {
-    // 已取消
-    return 'highlight-cancel'
-  } else if (status === 5) {
-    // 退回
-    return 'highlight-return'
-  } else if (status === 6) {
-    // 委派
-    return 'highlight-todo'
-  } else if (status === 7) {
-    // 审批通过中
-    return 'highlight-todo'
-  } else if (status === 0) {
-    // 待审批
-    return 'highlight-todo'
+/** 流程图预览清空 */
+const clearViewer = () => {
+  if (processCanvas.value) {
+    processCanvas.value.innerHTML = ''
+  }
+  if (bpmnViewer.value) {
+    bpmnViewer.value.destroy()
   }
-  return ''
+  bpmnViewer.value = null
 }
 
-const getActivityOutgoing = (activity) => {
-  // 如果有 outgoing,则直接使用它
-  if (activity.outgoing && activity.outgoing.length > 0) {
-    return activity.outgoing
+/** 添加自定义箭头 */
+// TODO 芋艿:自定义箭头不生效,有点奇怪!!!!相关的 marker-end、marker-start 暂时也注释了!!!
+const addCustomDefs = () => {
+  if (!bpmnViewer.value) {
+    return
   }
-  // 如果没有,则遍历获得起点为它的【bpmn:SequenceFlow】节点们。原因是:bpmn-js 的 UserTask 拿不到 outgoing
-  const flowElements = bpmnModeler.getDefinitions().rootElements[0].flowElements
-  const outgoing: any[] = []
-  flowElements.forEach((item: any) => {
-    if (item.$type !== 'bpmn:SequenceFlow') {
-      return
-    }
-    if (item.sourceRef.id === activity.key) {
-      outgoing.push(item)
-    }
-  })
-  return outgoing
+  const canvas = bpmnViewer.value?.get('canvas')
+  const svg = canvas?._svg
+  svg.appendChild(customDefs.value)
 }
-const initModelListeners = () => {
-  const EventBus = bpmnModeler.get('eventBus')
-  // 注册需要的监听事件
-  EventBus.on('element.hover', function (eventObj) {
-    let element = eventObj ? eventObj.element : null
-    elementHover(element)
-  })
-  EventBus.on('element.out', function (eventObj) {
-    let element = eventObj ? eventObj.element : null
-    elementOut(element)
-  })
-}
-// 流程图的元素被 hover
-const elementHover = (element) => {
-  element.value = element
-  !elementOverlayIds.value && (elementOverlayIds.value = {})
-  !overlays.value && (overlays.value = bpmnModeler.get('overlays'))
-  // 展示信息
-  // console.log(activityLists.value, 'activityLists.value')
-  // console.log(element.value, 'element.value')
-  const activity = activityLists.value.find((m) => m.key === element.value.id)
-  // console.log(activity, 'activityactivityactivityactivity')
-  if (!activity) {
+
+/** 节点选中 */
+const onSelectElement = (element: any) => {
+  // 清空原选中
+  selectActivityType.value = undefined
+  dialogTitle.value = undefined
+  if (!element || !processInstance.value?.id) {
     return
   }
-  if (!elementOverlayIds.value[element.value.id] && element.value.type !== 'bpmn:Process') {
-    let html = `<div class="element-overlays">
-            <p>Elemet id: ${element.value.id}</p>
-            <p>Elemet type: ${element.value.type}</p>
-          </div>` // 默认值
-    if (element.value.type === 'bpmn:StartEvent' && processInstance.value) {
-      html = `<p>发起人:${processInstance.value.startUser.nickname}</p>
-                  <p>部门:${processInstance.value.startUser.deptName}</p>
-                  <p>创建时间:${formatDate(processInstance.value.createTime)}`
-    } else if (element.value.type === 'bpmn:UserTask') {
-      let task = taskList.value.find((m) => m.id === activity.taskId) // 找到活动对应的 taskId
-      if (!task) {
-        return
-      }
-      let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS)
-      let dataResult = ''
-      optionData.forEach((element) => {
-        if (element.value == task.status) {
-          dataResult = element.label
-        }
-      })
-      html = `<p>审批人:${task.assigneeUser.nickname}</p>
-                  <p>部门:${task.assigneeUser.deptName}</p>
-                  <p>结果:${dataResult}</p>
-                  <p>创建时间:${formatDate(task.createTime)}</p>`
-      // html = `<p>审批人:${task.assigneeUser.nickname}</p>
-      //             <p>部门:${task.assigneeUser.deptName}</p>
-      //             <p>结果:${getIntDictOptions(
-      //               DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT,
-      //               task.status
-      //             )}</p>
-      //             <p>创建时间:${formatDate(task.createTime)}</p>`
-      if (task.endTime) {
-        html += `<p>结束时间:${formatDate(task.endTime)}</p>`
-      }
-      if (task.reason) {
-        html += `<p>审批建议:${task.reason}</p>`
-      }
-    } else if (element.value.type === 'bpmn:ServiceTask' && processInstance.value) {
-      if (activity.startTime > 0) {
-        html = `<p>创建时间:${formatDate(activity.startTime)}</p>`
-      }
-      if (activity.endTime > 0) {
-        html += `<p>结束时间:${formatDate(activity.endTime)}</p>`
-      }
-      console.log(html)
-    } else if (element.value.type === 'bpmn:EndEvent' && processInstance.value) {
-      let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS)
-      let dataResult = ''
-      optionData.forEach((element) => {
-        if (element.value == processInstance.value.status) {
-          dataResult = element.label
-        }
-      })
-      html = `<p>结果:${dataResult}</p>`
-      // html = `<p>结果:${getIntDictOptions(
-      //   DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT,
-      //   processInstance.value.status
-      // )}</p>`
-      if (processInstance.value.endTime) {
-        html += `<p>结束时间:${formatDate(processInstance.value.endTime)}</p>`
+
+  // UserTask 的情况
+  const activityType = element.type
+  selectActivityType.value = activityType
+  if (activityType === 'bpmn:UserTask') {
+    dialogTitle.value = element.businessObject ? element.businessObject.name : undefined
+    selectTasks.value = tasks.value.filter((item: any) => item?.taskDefinitionKey === element.id)
+    dialogVisible.value = true
+  } else if (activityType === 'bpmn:EndEvent' || activityType === 'bpmn:StartEvent') {
+    dialogTitle.value = '审批信息'
+    selectTasks.value = [
+      {
+        assigneeUser: processInstance.value.startUser,
+        createTime: processInstance.value.startTime,
+        endTime: processInstance.value.endTime,
+        status: processInstance.value.status,
+        durationInMillis: processInstance.value.durationInMillis
       }
-    }
-    // console.log(html, 'html111111111111111')
-    elementOverlayIds.value[element.value.id] = toRaw(overlays.value)?.add(element.value, {
-      position: { left: 0, bottom: 0 },
-      html: `<div class="element-overlays">${html}</div>`
-    })
+    ]
+    dialogVisible.value = true
   }
 }
 
-// 流程图的元素被 out
-const elementOut = (element) => {
-  toRaw(overlays.value).remove({ element })
-  elementOverlayIds.value[element.id] = null
-}
+/** 初始化 BPMN 视图 */
+const importXML = async (xml: string) => {
+  // 清空流程图
+  clearViewer()
 
-onMounted(() => {
-  xml.value = props.value
-  activityLists.value = props.activityData
-  // 初始化
-  initBpmnModeler()
-  createNewDiagram(xml.value)
-  // 初始模型的监听器
-  initModelListeners()
-})
+  // 初始化流程图
+  if (xml != null && xml !== '') {
+    try {
+      bpmnViewer.value = new BpmnViewer({
+        additionalModules: [MoveCanvasModule],
+        container: processCanvas.value
+      })
+      // 增加点击事件
+      bpmnViewer.value.on('element.click', ({ element }) => {
+        onSelectElement(element)
+      })
 
-onBeforeUnmount(() => {
-  // this.$once('hook:beforeDestroy', () => {
-  // })
-  if (bpmnModeler) bpmnModeler.destroy()
-  emit('destroy', bpmnModeler)
-  bpmnModeler = null
-})
+      // 初始化 BPMN 视图
+      isLoading.value = true
+      await bpmnViewer.value.importXML(xml)
+      // 自定义成功的箭头
+      addCustomDefs()
+    } catch (e) {
+      clearViewer()
+    } finally {
+      isLoading.value = false
+      // 高亮流程
+      setProcessStatus(props.view)
+    }
+  }
+}
 
-watch(
-  () => props.value,
-  (newValue) => {
-    xml.value = newValue
-    createNewDiagram(xml.value)
+/** 高亮流程 */
+const setProcessStatus = (view: any) => {
+  // 设置相关变量
+  if (!view || !view.processInstance) {
+    return
   }
-)
-watch(
-  () => props.activityData,
-  (newActivityData) => {
-    activityLists.value = newActivityData
-    createNewDiagram(xml.value)
+  processInstance.value = view.processInstance
+  tasks.value = view.tasks
+  if (isLoading.value || !bpmnViewer.value) {
+    return
   }
-)
-watch(
-  () => props.processInstanceData,
-  (newProcessInstanceData) => {
-    processInstance.value = newProcessInstanceData
-    createNewDiagram(xml.value)
+  const {
+    unfinishedTaskActivityIds,
+    finishedTaskActivityIds,
+    finishedSequenceFlowActivityIds,
+    rejectedTaskActivityIds
+  } = view
+  const canvas = bpmnViewer.value.get('canvas')
+  const elementRegistry = bpmnViewer.value.get('elementRegistry')
+
+  // 已完成节点
+  if (Array.isArray(finishedSequenceFlowActivityIds)) {
+    finishedSequenceFlowActivityIds.forEach((item: any) => {
+      if (item != null) {
+        canvas.addMarker(item, 'success')
+        const element = elementRegistry.get(item)
+        const conditionExpression = element.businessObject.conditionExpression
+        if (conditionExpression) {
+          canvas.addMarker(item, 'condition-expression')
+        }
+      }
+    })
   }
-)
-watch(
-  () => props.taskData,
-  (newTaskListData) => {
-    taskList.value = newTaskListData
-    createNewDiagram(xml.value)
+  if (Array.isArray(finishedTaskActivityIds)) {
+    finishedTaskActivityIds.forEach((item: any) => canvas.addMarker(item, 'success'))
   }
-)
-</script>
 
-<style lang="scss">
-/** 处理中 */
-.highlight-todo.djs-connection > .djs-visual > path {
-  stroke: #1890ff !important;
-  stroke-dasharray: 4px !important;
-  fill-opacity: 0.2 !important;
-}
-
-.highlight-todo.djs-shape .djs-visual > :nth-child(1) {
-  fill: #1890ff !important;
-  stroke: #1890ff !important;
-  stroke-dasharray: 4px !important;
-  fill-opacity: 0.2 !important;
-}
-
-:deep(.highlight-todo.djs-connection > .djs-visual > path) {
-  stroke: #1890ff !important;
-  stroke-dasharray: 4px !important;
-  fill-opacity: 0.2 !important;
-  marker-end: url('#sequenceflow-end-_E7DFDF-_E7DFDF-803g1kf6zwzmcig1y2ulm5egr');
-}
-
-:deep(.highlight-todo.djs-shape .djs-visual > :nth-child(1)) {
-  fill: #1890ff !important;
-  stroke: #1890ff !important;
-  stroke-dasharray: 4px !important;
-  fill-opacity: 0.2 !important;
-}
-
-/** 通过 */
-.highlight.djs-shape .djs-visual > :nth-child(1) {
-  fill: green !important;
-  stroke: green !important;
-  fill-opacity: 0.2 !important;
-}
-
-.highlight.djs-shape .djs-visual > :nth-child(2) {
-  fill: green !important;
-}
-
-.highlight.djs-shape .djs-visual > path {
-  fill: green !important;
-  fill-opacity: 0.2 !important;
-  stroke: green !important;
-}
-
-.highlight.djs-connection > .djs-visual > path {
-  stroke: green !important;
-}
-
-.highlight:not(.djs-connection) .djs-visual > :nth-child(1) {
-  fill: green !important; /* color elements as green */
-}
-
-:deep(.highlight.djs-shape .djs-visual > :nth-child(1)) {
-  fill: green !important;
-  stroke: green !important;
-  fill-opacity: 0.2 !important;
-}
-
-:deep(.highlight.djs-shape .djs-visual > :nth-child(2)) {
-  fill: green !important;
-}
-
-:deep(.highlight.djs-shape .djs-visual > path) {
-  fill: green !important;
-  fill-opacity: 0.2 !important;
-  stroke: green !important;
-}
-
-:deep(.highlight.djs-connection > .djs-visual > path) {
-  stroke: green !important;
-}
-
-.djs-element.highlight > .djs-visual > path {
-  stroke: green !important;
-}
-
-/** 不通过 */
-.highlight-reject.djs-shape .djs-visual > :nth-child(1) {
-  fill: red !important;
-  stroke: red !important;
-  fill-opacity: 0.2 !important;
-}
-
-.highlight-reject.djs-shape .djs-visual > :nth-child(2) {
-  fill: red !important;
-}
-
-.highlight-reject.djs-shape .djs-visual > path {
-  fill: red !important;
-  fill-opacity: 0.2 !important;
-  stroke: red !important;
-}
-
-.highlight-reject.djs-connection > .djs-visual > path {
-  stroke: red !important;
-  marker-end: url(#sequenceflow-end-white-success) !important;
-}
-
-.highlight-reject:not(.djs-connection) .djs-visual > :nth-child(1) {
-  fill: red !important; /* color elements as green */
-}
-
-:deep(.highlight-reject.djs-shape .djs-visual > :nth-child(1)) {
-  fill: red !important;
-  stroke: red !important;
-  fill-opacity: 0.2 !important;
-}
-
-:deep(.highlight-reject.djs-shape .djs-visual > :nth-child(2)) {
-  fill: red !important;
-}
-
-:deep(.highlight-reject.djs-shape .djs-visual > path) {
-  fill: red !important;
-  fill-opacity: 0.2 !important;
-  stroke: red !important;
-}
-
-:deep(.highlight-reject.djs-connection > .djs-visual > path) {
-  stroke: red !important;
-}
-
-/** 已取消 */
-.highlight-cancel.djs-shape .djs-visual > :nth-child(1) {
-  fill: grey !important;
-  stroke: grey !important;
-  fill-opacity: 0.2 !important;
-}
-
-.highlight-cancel.djs-shape .djs-visual > :nth-child(2) {
-  fill: grey !important;
-}
-
-.highlight-cancel.djs-shape .djs-visual > path {
-  fill: grey !important;
-  fill-opacity: 0.2 !important;
-  stroke: grey !important;
-}
-
-.highlight-cancel.djs-connection > .djs-visual > path {
-  stroke: grey !important;
-}
-
-.highlight-cancel:not(.djs-connection) .djs-visual > :nth-child(1) {
-  fill: grey !important; /* color elements as green */
-}
-
-:deep(.highlight-cancel.djs-shape .djs-visual > :nth-child(1)) {
-  fill: grey !important;
-  stroke: grey !important;
-  fill-opacity: 0.2 !important;
-}
-
-:deep(.highlight-cancel.djs-shape .djs-visual > :nth-child(2)) {
-  fill: grey !important;
-}
-
-:deep(.highlight-cancel.djs-shape .djs-visual > path) {
-  fill: grey !important;
-  fill-opacity: 0.2 !important;
-  stroke: grey !important;
-}
-
-:deep(.highlight-cancel.djs-connection > .djs-visual > path) {
-  stroke: grey !important;
-}
-
-/** 回退 */
-.highlight-return.djs-shape .djs-visual > :nth-child(1) {
-  fill: #e6a23c !important;
-  stroke: #e6a23c !important;
-  fill-opacity: 0.2 !important;
-}
-
-.highlight-return.djs-shape .djs-visual > :nth-child(2) {
-  fill: #e6a23c !important;
-}
-
-.highlight-return.djs-shape .djs-visual > path {
-  fill: #e6a23c !important;
-  fill-opacity: 0.2 !important;
-  stroke: #e6a23c !important;
-}
-
-.highlight-return.djs-connection > .djs-visual > path {
-  stroke: #e6a23c !important;
-}
+  // 未完成节点
+  if (Array.isArray(unfinishedTaskActivityIds)) {
+    unfinishedTaskActivityIds.forEach((item: any) => canvas.addMarker(item, 'primary'))
+  }
 
-.highlight-return:not(.djs-connection) .djs-visual > :nth-child(1) {
-  fill: #e6a23c !important; /* color elements as green */
-}
+  // 被拒绝节点
+  if (Array.isArray(rejectedTaskActivityIds)) {
+    rejectedTaskActivityIds.forEach((item: any) => {
+      if (item != null) {
+        canvas.addMarker(item, 'danger')
+      }
+    })
+  }
 
-:deep(.highlight-return.djs-shape .djs-visual > :nth-child(1)) {
-  fill: #e6a23c !important;
-  stroke: #e6a23c !important;
-  fill-opacity: 0.2 !important;
+  // 特殊:处理 end 节点的高亮。因为 end 在拒绝、取消时,被后端计算成了 finishedTaskActivityIds 里
+  if (
+    [BpmProcessInstanceStatus.CANCEL, BpmProcessInstanceStatus.REJECT].includes(
+      processInstance.value.status
+    )
+  ) {
+    const endNodes = elementRegistry.filter((element: any) => element.type === 'bpmn:EndEvent')
+    endNodes.forEach((item: any) => {
+      canvas.removeMarker(item.id, 'success')
+      if (processInstance.value.status === BpmProcessInstanceStatus.CANCEL) {
+        canvas.addMarker(item.id, 'cancel')
+      } else {
+        canvas.addMarker(item.id, 'danger')
+      }
+    })
+  }
 }
 
-:deep(.highlight-return.djs-shape .djs-visual > :nth-child(2)) {
-  fill: #e6a23c !important;
-}
+watch(
+  () => props.xml,
+  (newXml) => {
+    importXML(newXml)
+  },
+  { immediate: true }
+)
 
-:deep(.highlight-return.djs-shape .djs-visual > path) {
-  fill: #e6a23c !important;
-  fill-opacity: 0.2 !important;
-  stroke: #e6a23c !important;
-}
+watch(
+  () => props.view,
+  (newView) => {
+    setProcessStatus(newView)
+  },
+  { immediate: true }
+)
 
-:deep(.highlight-return.djs-connection > .djs-visual > path) {
-  stroke: #e6a23c !important;
-}
+/** mounted:初始化 */
+onMounted(() => {
+  importXML(props.xml)
+  setProcessStatus(props.view)
+})
 
-.element-overlays {
-  width: 200px;
-  padding: 8px;
-  color: #fafafa;
-  background: rgb(0 0 0 / 60%);
-  border-radius: 4px;
-  box-sizing: border-box;
-}
-</style>
+/** unmount:销毁 */
+onBeforeUnmount(() => {
+  clearViewer()
+})
+</script>

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

@@ -1211,6 +1211,76 @@
           "isAttr": true
         }
       ]
+    },
+    {
+      "name": "AssignStartUserHandlerType",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"]
+      },
+      "properties": [
+      {
+        "name": "value",
+        "type": "Integer",
+        "isBody": true
+      }
+      ]
+    },
+    {
+      "name": "RejectHandlerType",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:UserTask"]
+      },
+      "properties": [
+        {
+          "name": "value",
+          "type": "Integer",
+          "isBody": true
+        }
+      ]
+    },
+    {
+      "name": "RejectReturnTaskId",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:UserTask"]
+      },
+      "properties": [
+        {
+          "name": "value",
+          "type": "String",
+          "isBody": true
+        }
+      ]
+    },
+    {
+      "name": "AssignEmptyHandlerType",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:UserTask"]
+      },
+      "properties": [
+        {
+          "name": "value",
+          "type": "Integer",
+          "isBody": true
+        }
+      ]
+    },
+    {
+      "name": "AssignEmptyUserIds",
+      "superClass": ["Element"],
+      "meta": {
+        "allowedIn": ["bpmn:UserTask"]
+      },
+      "properties": [
+        {
+          "name": "value",
+          "type": "String",
+          "isBody": true
+        }
+      ]
     }
   ],
   "emumerations": []

+ 5 - 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: '700px' }">
     <el-collapse v-model="activeTab">
       <el-collapse-item name="base">
         <!-- class="panel-tab__title" -->
@@ -54,6 +54,10 @@
         <template #title><Icon icon="ep:promotion" />其他</template>
         <element-other-config :id="elementId" />
       </el-collapse-item>
+      <el-collapse-item name="customConfig" v-if="elementType.indexOf('Task') !== -1" key="customConfig">
+        <template #title><Icon icon="ep:circle-plus-filled" />自定义配置</template>
+        <element-custom-config :id="elementId" :type="elementType" />
+      </el-collapse-item>
     </el-collapse>
   </div>
 </template>

+ 283 - 0
src/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue

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

+ 3 - 3
src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue

@@ -268,9 +268,9 @@ const bpmnInstances = () => (window as any)?.bpmnInstances
 const resetFormList = () => {
   bpmnELement.value = bpmnInstances().bpmnElement
   formKey.value = bpmnELement.value.businessObject.formKey
-  if (formKey.value?.length > 0) {
-    formKey.value = parseInt(formKey.value)
-  }
+  // if (formKey.value?.length > 0) {
+  //   formKey.value = parseInt(formKey.value)
+  // }
   // 获取元素扩展属性 或者 创建扩展属性
   elExtensionElements.value =
     bpmnELement.value.businessObject.get('extensionElements') ||

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

@@ -80,7 +80,7 @@ const resetAttributesList = () => {
   otherExtensionList.value = [] // 其他扩展配置
   bpmnElementProperties.value =
     // bpmnElement.value.businessObject?.extensionElements?.filter((ex) => {
-    bpmnElement.value.businessObject?.extensionElements?.values.filter((ex) => {
+    bpmnElement.value.businessObject?.extensionElements?.values?.filter((ex) => {
       if (ex.$type !== `${prefix}:Properties`) {
         otherExtensionList.value.push(ex)
       }

+ 115 - 0
src/components/bpmnProcessDesigner/package/theme/index.scss

@@ -1,2 +1,117 @@
 @use './process-designer.scss';
 @use './process-panel.scss';
+
+$success-color: #4eb819;
+$primary-color: #409EFF;
+$danger-color: #F56C6C;
+$cancel-color: #909399;
+
+.process-viewer {
+  position: relative;
+  border: 1px solid #EFEFEF;
+  background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+') repeat!important;
+
+  .success-arrow {
+    fill: $success-color;
+    stroke: $success-color;
+  }
+
+  .success-conditional {
+    fill: white;
+    stroke: $success-color;
+  }
+
+  .success.djs-connection {
+    .djs-visual path {
+      stroke: $success-color!important;
+      //marker-end: url(#sequenceflow-end-white-success)!important;
+    }
+  }
+
+  .success.djs-connection.condition-expression {
+    .djs-visual path {
+      //marker-start: url(#conditional-flow-marker-white-success)!important;
+    }
+  }
+
+  .success.djs-shape {
+    .djs-visual rect {
+      stroke: $success-color!important;
+      fill: $success-color!important;
+      fill-opacity: 0.15!important;
+    }
+
+    .djs-visual polygon {
+      stroke: $success-color!important;
+    }
+
+    .djs-visual path:nth-child(2) {
+      stroke: $success-color!important;
+      fill: $success-color!important;
+    }
+
+    .djs-visual circle {
+      stroke: $success-color!important;
+      fill: $success-color!important;
+      fill-opacity: 0.15!important;
+    }
+  }
+
+  .primary.djs-shape {
+    .djs-visual rect {
+      stroke: $primary-color!important;
+      fill: $primary-color!important;
+      fill-opacity: 0.15!important;
+    }
+
+    .djs-visual polygon {
+      stroke: $primary-color!important;
+    }
+
+    .djs-visual circle {
+      stroke: $primary-color!important;
+      fill: $primary-color!important;
+      fill-opacity: 0.15!important;
+    }
+  }
+
+  .danger.djs-shape {
+    .djs-visual rect {
+      stroke: $danger-color!important;
+      fill: $danger-color!important;
+      fill-opacity: 0.15!important;
+    }
+
+    .djs-visual polygon {
+      stroke: $danger-color!important;
+    }
+
+    .djs-visual circle {
+      stroke: $danger-color!important;
+      fill: $danger-color!important;
+      fill-opacity: 0.15!important;
+    }
+  }
+
+  .cancel.djs-shape {
+    .djs-visual rect {
+      stroke: $cancel-color!important;
+      fill: $cancel-color!important;
+      fill-opacity: 0.15!important;
+    }
+
+    .djs-visual polygon {
+      stroke: $cancel-color!important;
+    }
+
+    .djs-visual circle {
+      stroke: $cancel-color!important;
+      fill: $cancel-color!important;
+      fill-opacity: 0.15!important;
+    }
+  }
+}
+
+.process-viewer .djs-tooltip-container, .process-viewer .djs-overlay-container, .process-viewer .djs-palette {
+  display: none;
+}

+ 3 - 4
src/router/modules/remaining.ts

@@ -267,9 +267,9 @@ const remainingRouter: AppRouteRecordRaw[] = [
         }
       },
       {
-        path: 'manager/simple/workflow/model/edit',
-        component: () => import('@/views/bpm/simpleWorkflow/index.vue'),
-        name: 'SimpleWorkflowDesignEditor',
+        path: 'manager/simple/model',
+        component: () => import('@/views/bpm/simple/SimpleModelDesign.vue'),
+        name: 'SimpleModelDesign',
         meta: {
           noCache: true,
           hidden: true,
@@ -292,7 +292,6 @@ const remainingRouter: AppRouteRecordRaw[] = [
       },
       {
         path: 'process-instance/detail',
-        // component: () => import('@/views/bpm/processInstance/detail/index_new.vue'), // TODO 芋艿:新审批界面,已适配 simple 模式,未来会适配 bpmn 模式
         component: () => import('@/views/bpm/processInstance/detail/index.vue'),
         name: 'BpmProcessInstanceDetail',
         meta: {

+ 8 - 0
src/utils/constants.ts

@@ -449,3 +449,11 @@ export const BpmModelFormType = {
   NORMAL: 10, // 流程表单
   CUSTOM: 20 // 业务表单
 }
+
+export const BpmProcessInstanceStatus = {
+  NOT_START: -1, // 未开始
+  RUNNING: 1, // 审批中
+  APPROVE: 2, // 审批通过
+  REJECT: 3, // 审批不通过
+  CANCEL: 4 // 已取消
+}

+ 1 - 0
src/utils/formCreate.ts

@@ -44,6 +44,7 @@ export const setConfAndFields2 = (
   value?: object
 ) => {
   if (isRef(detailPreview)) {
+    // @ts-ignore
     detailPreview = detailPreview.value
   }
   // @ts-ignore

+ 3 - 2
src/views/bpm/category/CategoryForm.vue

@@ -42,6 +42,7 @@
 <script setup lang="ts">
 import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
 import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import { CommonStatusEnum } from '@/utils/constants'
 
 /** BPM 流程分类 表单 */
 defineOptions({ name: 'CategoryForm' })
@@ -57,7 +58,7 @@ const formData = ref({
   id: undefined,
   name: undefined,
   code: undefined,
-  status: undefined,
+  status: CommonStatusEnum.ENABLE,
   sort: undefined
 })
 const formRules = reactive({
@@ -116,7 +117,7 @@ const resetForm = () => {
     id: undefined,
     name: undefined,
     code: undefined,
-    status: undefined,
+    status: CommonStatusEnum.ENABLE,
     sort: undefined
   }
   formRef.value?.resetFields()

+ 8 - 14
src/views/bpm/definition/index.vue

@@ -70,13 +70,7 @@
 
   <!-- 弹窗:流程模型图的预览 -->
   <Dialog title="流程图" v-model="bpmnDetailVisible" width="800">
-    <MyProcessViewer
-      key="designer"
-      v-model="bpmnXml"
-      :value="bpmnXml as any"
-      v-bind="bpmnControlForm"
-      :prefix="bpmnControlForm.prefix"
-    />
+    <MyProcessViewer style="height: 700px" key="designer" :xml="bpmnXml" />
   </Dialog>
 </template>
 
@@ -118,7 +112,7 @@ const formDetailPreview = ref({
   rule: [],
   option: {}
 })
-const handleFormDetail = async (row) => {
+const handleFormDetail = async (row: any) => {
   if (row.formType == 10) {
     // 设置表单
     setConfAndFields2(formDetailPreview, row.formConf, row.formFields)
@@ -133,13 +127,13 @@ const handleFormDetail = async (row) => {
 
 /** 流程图的详情按钮操作 */
 const bpmnDetailVisible = ref(false)
-const bpmnXml = ref(null)
-const bpmnControlForm = ref({
-  prefix: 'flowable'
-})
-const handleBpmnDetail = async (row) => {
-  bpmnXml.value = (await DefinitionApi.getProcessDefinition(row.id))?.bpmnXml
+const bpmnXml = ref('')
+const handleBpmnDetail = async (row: any) => {
+  // 设置可见
+  bpmnXml.value = ''
   bpmnDetailVisible.value = true
+  // 加载 BPMN XML
+  bpmnXml.value = (await DefinitionApi.getProcessDefinition(row.id))?.bpmnXml
 }
 
 /** 初始化 **/

+ 5 - 1
src/views/bpm/form/editor/index.vue

@@ -64,7 +64,11 @@ const designerConfig = ref({
   switchType: [], // 是否可以切换组件类型,或者可以相互切换的字段
   autoActive: true, // 是否自动选中拖入的组件
   useTemplate: false, // 是否生成vue2语法的模板组件
-  formOptions: {}, // 定义表单配置默认值
+  formOptions: {
+    form: {
+      labelWidth: '100px' // 设置默认的 label 宽度为 100px
+    }
+  }, // 定义表单配置默认值
   fieldReadonly: false, // 配置field是否可以编辑
   hiddenDragMenu: false, // 隐藏拖拽操作按钮
   hiddenDragBtn: false, // 隐藏拖拽按钮

+ 2 - 1
src/views/bpm/form/index.vue

@@ -143,8 +143,9 @@ const openForm = (id?: number) => {
   const toRouter: { name: string; query?: { id: number } } = {
     name: 'BpmFormEditor'
   }
+  console.log(typeof id)
   // 表单新建的时候id传的是event需要排除
-  if (typeof id === 'number') {
+  if (typeof id === 'number' || typeof id === 'string') {
     toRouter.query = {
       id
     }

+ 532 - 0
src/views/bpm/model/CategoryDraggableModel.vue

@@ -0,0 +1,532 @@
+<template>
+  <div class="flex items-center h-50px">
+    <!-- 头部:分类名 -->
+    <div class="flex items-center">
+      <el-tooltip content="拖动排序" v-if="isCategorySorting">
+        <Icon
+          :size="22"
+          icon="ic:round-drag-indicator"
+          class="ml-10px category-drag-icon cursor-move text-#8a909c"
+        />
+      </el-tooltip>
+      <h3 class="ml-20px mr-8px text-18px">{{ categoryInfo.name }}</h3>
+      <div class="color-gray-600 text-16px"> ({{ categoryInfo.modelList?.length || 0 }}) </div>
+    </div>
+    <!-- 头部:操作 -->
+    <div class="flex-1 flex" v-if="!isCategorySorting">
+      <div
+        v-if="categoryInfo.modelList.length > 0"
+        class="ml-20px flex items-center"
+        :class="[
+          'transition-transform duration-300 cursor-pointer',
+          isExpand ? 'rotate-180' : 'rotate-0'
+        ]"
+        @click="isExpand = !isExpand"
+      >
+        <Icon icon="ep:arrow-down-bold" color="#999" />
+      </div>
+      <div class="ml-auto flex items-center" :class="isModelSorting ? 'mr-15px' : 'mr-45px'">
+        <template v-if="!isModelSorting">
+          <el-button
+            v-if="categoryInfo.modelList.length > 0"
+            link
+            type="info"
+            class="mr-20px"
+            @click.stop="handleModelSort"
+          >
+            <Icon icon="fa:sort-amount-desc" class="mr-5px" />
+            排序
+          </el-button>
+          <el-button v-else link type="info" class="mr-20px" @click.stop="openModelForm('create')">
+            <Icon icon="fa:plus" class="mr-5px" />
+            新建
+          </el-button>
+          <el-dropdown
+            @command="(command) => handleCategoryCommand(command, categoryInfo)"
+            placement="bottom"
+          >
+            <el-button link type="info">
+              <Icon icon="ep:setting" class="mr-5px" />
+              分类
+            </el-button>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item command="handleRename"> 重命名 </el-dropdown-item>
+                <el-dropdown-item command="handleDeleteCategory"> 删除该类 </el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </template>
+        <template v-else>
+          <el-button @click.stop="handleModelSortCancel"> 取 消 </el-button>
+          <el-button type="primary" @click.stop="handleModelSortSubmit"> 保存排序 </el-button>
+        </template>
+      </div>
+    </div>
+  </div>
+  <!-- 模型列表 -->
+  <el-collapse-transition>
+    <div v-show="isExpand">
+      <el-table
+        :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"
+      >
+        <el-table-column label="流程名" prop="name" min-width="150">
+          <template #default="scope">
+            <div class="flex items-center">
+              <el-tooltip content="拖动排序" v-if="isModelSorting">
+                <Icon
+                  icon="ic:round-drag-indicator"
+                  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 }}
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="可见范围" prop="startUserIds" min-width="100">
+          <template #default="scope">
+            <el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0">
+              全部可见
+            </el-text>
+            <el-text v-else-if="scope.row.startUsers.length == 1">
+              {{ scope.row.startUsers[0].nickname }}
+            </el-text>
+            <el-text v-else>
+              <el-tooltip
+                class="box-item"
+                effect="dark"
+                placement="top"
+                :content="scope.row.startUsers.map((user: any) => user.nickname).join('、')"
+              >
+                {{ scope.row.startUsers[0].nickname }}等 {{ scope.row.startUsers.length }} 人可见
+              </el-tooltip>
+            </el-text>
+          </template>
+        </el-table-column>
+        <el-table-column label="表单信息" prop="formType" min-width="200">
+          <template #default="scope">
+            <el-button
+              v-if="scope.row.formType === BpmModelFormType.NORMAL"
+              type="primary"
+              link
+              @click="handleFormDetail(scope.row)"
+            >
+              <span>{{ scope.row.formName }}</span>
+            </el-button>
+            <el-button
+              v-else-if="scope.row.formType === BpmModelFormType.CUSTOM"
+              type="primary"
+              link
+              @click="handleFormDetail(scope.row)"
+            >
+              <span>{{ scope.row.formCustomCreatePath }}</span>
+            </el-button>
+            <label v-else>暂无表单</label>
+          </template>
+        </el-table-column>
+        <el-table-column label="最后发布" prop="deploymentTime" min-width="250">
+          <template #default="scope">
+            <div class="flex items-center">
+              <span v-if="scope.row.processDefinition" class="w-150px">
+                {{ formatDate(scope.row.processDefinition.deploymentTime) }}
+              </span>
+              <el-tag v-if="scope.row.processDefinition">
+                v{{ scope.row.processDefinition.version }}
+              </el-tag>
+              <el-tag v-else type="warning">未部署</el-tag>
+              <el-tag
+                v-if="scope.row.processDefinition?.suspensionState === 2"
+                type="warning"
+                class="ml-10px"
+              >
+                已停用
+              </el-tag>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="200" fixed="right">
+          <template #default="scope">
+            <el-button
+              link
+              type="primary"
+              @click="openModelForm('update', scope.row.id)"
+              v-hasPermi="['bpm:model:update']"
+              :disabled="!isManagerUser(scope.row)"
+            >
+              修改
+            </el-button>
+            <el-button
+              link
+              class="!ml-5px"
+              type="primary"
+              @click="handleDesign(scope.row)"
+              v-hasPermi="['bpm:model:update']"
+              :disabled="!isManagerUser(scope.row)"
+            >
+              设计
+            </el-button>
+            <el-button
+              link
+              class="!ml-5px"
+              type="primary"
+              @click="handleDeploy(scope.row)"
+              v-hasPermi="['bpm:model:deploy']"
+              :disabled="!isManagerUser(scope.row)"
+            >
+              发布
+            </el-button>
+            <el-dropdown
+              class="!align-middle ml-5px"
+              @command="(command) => handleModelCommand(command, scope.row)"
+              v-hasPermi="['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete']"
+            >
+              <el-button type="primary" link>更多</el-button>
+              <template #dropdown>
+                <el-dropdown-menu>
+                  <el-dropdown-item
+                    command="handleDefinitionList"
+                    v-if="checkPermi(['bpm:process-definition:query'])"
+                  >
+                    历史
+                  </el-dropdown-item>
+                  <el-dropdown-item
+                    command="handleChangeState"
+                    v-if="checkPermi(['bpm:model:update']) && scope.row.processDefinition"
+                    :disabled="!isManagerUser(scope.row)"
+                  >
+                    {{ scope.row.processDefinition.suspensionState === 1 ? '停用' : '启用' }}
+                  </el-dropdown-item>
+                  <el-dropdown-item
+                    type="danger"
+                    command="handleDelete"
+                    v-if="checkPermi(['bpm:model:delete'])"
+                    :disabled="!isManagerUser(scope.row)"
+                  >
+                    删除
+                  </el-dropdown-item>
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+  </el-collapse-transition>
+
+  <!-- 弹窗:重命名分类 -->
+  <Dialog :fullscreen="false" class="rename-dialog" v-model="renameCategoryVisible" width="400">
+    <template #title>
+      <div class="pl-10px font-bold text-18px"> 重命名分类 </div>
+    </template>
+    <div class="px-30px">
+      <el-input v-model="renameCategoryForm.name" />
+    </div>
+    <template #footer>
+      <div class="pr-25px pb-25px">
+        <el-button @click="renameCategoryVisible = false">取 消</el-button>
+        <el-button type="primary" @click="handleRenameConfirm">确 定</el-button>
+      </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'
+import { setConfAndFields2 } from '@/utils/formCreate'
+import { BpmModelFormType, BpmModelType } 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'
+
+defineOptions({ name: 'BpmModel' })
+
+const props = defineProps({
+  categoryInfo: propTypes.object.def([]), // 分类后的数据
+  isCategorySorting: propTypes.bool.def(false) // 是否分类在排序
+})
+const emit = defineEmits(['success'])
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const { push } = useRouter() // 路由
+const userStore = useUserStoreWithOut() // 用户信息缓存
+const isDark = computed(() => useAppStore().getIsDark) // 是否黑暗模式
+
+const isModelSorting = ref(false) // 是否正处于排序状态
+const originalData: any = ref([]) // 原始数据
+const modelList: any = ref([]) // 模型列表
+const isExpand = ref(false) // 是否处于展开状态
+
+/** '更多'操作按钮 */
+const handleModelCommand = (command: string, row: any) => {
+  switch (command) {
+    case 'handleDefinitionList':
+      handleDefinitionList(row)
+      break
+    case 'handleDelete':
+      handleDelete(row)
+      break
+    case 'handleChangeState':
+      handleChangeState(row)
+      break
+    default:
+      break
+  }
+}
+
+/** '分类'操作按钮 */
+const handleCategoryCommand = async (command: string, row: any) => {
+  switch (command) {
+    case 'handleRename':
+      renameCategoryForm.value = await CategoryApi.getCategory(row.id)
+      renameCategoryVisible.value = true
+      break
+    case 'handleDeleteCategory':
+      await handleDeleteCategory()
+      break
+    default:
+      break
+  }
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (row: any) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await ModelApi.deleteModel(row.id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    emit('success')
+  } catch {}
+}
+
+/** 更新状态操作 */
+const handleChangeState = async (row: any) => {
+  const state = row.processDefinition.suspensionState
+  const newState = state === 1 ? 2 : 1
+  try {
+    // 修改状态的二次确认
+    const id = row.id
+    debugger
+    const statusState = state === 1 ? '停用' : '启用'
+    const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
+    await message.confirm(content)
+    // 发起修改状态
+    await ModelApi.updateModelState(id, newState)
+    message.success(statusState + '成功')
+    // 刷新列表
+    emit('success')
+  } catch {}
+}
+
+/** 设计流程 */
+const handleDesign = (row: any) => {
+  if (row.type == BpmModelType.BPMN) {
+    push({
+      name: 'BpmModelEditor',
+      query: {
+        modelId: row.id
+      }
+    })
+  } else {
+    push({
+      name: 'SimpleModelDesign',
+      query: {
+        modelId: row.id
+      }
+    })
+  }
+}
+
+/** 发布流程 */
+const handleDeploy = async (row: any) => {
+  try {
+    // 删除的二次确认
+    await message.confirm('是否部署该流程!!')
+    // 发起部署
+    await ModelApi.deployModel(row.id)
+    message.success(t('部署成功'))
+    // 刷新列表
+    emit('success')
+  } catch {}
+}
+
+/** 跳转到指定流程定义列表 */
+const handleDefinitionList = (row: any) => {
+  push({
+    name: 'BpmProcessDefinition',
+    query: {
+      key: row.key
+    }
+  })
+}
+
+/** 流程表单的详情按钮操作 */
+const formDetailVisible = ref(false)
+const formDetailPreview = ref({
+  rule: [],
+  option: {}
+})
+const handleFormDetail = async (row: any) => {
+  if (row.formType == 10) {
+    // 设置表单
+    const data = await FormApi.getForm(row.formId)
+    setConfAndFields2(formDetailPreview, data.conf, data.fields)
+    // 弹窗打开
+    formDetailVisible.value = true
+  } else {
+    await push({
+      path: row.formCustomCreatePath
+    })
+  }
+}
+
+/** 判断是否可以操作 */
+const isManagerUser = (row: any) => {
+  const userId = userStore.getUser.id
+  return row.managerUserIds && row.managerUserIds.includes(userId)
+}
+
+/** 处理模型的排序 **/
+const handleModelSort = () => {
+  // 保存初始数据
+  originalData.value = cloneDeep(props.categoryInfo.modelList)
+  isModelSorting.value = true
+  initSort()
+}
+
+/** 处理模型的排序提交 */
+const handleModelSortSubmit = async () => {
+  // 保存排序
+  const ids = modelList.value.map((item: any) => item.id)
+  await ModelApi.updateModelSortBatch(ids)
+  // 刷新列表
+  isModelSorting.value = false
+  message.success('排序模型成功')
+  emit('success')
+}
+
+/** 处理模型的排序取消 */
+const handleModelSortCancel = () => {
+  // 恢复初始数据
+  modelList.value = cloneDeep(originalData.value)
+  isModelSorting.value = false
+}
+
+/** 创建拖拽实例 */
+const tableRef = ref()
+const initSort = () => {
+  const table = document.querySelector(`.${props.categoryInfo.name} .el-table__body-wrapper tbody`)
+  Sortable.create(table, {
+    group: 'shared',
+    animation: 150,
+    draggable: '.el-table__row',
+    handle: '.drag-icon',
+    // 结束拖动事件
+    onEnd: ({ newDraggableIndex, oldDraggableIndex }) => {
+      if (oldDraggableIndex !== newDraggableIndex) {
+        modelList.value.splice(
+          newDraggableIndex,
+          0,
+          modelList.value.splice(oldDraggableIndex, 1)[0]
+        )
+      }
+    }
+  })
+}
+
+/** 更新 modelList 模型列表 */
+const updateModeList = () => {
+  modelList.value = cloneDeep(props.categoryInfo.modelList)
+  if (props.categoryInfo.modelList.length > 0) {
+    isExpand.value = true
+  }
+}
+
+/** 重命名弹窗确定 */
+const renameCategoryVisible = ref(false)
+const renameCategoryForm = ref({
+  name: ''
+})
+const handleRenameConfirm = async () => {
+  if (renameCategoryForm.value?.name.length === 0) {
+    return message.warning('请输入名称')
+  }
+  // 发起修改
+  await CategoryApi.updateCategory(renameCategoryForm.value as CategoryVO)
+  message.success('重命名成功')
+  // 刷新列表
+  renameCategoryVisible.value = false
+  emit('success')
+}
+
+/** 删除分类 */
+const handleDeleteCategory = async () => {
+  try {
+    if (props.categoryInfo.modelList.length > 0) {
+      return message.warning('该分类下仍有流程定义,不允许删除')
+    }
+    await message.confirm('确认删除分类吗?')
+    // 发起删除
+    await CategoryApi.deleteCategory(props.categoryInfo.id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    emit('success')
+  } catch {}
+}
+
+/** 添加流程模型弹窗 */
+const modelFormRef = ref()
+const openModelForm = (type: string, id?: number) => {
+  modelFormRef.value.open(type, id)
+}
+
+watch(() => props.categoryInfo.modelList, updateModeList, { immediate: true })
+watch(
+  () => props.isCategorySorting,
+  (val) => {
+    if (val) isExpand.value = false
+  },
+  { immediate: true }
+)
+</script>
+
+<style lang="scss">
+.rename-dialog.el-dialog {
+  padding: 0 !important;
+
+  .el-dialog__header {
+    border-bottom: none;
+  }
+
+  .el-dialog__footer {
+    border-top: none !important;
+  }
+}
+</style>
+<style lang="scss" scoped>
+:deep() {
+  .el-table__cell {
+    overflow: hidden;
+    border-bottom: none !important;
+  }
+}
+</style>

+ 7 - 1
src/views/bpm/model/ModelForm.vue

@@ -155,6 +155,7 @@
   </Dialog>
 </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'
@@ -170,7 +171,9 @@ 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)提交的按钮禁用
@@ -232,6 +235,9 @@ const open = async (type: string, id?: string) => {
   categoryList.value = await CategoryApi.getCategorySimpleList()
   // 查询用户列表
   userList.value = await UserApi.getSimpleUserList()
+  if (props.categoryId) {
+    formData.value.category = props.categoryId
+  }
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 

+ 157 - 340
src/views/bpm/model/index.vue

@@ -1,216 +1,94 @@
 <template>
-  <doc-alert title="流程设计器(BPMN)" url="https://doc.iocoder.cn/bpm/model-designer-dingding/" />
-  <doc-alert
-    title="流程设计器(钉钉、飞书)"
-    url="https://doc.iocoder.cn/bpm/model-designer-bpmn/"
-  />
-  <doc-alert title="选择审批人、发起人自选" url="https://doc.iocoder.cn/bpm/assignee/" />
-  <doc-alert title="会签、或签、依次审批" url="https://doc.iocoder.cn/bpm/multi-instance/" />
-
   <ContentWrap>
-    <!-- 搜索工作栏 -->
-    <el-form
-      class="-mb-15px"
-      :model="queryParams"
-      ref="queryFormRef"
-      :inline="true"
-      label-width="68px"
-    >
-      <el-form-item label="流程标识" prop="key">
-        <el-input
-          v-model="queryParams.key"
-          placeholder="请输入流程标识"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="流程名称" prop="name">
-        <el-input
-          v-model="queryParams.name"
-          placeholder="请输入流程名称"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="流程分类" prop="category">
-        <el-select
-          v-model="queryParams.category"
-          placeholder="请选择流程分类"
-          clearable
-          class="!w-240px"
-        >
-          <el-option
-            v-for="category in categoryList"
-            :key="category.code"
-            :label="category.name"
-            :value="category.code"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item>
-        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
-        <el-button
-          type="primary"
-          plain
-          @click="openForm('create')"
-          v-hasPermi="['bpm:model:create']"
-        >
-          <Icon icon="ep:plus" class="mr-5px" /> 新建
-        </el-button>
-      </el-form-item>
-    </el-form>
-  </ContentWrap>
-
-  <!-- 列表 -->
-  <ContentWrap>
-    <el-table v-loading="loading" :data="list">
-      <el-table-column label="流程名称" align="center" prop="name" min-width="200" />
-      <el-table-column label="流程图标" align="center" prop="icon" min-width="100">
-        <template #default="scope">
-          <el-image :src="scope.row.icon" class="h-32px w-32px" />
-        </template>
-      </el-table-column>
-      <el-table-column label="可见范围" align="center" prop="startUserIds" min-width="100">
-        <template #default="scope">
-          <el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0">
-            全部可见
-          </el-text>
-          <el-text v-else-if="scope.row.startUsers.length == 1">
-            {{ scope.row.startUsers[0].nickname }}
-          </el-text>
-          <el-text v-else>
-            <el-tooltip
-              class="box-item"
-              effect="dark"
-              placement="top"
-              :content="scope.row.startUsers.map((user: any) => user.nickname).join('、')"
-            >
-              {{ scope.row.startUsers[0].nickname }}等 {{ scope.row.startUsers.length }} 人可见
-            </el-tooltip>
-          </el-text>
-        </template>
-      </el-table-column>
-      <el-table-column label="流程分类" align="center" prop="categoryName" min-width="100" />
-      <el-table-column label="表单信息" align="center" prop="formType" min-width="200">
-        <template #default="scope">
-          <el-button
-            v-if="scope.row.formType === 10"
-            type="primary"
-            link
-            @click="handleFormDetail(scope.row)"
-          >
-            <span>{{ scope.row.formName }}</span>
-          </el-button>
-          <el-button
-            v-else-if="scope.row.formType === 20"
-            type="primary"
-            link
-            @click="handleFormDetail(scope.row)"
-          >
-            <span>{{ scope.row.formCustomCreatePath }}</span>
-          </el-button>
-          <label v-else>暂无表单</label>
-        </template>
-      </el-table-column>
-      <el-table-column label="最后发布" align="center" prop="deploymentTime" min-width="250">
-        <template #default="scope">
-          <span v-if="scope.row.processDefinition">
-            {{ formatDate(scope.row.processDefinition.deploymentTime) }}
-          </span>
-          <el-tag v-if="scope.row.processDefinition" class="ml-10px">
-            v{{ scope.row.processDefinition.version }}
-          </el-tag>
-          <el-tag v-else type="warning">未部署</el-tag>
-          <el-tag
-            v-if="scope.row.processDefinition?.suspensionState === 2"
-            type="warning"
-            class="ml-10px"
-          >
-            已停用
-          </el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column label="操作" align="center" width="200" fixed="right">
-        <template #default="scope">
-          <el-button
-            link
-            type="primary"
-            @click="openForm('update', scope.row.id)"
-            v-hasPermi="['bpm:model:update']"
-            :disabled="!isManagerUser(scope.row)"
-          >
-            修改
-          </el-button>
-          <el-button
-            link
-            class="!ml-5px"
-            type="primary"
-            @click="handleDesign(scope.row)"
-            v-hasPermi="['bpm:model:update']"
-            :disabled="!isManagerUser(scope.row)"
+    <div class="flex justify-between pl-20px items-center">
+      <h3 class="font-extrabold">流程模型</h3>
+      <!-- 搜索工作栏 -->
+      <el-form
+        v-if="!isCategorySorting"
+        class="-mb-15px flex mr-10px"
+        :model="queryParams"
+        ref="queryFormRef"
+        :inline="true"
+        label-width="68px"
+        @submit.prevent
+      >
+        <el-form-item prop="name" class="ml-auto">
+          <el-input
+            v-model="queryParams.name"
+            placeholder="搜索流程"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-240px"
           >
-            设计
-          </el-button>
-          <el-button
-            link
-            class="!ml-5px"
-            type="primary"
-            @click="handleDeploy(scope.row)"
-            v-hasPermi="['bpm:model:deploy']"
-            :disabled="!isManagerUser(scope.row)"
-          >
-            发布
+            <template #prefix>
+              <Icon icon="ep:search" class="mx-10px" />
+            </template>
+          </el-input>
+        </el-form-item>
+        <!-- 右上角:新建模型、更多操作 -->
+        <el-form-item>
+          <el-button type="primary" @click="openForm('create')" v-hasPermi="['bpm:model:create']">
+            <Icon icon="ep:plus" class="mr-5px" /> 新建模型
           </el-button>
-          <el-dropdown
-            class="!align-middle ml-5px"
-            @command="(command) => handleCommand(command, scope.row)"
-            v-hasPermi="['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete']"
-          >
-            <el-button type="primary" link>更多</el-button>
+        </el-form-item>
+        <el-form-item>
+          <el-dropdown @command="(command) => handleCommand(command)" placement="bottom-end">
+            <el-button class="w-30px" plain>
+              <Icon icon="ep:setting" />
+            </el-button>
             <template #dropdown>
               <el-dropdown-menu>
-                <el-dropdown-item
-                  command="handleDefinitionList"
-                  v-if="checkPermi(['bpm:process-definition:query'])"
-                >
-                  历史
-                </el-dropdown-item>
-                <el-dropdown-item
-                  command="handleChangeState"
-                  v-if="checkPermi(['bpm:model:update']) && scope.row.processDefinition"
-                  :disabled="!isManagerUser(scope.row)"
-                >
-                  {{ scope.row.processDefinition.suspensionState === 1 ? '停用' : '启用' }}
+                <el-dropdown-item command="handleCategoryAdd">
+                  <Icon icon="ep:circle-plus" :size="13" class="mr-5px" />
+                  新建分类
                 </el-dropdown-item>
-                <el-dropdown-item
-                  type="danger"
-                  command="handleDelete"
-                  v-if="checkPermi(['bpm:model:delete'])"
-                  :disabled="!isManagerUser(scope.row)"
-                >
-                  删除
+                <el-dropdown-item command="handleCategorySort">
+                  <Icon icon="fa:sort-amount-desc" :size="13" class="mr-5px" />
+                  分类排序
                 </el-dropdown-item>
               </el-dropdown-menu>
             </template>
           </el-dropdown>
+        </el-form-item>
+      </el-form>
+      <div class="mr-20px" v-else>
+        <el-button @click="handleCategorySortCancel"> 取 消 </el-button>
+        <el-button type="primary" @click="handleCategorySortSubmit"> 保存排序 </el-button>
+      </div>
+    </div>
+
+    <el-divider />
+
+    <!-- 按照分类,展示其所属的模型列表 -->
+    <div class="px-15px">
+      <draggable
+        :disabled="!isCategorySorting"
+        v-model="categoryGroup"
+        item-key="id"
+        :animation="400"
+      >
+        <template #item="{ element }">
+          <ContentWrap
+            class="rounded-lg transition-all duration-300 ease-in-out hover:shadow-xl"
+            v-loading="loading"
+            :body-style="{ padding: 0 }"
+            :key="element.id"
+          >
+            <CategoryDraggableModel
+              :isCategorySorting="isCategorySorting"
+              :categoryInfo="element"
+              @success="getList"
+            />
+          </ContentWrap>
         </template>
-      </el-table-column>
-    </el-table>
-    <!-- 分页 -->
-    <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
-      v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
+      </draggable>
+    </div>
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改流程 -->
   <ModelForm ref="formRef" @success="getList" />
-
+  <!-- 表单弹窗:添加分类 -->
+  <CategoryForm ref="categoryFormRef" @success="getList" />
   <!-- 弹窗:表单详情 -->
   <Dialog title="表单详情" v-model="formDetailVisible" width="800">
     <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
@@ -218,187 +96,126 @@
 </template>
 
 <script lang="ts" setup>
-import { formatDate } from '@/utils/formatTime'
+import draggable from 'vuedraggable'
+import { CategoryApi } from '@/api/bpm/category'
 import * as ModelApi from '@/api/bpm/model'
-import * as FormApi from '@/api/bpm/form'
 import ModelForm from './ModelForm.vue'
-import { setConfAndFields2 } from '@/utils/formCreate'
-import { CategoryApi } from '@/api/bpm/category'
-import { BpmModelType } from '@/utils/constants'
-import { checkPermi } from '@/utils/permission'
-import { useUserStoreWithOut } from '@/store/modules/user'
+import CategoryForm from '../category/CategoryForm.vue'
+import { cloneDeep } from 'lodash-es'
+import CategoryDraggableModel from './CategoryDraggableModel.vue'
 
 defineOptions({ name: 'BpmModel' })
 
 const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
-const { push } = useRouter() // 路由
-const userStore = useUserStoreWithOut() // 用户信息缓存
-
 const loading = ref(true) // 列表的加载中
-const total = ref(0) // 列表的总页数
-const list = ref([]) // 列表的数据
+const isCategorySorting = ref(false) // 是否 category 正处于排序状态
 const queryParams = reactive({
-  pageNo: 1,
-  pageSize: 10,
-  key: undefined,
-  name: undefined,
-  category: undefined
+  name: undefined
 })
 const queryFormRef = ref() // 搜索的表单
-const categoryList = ref([]) // 流程分类列表
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    const data = await ModelApi.getModelPage(queryParams)
-    list.value = data.list
-    total.value = data.total
-  } finally {
-    loading.value = false
-  }
-}
+const categoryGroup: any = ref([]) // 按照 category 分组的数据
+const originalData: any = ref([]) // 原始数据
 
 /** 搜索按钮操作 */
 const handleQuery = () => {
-  queryParams.pageNo = 1
   getList()
 }
 
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value.resetFields()
-  handleQuery()
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
 }
 
-/** '更多'操作按钮 */
-const handleCommand = (command: string, row: any) => {
+/** 流程表单的详情按钮操作 */
+const formDetailVisible = ref(false)
+const formDetailPreview = ref({
+  rule: [],
+  option: {}
+})
+
+/** 右上角设置按钮 */
+const handleCommand = (command: string) => {
   switch (command) {
-    case 'handleDefinitionList':
-      handleDefinitionList(row)
+    case 'handleCategoryAdd':
+      handleCategoryAdd()
       break
-    case 'handleDelete':
-      handleDelete(row)
-      break
-    case 'handleChangeState':
-      handleChangeState(row)
+    case 'handleCategorySort':
+      handleCategorySort()
       break
     default:
       break
   }
 }
 
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, id)
+/** 新建分类 */
+const categoryFormRef = ref()
+const handleCategoryAdd = () => {
+  categoryFormRef.value.open('create')
 }
 
-/** 删除按钮操作 */
-const handleDelete = async (row: any) => {
-  try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await ModelApi.deleteModel(row.id)
-    message.success(t('common.delSuccess'))
-    // 刷新列表
-    await getList()
-  } catch {}
+/** 分类排序的提交 */
+const handleCategorySort = () => {
+  // 保存初始数据
+  originalData.value = cloneDeep(categoryGroup.value)
+  isCategorySorting.value = true
 }
 
-/** 更新状态操作 */
-const handleChangeState = async (row: any) => {
-  const state = row.processDefinition.suspensionState
-  const newState = state === 1 ? 2 : 1
-  try {
-    // 修改状态的二次确认
-    const id = row.id
-    debugger
-    const statusState = state === 1 ? '停用' : '启用'
-    const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
-    await message.confirm(content)
-    // 发起修改状态
-    await ModelApi.updateModelState(id, newState)
-    message.success(statusState + '成功')
-    // 刷新列表
-    await getList()
-  } catch {}
+/** 分类排序的取消 */
+const handleCategorySortCancel = () => {
+  // 恢复初始数据
+  categoryGroup.value = cloneDeep(originalData.value)
+  isCategorySorting.value = false
 }
 
-/** 设计流程 */
-const handleDesign = (row: any) => {
-  if (row.type == BpmModelType.BPMN) {
-    push({
-      name: 'BpmModelEditor',
-      query: {
-        modelId: row.id
-      }
-    })
-  } else {
-    push({
-      name: 'SimpleWorkflowDesignEditor',
-      query: {
-        modelId: row.id
-      }
-    })
-  }
+/** 分类排序的保存 */
+const handleCategorySortSubmit = async () => {
+  // 保存排序
+  const ids = categoryGroup.value.map((item: any) => item.id)
+  await CategoryApi.updateCategorySortBatch(ids)
+  // 刷新列表
+  isCategorySorting.value = false
+  message.success('排序分类成功')
+  await getList()
 }
 
-/** 发布流程 */
-const handleDeploy = async (row: any) => {
+/** 加载数据 */
+const getList = async () => {
+  loading.value = true
   try {
-    // 删除的二次确认
-    await message.confirm('是否部署该流程!!')
-    // 发起部署
-    await ModelApi.deployModel(row.id)
-    message.success(t('部署成功'))
-    // 刷新列表
-    await getList()
-  } catch {}
-}
-
-/** 跳转到指定流程定义列表 */
-const handleDefinitionList = (row) => {
-  push({
-    name: 'BpmProcessDefinition',
-    query: {
-      key: row.key
-    }
-  })
-}
-
-/** 流程表单的详情按钮操作 */
-const formDetailVisible = ref(false)
-const formDetailPreview = ref({
-  rule: [],
-  option: {}
-})
-const handleFormDetail = async (row: any) => {
-  if (row.formType == 10) {
-    // 设置表单
-    const data = await FormApi.getForm(row.formId)
-    setConfAndFields2(formDetailPreview, data.conf, data.fields)
-    // 弹窗打开
-    formDetailVisible.value = true
-  } else {
-    await push({
-      path: row.formCustomCreatePath
-    })
+    // 查询模型 + 分裂的列表
+    const modelList = await ModelApi.getModelList(queryParams.name)
+    const categoryList = await CategoryApi.getCategorySimpleList()
+    // 按照 category 聚合
+    // 注意:必须一次性赋值给 categoryGroup,否则每次操作后,列表会重新渲染,滚动条的位置会偏离!!!
+    categoryGroup.value = categoryList.map((category: any) => ({
+      ...category,
+      modelList: modelList.filter((model: any) => model.categoryName == category.name)
+    }))
+  } finally {
+    loading.value = false
   }
 }
 
-/** 判断是否可以操作 */
-const isManagerUser = (row: any) => {
-  const userId = userStore.getUser.id
-  return row.managerUserIds && row.managerUserIds.includes(userId)
-}
-
 /** 初始化 **/
-onMounted(async () => {
-  await getList()
-  // 查询流程分类列表
-  categoryList.value = await CategoryApi.getCategorySimpleList()
+onMounted(() => {
+  getList()
 })
 </script>
+
+<style lang="scss" scoped>
+:deep() {
+  .el-table--fit .el-table__inner-wrapper:before {
+    height: 0;
+  }
+  .el-card {
+    border-radius: 8px;
+  }
+  .el-form--inline .el-form-item {
+    margin-right: 10px;
+  }
+  .el-divider--horizontal {
+    margin-top: 6px;
+  }
+}
+</style>

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

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

+ 259 - 0
src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue

@@ -0,0 +1,259 @@
+<template>
+  <ContentWrap :bodyStyle="{ padding: '10px 20px 0' }">
+    <div class="processInstance-wrap-main">
+      <el-scrollbar>
+        <div class="text-#878c93 h-15px">流程:{{ selectProcessDefinition.name }}</div>
+        <el-divider class="!my-8px" />
+
+        <!-- 中间主要内容 tab 栏 -->
+        <el-tabs v-model="activeTab">
+          <!-- 表单信息 -->
+          <el-tab-pane label="表单填写" name="form">
+            <div class="form-scroll-area">
+              <el-scrollbar>
+                <el-row>
+                  <el-col :span="17">
+                    <form-create
+                      :rule="detailForm.rule"
+                      v-model:api="fApi"
+                      v-model="detailForm.value"
+                      :option="detailForm.option"
+                      @submit="submitForm"
+                    />
+                  </el-col>
+
+                  <el-col :span="6" :offset="1">
+                    <!-- 流程时间线 -->
+                    <ProcessInstanceTimeline
+                      ref="timelineRef"
+                      :activity-nodes="activityNodes"
+                      :show-status-icon="false"
+                      @select-user-confirm="selectUserConfirm"
+                    />
+                  </el-col>
+                </el-row>
+              </el-scrollbar>
+            </div>
+          </el-tab-pane>
+          <!-- 流程图 -->
+          <el-tab-pane label="流程图" name="diagram">
+            <div class="form-scroll-area">
+              <!-- BPMN 流程图预览 -->
+              <ProcessInstanceBpmnViewer
+                :bpmn-xml="bpmnXML"
+                v-if="BpmModelType.BPMN === selectProcessDefinition.modelType"
+              />
+
+              <!-- Simple 流程图预览 -->
+              <ProcessInstanceSimpleViewer
+                :simple-json="simpleJson"
+                v-if="BpmModelType.SIMPLE === selectProcessDefinition.modelType"
+              />
+            </div>
+          </el-tab-pane>
+        </el-tabs>
+
+        <!-- 底部操作栏 -->
+        <div class="b-t-solid border-t-1px border-[var(--el-border-color)]">
+          <!-- 操作栏按钮 -->
+          <div
+            v-if="activeTab === 'form'"
+            class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
+          >
+            <el-button plain type="success" @click="submitForm">
+              <Icon icon="ep:select" />&nbsp; 发起
+            </el-button>
+            <el-button plain type="danger" @click="handleCancel">
+              <Icon icon="ep:close" />&nbsp; 取消
+            </el-button>
+          </div>
+        </div>
+      </el-scrollbar>
+    </div>
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { decodeFields, setConfAndFields2 } from '@/utils/formCreate'
+import { BpmModelType } from '@/utils/constants'
+import { CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts'
+import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
+import ProcessInstanceSimpleViewer from '../detail/ProcessInstanceSimpleViewer.vue'
+import ProcessInstanceTimeline from '../detail/ProcessInstanceTimeline.vue'
+import type { ApiAttrs } from '@form-create/element-ui/types/config'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import * as DefinitionApi from '@/api/bpm/definition'
+import { ApprovalNodeInfo } from '@/api/bpm/processInstance'
+
+defineOptions({ name: 'ProcessDefinitionDetail' })
+const props = defineProps<{
+  selectProcessDefinition: any
+}>()
+const emit = defineEmits(['cancel'])
+
+const { push, currentRoute } = useRouter() // 路由
+const message = useMessage() // 消息弹窗
+const { delView } = useTagsViewStore() // 视图操作
+
+const detailForm: any = ref({
+  rule: [],
+  option: {},
+  value: {}
+}) // 流程表单详情
+const fApi = ref<ApiAttrs>()
+// 指定审批人
+const startUserSelectTasks: any = ref([]) // 发起人需要选择审批人或抄送人的任务列表
+const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
+const bpmnXML: any = ref(null) // BPMN 数据
+const simpleJson = ref<string | undefined>() // Simple 设计器数据 json 格式
+
+const activeTab = ref('form') // 当前的 Tab
+const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) // 审批节点信息
+
+/** 设置表单信息、获取流程图数据 **/
+const initProcessInfo = async (row: any, formVariables?: any) => {
+  // 重置指定审批人
+  startUserSelectTasks.value = []
+  startUserSelectAssignees.value = {}
+
+  // 情况一:流程表单
+  if (row.formType == 10) {
+    // 设置表单
+    // 注意:需要从 formVariables 中,移除不在 row.formFields 的值。
+    // 原因是:后端返回的 formVariables 里面,会有一些非表单的信息。例如说,某个流程节点的审批人。
+    //        这样,就可能导致一个流程被审批不通过后,重新发起时,会直接后端报错!!!
+    const allowedFields = decodeFields(row.formFields).map((fieldObj: any) => fieldObj.field)
+    for (const key in formVariables) {
+      if (!allowedFields.includes(key)) {
+        delete formVariables[key]
+      }
+    }
+    setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
+    await nextTick()
+    fApi.value?.btn.show(false) // 隐藏提交按钮
+    // 获取流程审批信息
+    await getApprovalDetail(row)
+
+    // 加载流程图
+    const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id)
+    if (processDefinitionDetail) {
+      bpmnXML.value = processDefinitionDetail.bpmnXml
+      simpleJson.value = processDefinitionDetail.simpleModel
+    }
+    // 情况二:业务表单
+  } else if (row.formCustomCreatePath) {
+    await push({
+      path: row.formCustomCreatePath
+    })
+    // 这里暂时无需加载流程图,因为跳出到另外个 Tab;
+  }
+}
+
+/** 获取审批详情 */
+const getApprovalDetail = async (row: any) => {
+  try {
+    const data = await ProcessInstanceApi.getApprovalDetail({ processDefinitionId: row.id })
+    if (!data) {
+      message.error('查询不到审批详情信息!')
+      return
+    }
+
+    // 获取发起人自选的任务
+    startUserSelectTasks.value = data.activityNodes?.filter(
+      (node: ApprovalNodeInfo) => CandidateStrategy.START_USER_SELECT === node.candidateStrategy
+    )
+    if (startUserSelectTasks.value?.length > 0) {
+      for (const node of startUserSelectTasks.value) {
+        startUserSelectAssignees.value[node.id] = []
+      }
+    }
+
+    // 获取审批节点,显示 Timeline 的数据
+    activityNodes.value = data.activityNodes
+  } finally {
+  }
+}
+
+/** 提交按钮 */
+const submitForm = async () => {
+  if (!fApi.value || !props.selectProcessDefinition) {
+    return
+  }
+  // 如果有指定审批人,需要校验
+  if (startUserSelectTasks.value?.length > 0) {
+    for (const userTask of startUserSelectTasks.value) {
+      if (
+        Array.isArray(startUserSelectAssignees.value[userTask.id]) &&
+        startUserSelectAssignees.value[userTask.id].length === 0
+      )
+        return message.warning(`请选择${userTask.name}的候选人`)
+    }
+  }
+
+  // 提交请求
+  fApi.value.btn.loading(true)
+  try {
+    await ProcessInstanceApi.createProcessInstance({
+      processDefinitionId: props.selectProcessDefinition.id,
+      variables: detailForm.value.value,
+      startUserSelectAssignees: startUserSelectAssignees.value
+    })
+    // 提示
+    message.success('发起流程成功')
+    // 跳转回去
+    delView(unref(currentRoute))
+    await push({
+      name: 'BpmProcessInstanceMy'
+    })
+  } finally {
+    fApi.value.btn.loading(false)
+  }
+}
+
+/** 取消发起审批 */
+const handleCancel = () => {
+  emit('cancel')
+}
+
+/** 选择发起人 */
+const selectUserConfirm = (id: string, userList: any[]) => {
+  startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id)
+}
+
+defineExpose({ initProcessInfo })
+</script>
+
+<style lang="scss" scoped>
+$wrap-padding-height: 20px;
+$wrap-margin-height: 15px;
+$button-height: 51px;
+$process-header-height: 105px;
+
+.processInstance-wrap-main {
+  height: calc(
+    100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
+  );
+  max-height: calc(
+    100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
+  );
+  overflow: auto;
+
+  .form-scroll-area {
+    height: calc(
+      100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
+        $process-header-height - 40px
+    );
+    max-height: calc(
+      100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
+        $process-header-height - 40px
+    );
+    overflow: auto;
+  }
+}
+
+.form-box {
+  :deep(.el-card) {
+    border: none;
+  }
+}
+</style>

+ 232 - 190
src/views/bpm/processInstance/create/index.vue

@@ -1,133 +1,115 @@
 <template>
-  <doc-alert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" />
-
   <!-- 第一步,通过流程定义的列表,选择对应的流程 -->
-  <ContentWrap v-if="!selectProcessDefinition" v-loading="loading">
-    <el-tabs tab-position="left" v-model="categoryActive">
-      <el-tab-pane
-        :label="category.name"
-        :name="category.code"
-        :key="category.code"
-        v-for="category in categoryList"
-      >
-        <el-row :gutter="20">
-          <el-col
-            :lg="6"
-            :sm="12"
-            :xs="24"
-            v-for="definition in categoryProcessDefinitionList"
-            :key="definition.id"
-          >
-            <el-card
-              shadow="hover"
-              class="mb-20px cursor-pointer"
-              @click="handleSelect(definition)"
+  <template v-if="!selectProcessDefinition">
+    <el-input
+      v-model="searchName"
+      class="!w-50% mb-15px"
+      placeholder="请输入流程名称"
+      clearable
+      @input="handleQuery"
+      @clear="handleQuery"
+    >
+      <template #prefix>
+        <Icon icon="ep:search" />
+      </template>
+    </el-input>
+    <ContentWrap
+      :class="{ 'process-definition-container': filteredProcessDefinitionList?.length }"
+      class="position-relative pb-20px h-700px"
+      v-loading="loading"
+    >
+      <el-row v-if="filteredProcessDefinitionList?.length" :gutter="20" class="!flex-nowrap">
+        <el-col :span="5">
+          <div class="flex flex-col">
+            <div
+              v-for="category in availableCategories"
+              :key="category.code"
+              class="flex items-center p-10px cursor-pointer text-14px rounded-md"
+              :class="categoryActive.code === category.code ? 'text-#3e7bff bg-#e8eeff' : ''"
+              @click="handleCategoryClick(category)"
             >
-              <template #default>
-                <div class="flex">
-                  <el-image :src="definition.icon" class="w-32px h-32px" />
-                  <el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
-                </div>
-              </template>
-            </el-card>
-          </el-col>
-        </el-row>
-      </el-tab-pane>
-    </el-tabs>
-  </ContentWrap>
-
-  <!-- 第二步,填写表单,进行流程的提交 -->
-  <ContentWrap v-else>
-    <el-card class="box-card">
-      <div class="clearfix">
-        <span class="el-icon-document">申请信息【{{ selectProcessDefinition.name }}】</span>
-        <el-button style="float: right" type="primary" @click="selectProcessDefinition = undefined">
-          <Icon icon="ep:delete" /> 选择其它流程
-        </el-button>
-      </div>
-      <el-col :span="16" :offset="6" style="margin-top: 20px">
-        <form-create
-          :rule="detailForm.rule"
-          v-model:api="fApi"
-          v-model="detailForm.value"
-          :option="detailForm.option"
-          @submit="submitForm"
-        >
-          <template #type-startUserSelect>
-            <el-col :span="24">
-              <el-card class="mb-10px">
-                <template #header>指定审批人</template>
-                <el-form
-                  :model="startUserSelectAssignees"
-                  :rules="startUserSelectAssigneesFormRules"
-                  ref="startUserSelectAssigneesFormRef"
+              {{ category.name }}
+            </div>
+          </div>
+        </el-col>
+        <el-col :span="19">
+          <el-scrollbar ref="scrollWrapper" height="700" @scroll="handleScroll">
+            <div
+              class="mb-20px pl-10px"
+              v-for="(definitions, categoryCode) in processDefinitionGroup"
+              :key="categoryCode"
+              :ref="`category-${categoryCode}`"
+            >
+              <h3 class="text-18px font-bold mb-10px mt-5px">
+                {{ getCategoryName(categoryCode as any) }}
+              </h3>
+              <div class="grid grid-cols-3 gap3">
+                <el-tooltip
+                  v-for="definition in definitions"
+                  :key="definition.id"
+                  :content="definition.description"
+                  :disabled="!definition.description || definition.description.trim().length === 0"
+                  placement="top"
                 >
-                  <el-form-item
-                    v-for="userTask in startUserSelectTasks"
-                    :key="userTask.id"
-                    :label="`任务【${userTask.name}】`"
-                    :prop="userTask.id"
+                  <el-card
+                    shadow="hover"
+                    class="cursor-pointer definition-item-card"
+                    @click="handleSelect(definition)"
                   >
-                    <el-select
-                      v-model="startUserSelectAssignees[userTask.id]"
-                      multiple
-                      placeholder="请选择审批人"
-                    >
-                      <el-option
-                        v-for="user in userList"
-                        :key="user.id"
-                        :label="user.nickname"
-                        :value="user.id"
-                      />
-                    </el-select>
-                  </el-form-item>
-                </el-form>
-              </el-card>
-            </el-col>
-          </template>
-        </form-create>
-      </el-col>
-    </el-card>
-    <!-- 流程图预览 -->
-    <ProcessInstanceBpmnViewer :bpmn-xml="bpmnXML as any" />
-  </ContentWrap>
+                    <template #default>
+                      <div class="flex">
+                        <el-image :src="definition.icon" class="w-32px h-32px" />
+                        <el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
+                      </div>
+                    </template>
+                  </el-card>
+                </el-tooltip>
+              </div>
+            </div>
+          </el-scrollbar>
+        </el-col>
+      </el-row>
+      <el-empty class="!py-200px" :image-size="200" description="没有找到搜索结果" v-else />
+    </ContentWrap>
+  </template>
+
+  <!-- 第二步,填写表单,进行流程的提交 -->
+  <ProcessDefinitionDetail
+    v-else
+    ref="processDefinitionDetailRef"
+    :selectProcessDefinition="selectProcessDefinition"
+    @cancel="selectProcessDefinition = undefined"
+  />
 </template>
+
 <script lang="ts" setup>
 import * as DefinitionApi from '@/api/bpm/definition'
 import * as ProcessInstanceApi from '@/api/bpm/processInstance'
-import { setConfAndFields2 } from '@/utils/formCreate'
-import type { ApiAttrs } from '@form-create/element-ui/types/config'
-import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
-import { CategoryApi } from '@/api/bpm/category'
-import { useTagsViewStore } from '@/store/modules/tagsView'
-import * as UserApi from '@/api/system/user'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import ProcessDefinitionDetail from './ProcessDefinitionDetail.vue'
+import { groupBy } from 'lodash-es'
 
 defineOptions({ name: 'BpmProcessInstanceCreate' })
 
+const { proxy } = getCurrentInstance() as any
 const route = useRoute() // 路由
-const { push, currentRoute } = useRouter() // 路由
 const message = useMessage() // 消息
-const { delView } = useTagsViewStore() // 视图操作
 
-const processInstanceId = route.query.processInstanceId
+const searchName = ref('') // 当前搜索关键字
+const processInstanceId: any = route.query.processInstanceId // 流程实例编号。场景:重新发起时
 const loading = ref(true) // 加载中
-const categoryList = ref([]) // 分类的列表
-const categoryActive = ref('') // 选中的分类
+const categoryList: any = ref([]) // 分类的列表
+const categoryActive: any = ref({}) // 选中的分类
 const processDefinitionList = ref([]) // 流程定义的列表
 
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
-    // 流程分类
-    categoryList.value = await CategoryApi.getCategorySimpleList()
-    if (categoryList.value.length > 0) {
-      categoryActive.value = categoryList.value[0].code
-    }
-    // 流程定义
-    processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
-      suspensionState: 1
-    })
+    // 所有流程分类数据
+    await getCategoryList()
+    // 所有流程定义数据
+    await getProcessDefinitionList()
 
     // 如果 processInstanceId 非空,说明是重新发起
     if (processInstanceId?.length > 0) {
@@ -137,7 +119,7 @@ const getList = async () => {
         return
       }
       const processDefinition = processDefinitionList.value.find(
-        (item) => item.key == processInstance.processDefinition?.key
+        (item: any) => item.key == processInstance.processDefinition?.key
       )
       if (!processDefinition) {
         message.error('重新发起流程失败,原因:流程定义不存在')
@@ -150,108 +132,168 @@ const getList = async () => {
   }
 }
 
-/** 选中分类对应的流程定义列表 */
-const categoryProcessDefinitionList = computed(() => {
-  return processDefinitionList.value.filter((item) => item.category == categoryActive.value)
+/** 获取所有流程分类数据 */
+const getCategoryList = async () => {
+  try {
+    // 流程分类
+    categoryList.value = await CategoryApi.getCategorySimpleList()
+  } finally {
+  }
+}
+
+/** 获取所有流程定义数据 */
+const getProcessDefinitionList = async () => {
+  try {
+    // 流程定义
+    processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
+      suspensionState: 1
+    })
+    // 初始化过滤列表为全部流程定义
+    filteredProcessDefinitionList.value = processDefinitionList.value
+
+    // 在获取完所有数据后,设置第一个有效分类为激活状态
+    if (availableCategories.value.length > 0 && !categoryActive.value?.code) {
+      categoryActive.value = availableCategories.value[0]
+    }
+  } finally {
+  }
+}
+
+/** 搜索流程 */
+const filteredProcessDefinitionList = ref([]) // 用于存储搜索过滤后的流程定义
+const handleQuery = () => {
+  if (searchName.value.trim()) {
+    // 如果有搜索关键字,进行过滤
+    filteredProcessDefinitionList.value = processDefinitionList.value.filter(
+      (definition: any) => definition.name.toLowerCase().includes(searchName.value.toLowerCase()) // 假设搜索依据是流程定义的名称
+    )
+  } else {
+    // 如果没有搜索关键字,恢复所有数据
+    filteredProcessDefinitionList.value = processDefinitionList.value
+  }
+}
+
+/** 流程定义的分组 */
+const processDefinitionGroup: any = computed(() => {
+  if (!processDefinitionList.value?.length) {
+    return {}
+  }
+
+  const grouped = groupBy(filteredProcessDefinitionList.value, 'category')
+  // 按照 categoryList 的顺序重新组织数据
+  const orderedGroup = {}
+  categoryList.value.forEach((category: any) => {
+    if (grouped[category.code]) {
+      orderedGroup[category.code] = grouped[category.code]
+    }
+  })
+  return orderedGroup
 })
 
+/** 左侧分类切换 */
+const handleCategoryClick = (category: any) => {
+  categoryActive.value = category
+  const categoryRef = proxy.$refs[`category-${category.code}`] // 获取点击分类对应的 DOM 元素
+  if (categoryRef?.length) {
+    const scrollWrapper = proxy.$refs.scrollWrapper // 获取右侧滚动容器
+    const categoryOffsetTop = categoryRef[0].offsetTop
+
+    // 滚动到对应位置
+    scrollWrapper.scrollTo({ top: categoryOffsetTop, behavior: 'smooth' })
+  }
+}
+
+/** 通过分类 code 获取对应的名称 */
+const getCategoryName = (categoryCode: string) => {
+  return categoryList.value?.find((ctg: any) => ctg.code === categoryCode)?.name
+}
+
 // ========== 表单相关 ==========
-const fApi = ref<ApiAttrs>()
-const detailForm = ref({
-  rule: [],
-  option: {},
-  value: {}
-}) // 流程表单详情
 const selectProcessDefinition = ref() // 选择的流程定义
-
-// 指定审批人
-const bpmnXML = ref(null) // BPMN 数据
-const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表
-const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
-const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref
-const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules
-const userList = ref<any[]>([]) // 用户列表
+const processDefinitionDetailRef = ref()
 
 /** 处理选择流程的按钮操作 **/
-const handleSelect = async (row, formVariables) => {
+const handleSelect = async (row, formVariables?) => {
   // 设置选择的流程
   selectProcessDefinition.value = row
+  // 初始化流程定义详情
+  await nextTick()
+  processDefinitionDetailRef.value?.initProcessInfo(row, formVariables)
+}
 
-  // 重置指定审批人
-  startUserSelectTasks.value = []
-  startUserSelectAssignees.value = {}
-  startUserSelectAssigneesFormRules.value = {}
-
-  // 情况一:流程表单
-  if (row.formType == 10) {
-    // 设置表单
-    setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
-    // 加载流程图
-    const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id)
-    if (processDefinitionDetail) {
-      bpmnXML.value = processDefinitionDetail.bpmnXml
-      startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
+/** 处理滚动事件,和左侧分类联动 */
+const handleScroll = (e: any) => {
+  // 直接使用事件对象获取滚动位置
+  const scrollTop = e.scrollTop
 
-      // 设置指定审批人
-      if (startUserSelectTasks.value?.length > 0) {
-        detailForm.value.rule.push({
-          type: 'startUserSelect',
-          props: {
-            title: '指定审批人'
-          }
-        })
-        // 设置校验规则
-        for (const userTask of startUserSelectTasks.value) {
-          startUserSelectAssignees.value[userTask.id] = []
-          startUserSelectAssigneesFormRules.value[userTask.id] = [
-            { required: true, message: '请选择审批人', trigger: 'blur' }
-          ]
+  // 获取所有分类区域的位置信息
+  const categoryPositions = categoryList.value
+    .map((category: CategoryVO) => {
+      const categoryRef = proxy.$refs[`category-${category.code}`]
+      if (categoryRef?.[0]) {
+        return {
+          code: category.code,
+          offsetTop: categoryRef[0].offsetTop,
+          height: categoryRef[0].offsetHeight
         }
-        // 加载用户列表
-        userList.value = await UserApi.getSimpleUserList()
       }
-    }
-    // 情况二:业务表单
-  } else if (row.formCustomCreatePath) {
-    await push({
-      path: row.formCustomCreatePath
+      return null
     })
-    // 这里暂时无需加载流程图,因为跳出到另外个 Tab;
-  }
-}
+    .filter(Boolean)
 
-/** 提交按钮 */
-const submitForm = async (formData) => {
-  if (!fApi.value || !selectProcessDefinition.value) {
-    return
-  }
-  // 如果有指定审批人,需要校验
-  if (startUserSelectTasks.value?.length > 0) {
-    await startUserSelectAssigneesFormRef.value.validate()
+  // 查找当前滚动位置对应的分类
+  let currentCategory = categoryPositions[0]
+  for (const position of categoryPositions) {
+    // 为了更好的用户体验,可以添加一个缓冲区域(比如 50px)
+    if (scrollTop >= position.offsetTop - 50) {
+      currentCategory = position
+    } else {
+      break
+    }
   }
 
-  // 提交请求
-  fApi.value.btn.loading(true)
-  try {
-    await ProcessInstanceApi.createProcessInstance({
-      processDefinitionId: selectProcessDefinition.value.id,
-      variables: formData,
-      startUserSelectAssignees: startUserSelectAssignees.value
-    })
-    // 提示
-    message.success('发起流程成功')
-    // 跳转回去
-    delView(unref(currentRoute))
-    await push({
-      name: 'BpmProcessInstanceMy'
-    })
-  } finally {
-    fApi.value.btn.loading(false)
+  // 更新当前 active 的分类
+  if (currentCategory && categoryActive.value.code !== currentCategory.code) {
+    categoryActive.value = categoryList.value.find(
+      (c: CategoryVO) => c.code === currentCategory.code
+    )
   }
 }
 
+/** 过滤出有流程的分类列表。目的:只展示有流程的分类 */
+const availableCategories = computed(() => {
+  if (!categoryList.value?.length || !processDefinitionGroup.value) {
+    return []
+  }
+
+  // 获取所有有流程的分类代码
+  const availableCategoryCodes = Object.keys(processDefinitionGroup.value)
+
+  // 过滤出有流程的分类
+  return categoryList.value.filter((category: CategoryVO) =>
+    availableCategoryCodes.includes(category.code)
+  )
+})
+
 /** 初始化 */
 onMounted(() => {
   getList()
 })
 </script>
+
+<style lang="scss" scoped>
+.process-definition-container::before {
+  content: '';
+  border-left: 1px solid #e6e6e6;
+  position: absolute;
+  left: 20.8%;
+  height: 100%;
+}
+:deep() {
+  .definition-item-card {
+    .el-card__body {
+      padding: 14px;
+    }
+  }
+}
+</style>

+ 267 - 0
src/views/bpm/processInstance/create/index_old.vue

@@ -0,0 +1,267 @@
+<template>
+  <doc-alert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" />
+
+  <!-- 第一步,通过流程定义的列表,选择对应的流程 -->
+  <ContentWrap v-if="!selectProcessDefinition" v-loading="loading">
+    <el-tabs tab-position="left" v-model="categoryActive">
+      <el-tab-pane
+        :label="category.name"
+        :name="category.code"
+        :key="category.code"
+        v-for="category in categoryList"
+      >
+        <el-row :gutter="20">
+          <el-col
+            :lg="6"
+            :sm="12"
+            :xs="24"
+            v-for="definition in categoryProcessDefinitionList"
+            :key="definition.id"
+          >
+            <el-card
+              shadow="hover"
+              class="mb-20px cursor-pointer"
+              @click="handleSelect(definition)"
+            >
+              <template #default>
+                <div class="flex">
+                  <el-image :src="definition.icon" class="w-32px h-32px" />
+                  <el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
+                </div>
+              </template>
+            </el-card>
+          </el-col>
+        </el-row>
+      </el-tab-pane>
+    </el-tabs>
+  </ContentWrap>
+
+  <!-- 第二步,填写表单,进行流程的提交 -->
+  <ContentWrap v-else>
+    <el-card class="box-card">
+      <div class="clearfix">
+        <span class="el-icon-document">申请信息【{{ selectProcessDefinition.name }}】</span>
+        <el-button style="float: right" type="primary" @click="selectProcessDefinition = undefined">
+          <Icon icon="ep:delete" /> 选择其它流程
+        </el-button>
+      </div>
+      <el-col :span="16" :offset="6" style="margin-top: 20px">
+        <form-create
+          :rule="detailForm.rule"
+          v-model:api="fApi"
+          v-model="detailForm.value"
+          :option="detailForm.option"
+          @submit="submitForm"
+        >
+          <template #type-startUserSelect>
+            <el-col :span="24">
+              <el-card class="mb-10px">
+                <template #header>指定审批人</template>
+                <el-form
+                  :model="startUserSelectAssignees"
+                  :rules="startUserSelectAssigneesFormRules"
+                  ref="startUserSelectAssigneesFormRef"
+                >
+                  <el-form-item
+                    v-for="userTask in startUserSelectTasks"
+                    :key="userTask.id"
+                    :label="`任务【${userTask.name}】`"
+                    :prop="userTask.id"
+                  >
+                    <el-select
+                      v-model="startUserSelectAssignees[userTask.id]"
+                      multiple
+                      placeholder="请选择审批人"
+                    >
+                      <el-option
+                        v-for="user in userList"
+                        :key="user.id"
+                        :label="user.nickname"
+                        :value="user.id"
+                      />
+                    </el-select>
+                  </el-form-item>
+                </el-form>
+              </el-card>
+            </el-col>
+          </template>
+        </form-create>
+      </el-col>
+    </el-card>
+    <!-- 流程图预览 -->
+    <ProcessInstanceBpmnViewer :bpmn-xml="bpmnXML as any" />
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as DefinitionApi from '@/api/bpm/definition'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import { decodeFields, setConfAndFields2 } from '@/utils/formCreate'
+import type { ApiAttrs } from '@form-create/element-ui/types/config'
+import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
+import { CategoryApi } from '@/api/bpm/category'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import * as UserApi from '@/api/system/user'
+
+defineOptions({ name: 'BpmProcessInstanceCreate' })
+
+const route = useRoute() // 路由
+const { push, currentRoute } = useRouter() // 路由
+const message = useMessage() // 消息
+const { delView } = useTagsViewStore() // 视图操作
+
+const processInstanceId = route.query.processInstanceId
+const loading = ref(true) // 加载中
+const categoryList = ref([]) // 分类的列表
+const categoryActive = ref('') // 选中的分类
+const processDefinitionList = ref([]) // 流程定义的列表
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    // 流程分类
+    categoryList.value = await CategoryApi.getCategorySimpleList()
+    if (categoryList.value.length > 0) {
+      categoryActive.value = categoryList.value[0].code
+    }
+    // 流程定义
+    processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
+      suspensionState: 1
+    })
+
+    // 如果 processInstanceId 非空,说明是重新发起
+    if (processInstanceId?.length > 0) {
+      const processInstance = await ProcessInstanceApi.getProcessInstance(processInstanceId)
+      if (!processInstance) {
+        message.error('重新发起流程失败,原因:流程实例不存在')
+        return
+      }
+      const processDefinition = processDefinitionList.value.find(
+        (item) => item.key == processInstance.processDefinition?.key
+      )
+      if (!processDefinition) {
+        message.error('重新发起流程失败,原因:流程定义不存在')
+        return
+      }
+      await handleSelect(processDefinition, processInstance.formVariables)
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 选中分类对应的流程定义列表 */
+const categoryProcessDefinitionList = computed(() => {
+  return processDefinitionList.value.filter((item) => item.category == categoryActive.value)
+})
+
+// ========== 表单相关 ==========
+const fApi = ref<ApiAttrs>()
+const detailForm = ref({
+  rule: [],
+  option: {},
+  value: {}
+}) // 流程表单详情
+const selectProcessDefinition = ref() // 选择的流程定义
+
+// 指定审批人
+const bpmnXML = ref(null) // BPMN 数据
+const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表
+const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
+const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref
+const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules
+const userList = ref<any[]>([]) // 用户列表
+
+/** 处理选择流程的按钮操作 **/
+const handleSelect = async (row, formVariables) => {
+  // 设置选择的流程
+  selectProcessDefinition.value = row
+
+  // 重置指定审批人
+  startUserSelectTasks.value = []
+  startUserSelectAssignees.value = {}
+  startUserSelectAssigneesFormRules.value = {}
+
+  // 情况一:流程表单
+  if (row.formType == 10) {
+    // 设置表单
+    // 注意:需要从 formVariables 中,移除不在 row.formFields 的值。
+    // 原因是:后端返回的 formVariables 里面,会有一些非表单的信息。例如说,某个流程节点的审批人。
+    //        这样,就可能导致一个流程被审批不通过后,重新发起时,会直接后端报错!!!
+    const allowedFields = decodeFields(row.formFields).map((fieldObj: any) => fieldObj.field)
+    for (const key in formVariables) {
+      if (!allowedFields.includes(key)) {
+        delete formVariables[key]
+      }
+    }
+    setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
+
+    // 加载流程图
+    const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id)
+    if (processDefinitionDetail) {
+      bpmnXML.value = processDefinitionDetail.bpmnXml
+      startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
+
+      // 设置指定审批人
+      if (startUserSelectTasks.value?.length > 0) {
+        detailForm.value.rule.push({
+          type: 'startUserSelect',
+          props: {
+            title: '指定审批人'
+          }
+        })
+        // 设置校验规则
+        for (const userTask of startUserSelectTasks.value) {
+          startUserSelectAssignees.value[userTask.id] = []
+          startUserSelectAssigneesFormRules.value[userTask.id] = [
+            { required: true, message: '请选择审批人', trigger: 'blur' }
+          ]
+        }
+        // 加载用户列表
+        userList.value = await UserApi.getSimpleUserList()
+      }
+    }
+    // 情况二:业务表单
+  } else if (row.formCustomCreatePath) {
+    await push({
+      path: row.formCustomCreatePath
+    })
+    // 这里暂时无需加载流程图,因为跳出到另外个 Tab;
+  }
+}
+
+/** 提交按钮 */
+const submitForm = async (formData) => {
+  if (!fApi.value || !selectProcessDefinition.value) {
+    return
+  }
+  // 如果有指定审批人,需要校验
+  if (startUserSelectTasks.value?.length > 0) {
+    await startUserSelectAssigneesFormRef.value.validate()
+  }
+
+  // 提交请求
+  fApi.value.btn.loading(true)
+  try {
+    await ProcessInstanceApi.createProcessInstance({
+      processDefinitionId: selectProcessDefinition.value.id,
+      variables: formData,
+      startUserSelectAssignees: startUserSelectAssignees.value
+    })
+    // 提示
+    message.success('发起流程成功')
+    // 跳转回去
+    delView(unref(currentRoute))
+    await push({
+      name: 'BpmProcessInstanceMy'
+    })
+  } finally {
+    fApi.value.btn.loading(false)
+  }
+}
+
+/** 初始化 */
+onMounted(() => {
+  getList()
+})
+</script>

+ 37 - 30
src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue

@@ -1,54 +1,61 @@
 <template>
   <el-card v-loading="loading" class="box-card">
-    <template #header>
-      <span class="el-icon-picture-outline">流程图</span>
-    </template>
-    <MyProcessViewer
-      key="designer"
-      :activityData="activityList"
-      :prefix="bpmnControlForm.prefix"
-      :processInstanceData="processInstance"
-      :taskData="tasks"
-      :value="bpmnXml"
-      v-bind="bpmnControlForm"
-    />
+    <MyProcessViewer key="designer" :xml="view.bpmnXml" :view="view" class="process-viewer" />
   </el-card>
 </template>
 <script lang="ts" setup>
 import { propTypes } from '@/utils/propTypes'
 import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
-import * as ActivityApi from '@/api/bpm/activity'
 
 defineOptions({ name: 'BpmProcessInstanceBpmnViewer' })
 
 const props = defineProps({
-  loading: propTypes.bool, // 是否加载中
-  id: propTypes.string, // 流程实例的编号
-  processInstance: propTypes.any, // 流程实例的信息
-  tasks: propTypes.array, // 流程任务的数组
-  bpmnXml: propTypes.string // BPMN XML
+  loading: propTypes.bool.def(false), // 是否加载中
+  bpmnXml: propTypes.string, // BPMN XML
+  modelView: propTypes.object
 })
 
-const bpmnControlForm = ref({
-  prefix: 'flowable'
-})
-const activityList = ref([]) // 任务列表
+const view = ref({
+  bpmnXml: ''
+}) // BPMN 流程图数据
+
 
 /** 只有 loading 完成时,才去加载流程列表 */
 watch(
-  () => props.loading,
-  async (value) => {
-    if (value && props.id) {
-      activityList.value = await ActivityApi.getActivityList({
-        processInstanceId: props.id
-      })
+  () => props.modelView,
+  async (newModelView) => {
+    // 加载最新
+    if (newModelView) {
+      //@ts-ignore
+      view.value = newModelView
     }
   }
 )
+
+/** 监听 bpmnXml */
+watch(
+  () => props.bpmnXml,
+  (value) => {
+    view.value.bpmnXml = value
+  }
+)
 </script>
-<style>
+<style lang="scss" scoped>
 .box-card {
+  height: 100%;
   width: 100%;
-  margin-bottom: 20px;
+  margin-bottom: 0;
+
+  :deep(.el-card__body) {
+    height: 100%;
+    padding: 0;
+  }
+
+  :deep(.process-viewer) {
+    height: 100% !important;
+    min-height: 100%;
+    width: 100%;
+    overflow: auto;
+  }
 }
 </style>

Fișier diff suprimat deoarece este prea mare
+ 683 - 172
src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue


+ 168 - 0
src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue

@@ -0,0 +1,168 @@
+<template>
+  <div v-loading="loading" class="process-viewer-container">
+    <SimpleProcessViewer
+      :flow-node="simpleModel"
+      :tasks="tasks"
+      :process-instance="processInstance"
+      class="process-viewer"
+    />
+  </div>
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { TaskStatusEnum } from '@/api/bpm/task'
+import { SimpleFlowNode, NodeType } from '@/components/SimpleProcessDesignerV2/src/consts'
+import { SimpleProcessViewer } from '@/components/SimpleProcessDesignerV2/src/'
+defineOptions({ name: 'BpmProcessInstanceSimpleViewer' })
+
+const props = defineProps({
+  loading: propTypes.bool.def(false), // 是否加载中
+  modelView: propTypes.object,
+  simpleJson: propTypes.string // Simple 模型结构数据 (json 格式)
+})
+const simpleModel = ref()
+// 用户任务
+const tasks = ref([])
+// 流程实例
+const processInstance = ref()
+
+/** 监控模型视图 包括任务列表、进行中的活动节点编号等 */
+watch(
+  () => props.modelView,
+  async (newModelView) => {
+    if (newModelView) {
+      tasks.value = newModelView.tasks
+      processInstance.value = newModelView.processInstance
+      // 已经拒绝的活动节点编号集合,只包括 UserTask
+      const rejectedTaskActivityIds: string[] = newModelView.rejectedTaskActivityIds
+      // 进行中的活动节点编号集合, 只包括 UserTask
+      const unfinishedTaskActivityIds: string[] = newModelView.unfinishedTaskActivityIds
+      // 已经完成的活动节点编号集合, 包括 UserTask、Gateway 等
+      const finishedActivityIds: string[] = newModelView.finishedTaskActivityIds
+      // 已经完成的连线节点编号集合,只包括 SequenceFlow
+      const finishedSequenceFlowActivityIds: string[] = newModelView.finishedSequenceFlowActivityIds
+      setSimpleModelNodeTaskStatus(
+        newModelView.simpleModel,
+        newModelView.processInstance.status,
+        rejectedTaskActivityIds,
+        unfinishedTaskActivityIds,
+        finishedActivityIds,
+        finishedSequenceFlowActivityIds
+      )
+      simpleModel.value = newModelView.simpleModel
+    }
+  }
+)
+/** 监控模型结构数据 */
+watch(
+  () => props.simpleJson,
+  async (value) => {
+    if (value) {
+      simpleModel.value = JSON.parse(value)
+    }
+  }
+)
+const setSimpleModelNodeTaskStatus = (
+  simpleModel: SimpleFlowNode | undefined,
+  processStatus: number,
+  rejectedTaskActivityIds: string[],
+  unfinishedTaskActivityIds: string[],
+  finishedActivityIds: string[],
+  finishedSequenceFlowActivityIds: string[]
+) => {
+  if (!simpleModel) {
+    return
+  }
+  // 结束节点
+  if (simpleModel.type === NodeType.END_EVENT_NODE) {
+    if (finishedActivityIds.includes(simpleModel.id)) {
+      simpleModel.activityStatus = processStatus
+    } else {
+      simpleModel.activityStatus = TaskStatusEnum.NOT_START
+    }
+    return
+  }
+
+  // 审批节点
+  if (
+    simpleModel.type === NodeType.START_USER_NODE ||
+    simpleModel.type === NodeType.USER_TASK_NODE
+  ) {
+    simpleModel.activityStatus = TaskStatusEnum.NOT_START
+    if (rejectedTaskActivityIds.includes(simpleModel.id)) {
+      simpleModel.activityStatus = TaskStatusEnum.REJECT
+    } else if (unfinishedTaskActivityIds.includes(simpleModel.id)) {
+      simpleModel.activityStatus = TaskStatusEnum.RUNNING
+    } else if (finishedActivityIds.includes(simpleModel.id)) {
+      simpleModel.activityStatus = TaskStatusEnum.APPROVE
+    }
+    // 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.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
+  ) {
+    // 网关节点。只有通过和未执行状态
+    if (finishedActivityIds.includes(simpleModel.id)) {
+      simpleModel.activityStatus = TaskStatusEnum.APPROVE
+    } else {
+      simpleModel.activityStatus = TaskStatusEnum.NOT_START
+    }
+    simpleModel.conditionNodes?.forEach((node) => {
+      setSimpleModelNodeTaskStatus(
+        node,
+        processStatus,
+        rejectedTaskActivityIds,
+        unfinishedTaskActivityIds,
+        finishedActivityIds,
+        finishedSequenceFlowActivityIds
+      )
+    })
+  }
+
+  setSimpleModelNodeTaskStatus(
+    simpleModel.childNode,
+    processStatus,
+    rejectedTaskActivityIds,
+    unfinishedTaskActivityIds,
+    finishedActivityIds,
+    finishedSequenceFlowActivityIds
+  )
+}
+</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>

+ 61 - 133
src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue

@@ -1,85 +1,50 @@
 <template>
-  <el-card v-loading="loading" class="box-card">
-    <template #header>
-      <span class="el-icon-picture-outline">审批记录</span>
-    </template>
-    <el-col :offset="3" :span="17">
-      <div class="block">
-        <el-timeline>
-          <el-timeline-item
-            v-if="processInstance.endTime"
-            :type="getProcessInstanceTimelineItemType(processInstance)"
-          >
-            <p style="font-weight: 700">
-              结束流程:在 {{ formatDate(processInstance?.endTime) }} 结束
-              <dict-tag
-                :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS"
-                :value="processInstance.status"
-              />
-            </p>
-          </el-timeline-item>
-          <el-timeline-item
-            v-for="(item, index) in tasks"
-            :key="index"
-            :type="getTaskTimelineItemType(item)"
-          >
-            <p style="font-weight: 700">
-              审批任务:{{ item.name }}
-              <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="item.status" />
-              <el-button
-                class="ml-10px"
-                v-if="!isEmpty(item.children)"
-                @click="openChildrenTask(item)"
-                size="small"
-              >
-                <Icon icon="ep:memo" /> 子任务
-              </el-button>
-              <el-button
-                class="ml-10px"
-                size="small"
-                v-if="item.formId > 0"
-                @click="handleFormDetail(item)"
-              >
-                <Icon icon="ep:document" /> 查看表单
-              </el-button>
-            </p>
-            <el-card :body-style="{ padding: '10px' }">
-              <label v-if="item.assigneeUser" style="margin-right: 30px; font-weight: normal">
-                审批人:{{ item.assigneeUser.nickname }}
-                <el-tag size="small" type="info">{{ item.assigneeUser.deptName }}</el-tag>
-              </label>
-              <label v-if="item.createTime" style="font-weight: normal">创建时间:</label>
-              <label style="font-weight: normal; color: #8a909c">
-                {{ formatDate(item?.createTime) }}
-              </label>
-              <label v-if="item.endTime" style="margin-left: 30px; font-weight: normal">
-                审批时间:
-              </label>
-              <label v-if="item.endTime" style="font-weight: normal; color: #8a909c">
-                {{ formatDate(item?.endTime) }}
-              </label>
-              <label v-if="item.durationInMillis" style="margin-left: 30px; font-weight: normal">
-                耗时:
-              </label>
-              <label v-if="item.durationInMillis" style="font-weight: normal; color: #8a909c">
-                {{ formatPast2(item?.durationInMillis) }}
-              </label>
-              <p v-if="item.reason"> 审批建议:{{ item.reason }} </p>
-            </el-card>
-          </el-timeline-item>
-          <el-timeline-item type="success">
-            <p style="font-weight: 700">
-              发起流程:【{{ processInstance.startUser?.nickname }}】在
-              {{ formatDate(processInstance?.startTime) }} 发起【 {{ processInstance.name }} 】流程
-            </p>
-          </el-timeline-item>
-        </el-timeline>
-      </div>
-    </el-col>
-  </el-card>
+  <el-table :data="tasks" border header-cell-class-name="table-header-gray">
+    <el-table-column label="审批节点" prop="name" min-width="120" align="center" />
+    <el-table-column label="审批人" min-width="100" align="center">
+      <template #default="scope">
+        {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
+      </template>
+    </el-table-column>
+    <el-table-column
+      :formatter="dateFormatter"
+      align="center"
+      label="开始时间"
+      prop="createTime"
+      min-width="140"
+    />
+    <el-table-column
+      :formatter="dateFormatter"
+      align="center"
+      label="结束时间"
+      prop="endTime"
+      min-width="140"
+    />
+    <el-table-column align="center" label="审批状态" prop="status" min-width="90">
+      <template #default="scope">
+        <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="审批建议" prop="reason" min-width="200">
+      <template #default="scope">
+        {{ scope.row.reason }}
+        <el-button
+          class="ml-10px"
+          size="small"
+          v-if="scope.row.formId > 0"
+          @click="handleFormDetail(scope.row)"
+        >
+          <Icon icon="ep:document" /> 查看表单
+        </el-button>
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="耗时" prop="durationInMillis" min-width="100">
+      <template #default="scope">
+        {{ formatPast2(scope.row.durationInMillis) }}
+      </template>
+    </el-table-column>
+  </el-table>
 
-  <!-- 弹窗:子任务  -->
-  <TaskSignList ref="taskSignListRef" @success="refresh" />
   <!-- 弹窗:表单 -->
   <Dialog title="表单详情" v-model="taskFormVisible" width="600">
     <form-create
@@ -91,61 +56,20 @@
   </Dialog>
 </template>
 <script lang="ts" setup>
-import { formatDate, formatPast2 } from '@/utils/formatTime'
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
 import { propTypes } from '@/utils/propTypes'
 import { DICT_TYPE } from '@/utils/dict'
-import { isEmpty } from '@/utils/is'
-import TaskSignList from './dialog/TaskSignList.vue'
 import type { ApiAttrs } from '@form-create/element-ui/types/config'
 import { setConfAndFields2 } from '@/utils/formCreate'
+import * as TaskApi from '@/api/bpm/task'
 
 defineOptions({ name: 'BpmProcessInstanceTaskList' })
 
-defineProps({
-  loading: propTypes.bool, // 是否加载中
-  processInstance: propTypes.object, // 流程实例
-  tasks: propTypes.arrayOf(propTypes.object) // 流程任务的数组
+const props = defineProps({
+  loading: propTypes.bool.def(false), // 是否加载中
+  id: propTypes.string // 流程实例的编号
 })
-
-/** 获得流程实例对应的颜色 */
-const getProcessInstanceTimelineItemType = (item: any) => {
-  if (item.status === 2) {
-    return 'success'
-  }
-  if (item.status === 3) {
-    return 'danger'
-  }
-  if (item.status === 4) {
-    return 'warning'
-  }
-  return ''
-}
-
-/** 获得任务对应的颜色 */
-const getTaskTimelineItemType = (item: any) => {
-  if ([0, 1, 6, 7].includes(item.status)) {
-    return 'primary'
-  }
-  if (item.status === 2) {
-    return 'success'
-  }
-  if (item.status === 3) {
-    return 'danger'
-  }
-  if (item.status === 4) {
-    return 'info'
-  }
-  if (item.status === 5) {
-    return 'warning'
-  }
-  return ''
-}
-
-/** 子任务 */
-const taskSignListRef = ref()
-const openChildrenTask = (item: any) => {
-  taskSignListRef.value.open(item)
-}
+const tasks = ref([]) // 流程任务的数组
 
 /** 查看表单 */
 const fApi = ref<ApiAttrs>() // form-create 的 API 操作类
@@ -155,7 +79,7 @@ const taskForm = ref({
   value: {}
 }) // 流程任务的表单详情
 const taskFormVisible = ref(false)
-const handleFormDetail = async (row) => {
+const handleFormDetail = async (row: any) => {
   // 设置表单
   setConfAndFields2(taskForm, row.formConf, row.formFields, row.formVariables)
   // 弹窗打开
@@ -167,9 +91,13 @@ const handleFormDetail = async (row) => {
   fApi.value?.fapi?.disabled(true)
 }
 
-/** 刷新数据 */
-const emit = defineEmits(['refresh']) // 定义 success 事件,用于操作成功后的回调
-const refresh = () => {
-  emit('refresh')
-}
+/** 只有 loading 完成时,才去加载流程列表 */
+watch(
+  () => props.loading,
+  async (value) => {
+    if (value) {
+      tasks.value = await TaskApi.getTaskListByProcessInstanceId(props.id)
+    }
+  }
+)
 </script>

+ 194 - 135
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue

@@ -3,155 +3,189 @@
   <el-timeline class="pt-20px">
     <!-- 遍历每个审批节点 -->
     <el-timeline-item
-      v-for="(activity, index) in approveNodes"
+      v-for="(activity, index) in activityNodes"
       :key="index"
       size="large"
       :icon="getApprovalNodeIcon(activity.status, activity.nodeType)"
       :color="getApprovalNodeColor(activity.status)"
     >
-      <div class="flex flex-col items-start">
-        <div class="font-bold"> {{ activity.name }}</div>
-        <div class="flex items-center mt-1">
-          <!-- 情况一:遍历每个审批节点下的【进行中】task 任务 -->
-          <div v-for="(task, idx) in activity.tasks" :key="idx" class="flex items-center">
-            <div class="flex items-center flex-col pr-2">
-              <div class="position-relative" v-if="task.assigneeUser || task.ownerUser">
-                <!-- 信息:头像 -->
-                <el-avatar
-                  :size="36"
-                  v-if="task.assigneeUser && task.assigneeUser.avatar"
-                  :src="task.assigneeUser.avatar"
-                />
-                <el-avatar v-else-if="task.assigneeUser && task.assigneeUser.nickname">
-                  {{ task.assigneeUser.nickname.substring(0, 1) }}
-                </el-avatar>
-                <el-avatar
-                  v-else-if="task.ownerUser && task.ownerUser.avatar"
-                  :src="task.ownerUser.avatar"
-                />
-                <el-avatar v-else-if="task.ownerUser && task.ownerUser.nickname">
-                  {{ task.ownerUser.nickname.substring(0, 1) }}
-                </el-avatar>
-                <!-- 信息:任务 ICON -->
-                <div
-                  class="position-absolute top-26px left-26px bg-#fff rounded-full flex items-center p-2px"
-                >
-                  <Icon
-                    :size="12"
-                    :icon="statusIconMap2[task.status]?.icon"
-                    :color="statusIconMap2[task.status]?.color"
-                  />
-                </div>
-              </div>
-              <div class="flex flex-col mt-1">
-                <!-- 信息:昵称 -->
-                <div
-                  v-if="task.assigneeUser && task.assigneeUser.nickname"
-                  class="text-10px text-align-center"
-                >
-                  {{ task.assigneeUser.nickname }}
-                </div>
-                <div
-                  v-else-if="task.ownerUser && task.ownerUser.nickname"
-                  class="text-10px text-align-center"
-                >
-                  {{ task.ownerUser.nickname }}
-                </div>
-                <!-- TODO @jason:审批意见,要展示哈。 -->
-                <!-- <div v-if="task.reason" :title="task.reason" class="text-13px text-truncate w-150px mt-1"> 审批意见: {{ task.reason }}</div> -->
-              </div>
-            </div>
+      <template #dot>
+        <div
+          class="position-absolute left--10px top--6px rounded-full border border-solid border-#dedede w-30px h-30px flex justify-center items-center bg-#3f73f7 p-5px"
+        >
+          <img class="w-full h-full" :src="getApprovalNodeImg(activity.nodeType)" alt="" />
+          <div
+            v-if="showStatusIcon"
+            class="position-absolute top-17px left-17px rounded-full flex items-center p-1px border-2 border-white border-solid"
+            :style="{ backgroundColor: getApprovalNodeColor(activity.status) }"
+          >
+            <el-icon :size="11" color="#fff">
+              <component :is="getApprovalNodeIcon(activity.status, activity.nodeType)" />
+            </el-icon>
           </div>
-          <!-- 情况二:遍历每个审批节点下的【候选的】task 任务。例如说,1)依次审批,2)未来的审批任务等 -->
+        </div>
+      </template>
+      <div class="flex flex-col items-start gap2" :id="`activity-task-${activity.id}`">
+        <!-- 第一行:节点名称、时间 -->
+        <div class="flex w-full">
+          <div class="font-bold"> {{ activity.name }}</div>
+          <!-- 信息:时间 -->
+          <div
+            v-if="activity.status !== TaskStatusEnum.NOT_START"
+            class="text-#a5a5a5 text-13px mt-1 ml-auto"
+          >
+            {{ getApprovalNodeTime(activity) }}
+          </div>
+        </div>
+        <!-- 需要自定义选择审批人 -->
+        <div
+          class="flex flex-wrap gap2 items-center"
+          v-if="
+            isEmpty(activity.tasks) &&
+            isEmpty(activity.candidateUsers) &&
+            CandidateStrategy.START_USER_SELECT === activity.candidateStrategy
+          "
+        >
+          <!--  && activity.nodeType === NodeType.USER_TASK_NODE -->
+
+          <el-tooltip content="添加用户" placement="left">
+            <el-button
+              class="!px-6px"
+              @click="handleSelectUser(activity.id, customApproveUsers[activity.id])"
+            >
+              <img class="w-18px text-#ccc" src="@/assets/svgs/bpm/add-user.svg" alt="" />
+            </el-button>
+          </el-tooltip>
           <div
-            v-for="(user, idx1) in activity.candidateUserList"
+            v-for="(user, idx1) in customApproveUsers[activity.id]"
             :key="idx1"
-            class="flex items-center"
+            class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
           >
-            <div class="flex items-center flex-col pr-2">
-              <div class="position-relative">
-                <!-- 信息:头像 -->
-                <el-avatar :size="36" v-if="user.avatar" :src="user.avatar" />
-                <el-avatar v-else-if="user.nickname && user.nickname">
-                  {{ user.nickname.substring(0, 1) }}
-                </el-avatar>
+            <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 }}
+          </div>
+        </div>
+        <div v-else class="flex items-center flex-wrap mt-1 gap2">
+          <!-- 情况一:遍历每个审批节点下的【进行中】task 任务 -->
+          <div v-for="(task, idx) in activity.tasks" :key="idx" class="flex flex-col pr-2 gap2">
+            <div
+              class="position-relative flex flex-wrap gap2"
+              v-if="task.assigneeUser || task.ownerUser"
+            >
+              <!-- 信息:头像昵称 -->
+              <div
+                class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+              >
+                <template v-if="task.assigneeUser?.avatar || task.assigneeUser?.nickname">
+                  <el-avatar
+                    class="!m-5px"
+                    :size="28"
+                    v-if="task.assigneeUser?.avatar"
+                    :src="task.assigneeUser?.avatar"
+                  />
+                  <el-avatar class="!m-5px" :size="28" v-else>
+                    {{ task.assigneeUser?.nickname.substring(0, 1) }}
+                  </el-avatar>
+                  {{ task.assigneeUser?.nickname }}
+                </template>
+                <template v-else-if="task.ownerUser?.avatar || task.ownerUser?.nickname">
+                  <el-avatar
+                    class="!m-5px"
+                    :size="28"
+                    v-if="task.ownerUser?.avatar"
+                    :src="task.ownerUser?.avatar"
+                  />
+                  <el-avatar class="!m-5px" :size="28" v-else>
+                    {{ task.ownerUser?.nickname.substring(0, 1) }}
+                  </el-avatar>
+                  {{ task.ownerUser?.nickname }}
+                </template>
                 <!-- 信息:任务 ICON -->
                 <div
-                  class="position-absolute top-26px left-26px bg-#fff rounded-full flex items-center p-2px"
+                  v-if="showStatusIcon && onlyStatusIconShow.includes(task.status)"
+                  class="position-absolute top-19px left-23px rounded-full flex items-center p-1px border-2 border-white border-solid"
+                  :style="{ backgroundColor: statusIconMap2[task.status]?.color }"
                 >
-                  <Icon
-                    :size="12"
-                    :icon="statusIconMap2['-1']?.icon"
-                    :color="statusIconMap2['-1']?.color"
-                  />
-                </div>
-              </div>
-              <div class="flex flex-col mt-1">
-                <!-- 信息:昵称 -->
-                <div v-if="user.nickname" class="text-10px text-align-center">
-                  {{ user.nickname }}
+                  <Icon :size="11" :icon="statusIconMap2[task.status]?.icon" color="#FFFFFF" />
                 </div>
-                <!-- <div v-if="task.reason" :title="task.reason" class="text-13px text-truncate w-150px mt-1"> 审批意见: {{ task.reason }}</div> -->
               </div>
             </div>
+            <teleport defer :to="`#activity-task-${activity.id}`">
+              <div
+                v-if="
+                  task.reason &&
+                  [NodeType.USER_TASK_NODE, NodeType.END_EVENT_NODE].includes(activity.nodeType)
+                "
+                class="text-#a5a5a5 text-13px mt-1 w-full bg-#f8f8fa p2 rounded-md"
+              >
+                审批意见:{{ task.reason }}
+              </div>
+            </teleport>
           </div>
-        </div>
-        <!-- 信息:时间 -->
-        <div
-          v-if="activity.status !== TaskStatusEnum.NOT_START"
-          class="text-#a5a5a5 text-13px mt-1"
-        >
-          {{ getApprovalNodeTime(activity) }}
-        </div>
-
-        <!-- TODO @jason:审批意见,要展示哈。 -->
-        <!-- <div class="color-#a1a6ae text-12px mb-10px"> {{ activity.assigneeUser.nickname }}</div>
-        <div v-if="activity.opinion" class="text-#a5a5a5 text-12px w-100%">
-          <div class="mb-5px">审批意见:</div>
+          <!-- 情况二:遍历每个审批节点下的【候选的】task 任务。例如说,1)依次审批,2)未来的审批任务等 -->
           <div
-            class="w-100% border-1px border-#a5a5a5 border-dashed rounded py-5px px-15px text-#2d2d2d"
+            v-for="(user, idx1) in activity.candidateUsers"
+            :key="idx1"
+            class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
           >
-            {{ activity.opinion }}
+            <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 -->
+            <div
+              v-if="showStatusIcon"
+              class="position-absolute top-20px left-24px rounded-full flex items-center p-1px border-2 border-white border-solid"
+              :style="{ backgroundColor: statusIconMap2['-1']?.color }"
+            >
+              <Icon :size="11" :icon="statusIconMap2['-1']?.icon" color="#FFFFFF" />
+            </div>
           </div>
         </div>
-        <div v-if="activity.createTime" class="text-#a5a5a5 text-13px">
-          {{ formatDate(activity.createTime) }}
-        </div> -->
       </div>
     </el-timeline-item>
   </el-timeline>
+
+  <!-- 用户选择弹窗 -->
+  <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
 </template>
 
 <script lang="ts" setup>
 import { formatDate } from '@/utils/formatTime'
 import * as ProcessInstanceApi from '@/api/bpm/processInstance'
 import { TaskStatusEnum } from '@/api/bpm/task'
-import { NodeType } from '@/components/SimpleProcessDesignerV2/src/consts'
+import { NodeType, CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts'
+import { isEmpty } from '@/utils/is'
 import { Check, Close, Loading, Clock, Minus, Delete } from '@element-plus/icons-vue'
+import starterSvg from '@/assets/svgs/bpm/starter.svg'
+import auditorSvg from '@/assets/svgs/bpm/auditor.svg'
+import copySvg from '@/assets/svgs/bpm/copy.svg'
+import conditionSvg from '@/assets/svgs/bpm/condition.svg'
+import parallelSvg from '@/assets/svgs/bpm/parallel.svg'
+import finishSvg from '@/assets/svgs/bpm/finish.svg'
+
 defineOptions({ name: 'BpmProcessInstanceTimeline' })
-const props = defineProps({
-  // 流程实例编号
-  processInstanceId: {
-    type: String,
-    required: false,
-    default: ''
-  },
-  // 流程定义编号
-  processDefinitionId: {
-    type: String,
-    required: false,
-    default: ''
+withDefaults(
+  defineProps<{
+    activityNodes: ProcessInstanceApi.ApprovalNodeInfo[] // 审批节点信息
+    showStatusIcon?: boolean // 是否显示头像右下角状态图标
+  }>(),
+  {
+    showStatusIcon: true // 默认值为 true
   }
-})
+)
 
 // 审批节点
-const approveNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([])
-
 const statusIconMap2 = {
   // 未开始
-  '-1': { color: '#e5e7ec', icon: 'ep-clock' },
+  '-1': { color: '#909398', icon: 'ep-clock' },
   // 待审批
-  '0': { color: '#e5e7ec', icon: 'ep:loading' },
+  '0': { color: '#00b32a', icon: 'ep:loading' },
   // 审批中
   '1': { color: '#448ef7', icon: 'ep:loading' },
   // 审批通过
@@ -160,7 +194,7 @@ const statusIconMap2 = {
   '3': { color: '#f46b6c', icon: 'fa-solid:times-circle' },
   // 取消
   '4': { color: '#cccccc', icon: 'ep:delete-filled' },
-  // 退
+  // 退
   '5': { color: '#f46b6c', icon: 'ep:remove-filled' },
   // 委派中
   '6': { color: '#448ef7', icon: 'ep:loading' },
@@ -170,8 +204,8 @@ const statusIconMap2 = {
 
 const statusIconMap = {
   // 审批未开始
-  '-1': { color: '#e5e7ec', icon: Clock },
-  '0': { color: '#e5e7ec', icon: Clock },
+  '-1': { color: '#909398', icon: Clock },
+  '0': { color: '#00b32a', icon: Clock },
   // 审批中
   '1': { color: '#448ef7', icon: Loading },
   // 审批通过
@@ -180,7 +214,7 @@ const statusIconMap = {
   '3': { color: '#f46b6c', icon: Close },
   // 已取消
   '4': { color: '#cccccc', icon: Delete },
-  // 退
+  // 退
   '5': { color: '#f46b6c', icon: Minus },
   // 委派中
   '6': { color: '#448ef7', icon: Loading },
@@ -188,13 +222,27 @@ const statusIconMap = {
   '7': { color: '#00b32a', icon: Check }
 }
 
-/** 获得审批详情 */
-const getApprovalDetail = async () => {
-  const data = await ProcessInstanceApi.getApprovalDetail(
-    props.processInstanceId,
-    props.processDefinitionId
-  )
-  approveNodes.value = data.approveNodes
+const nodeTypeSvgMap = {
+  // 结束节点
+  [NodeType.END_EVENT_NODE]: { color: '#909398', svg: finishSvg },
+  // 发起人节点
+  [NodeType.START_USER_NODE]: { color: '#909398', svg: starterSvg },
+  // 审批人节点
+  [NodeType.USER_TASK_NODE]: { color: '#ff943e', svg: auditorSvg },
+  // 抄送人节点
+  [NodeType.COPY_TASK_NODE]: { color: '#3296fb', svg: copySvg },
+  // 条件分支节点
+  [NodeType.CONDITION_NODE]: { color: '#14bb83', svg: conditionSvg },
+  // 并行分支节点
+  [NodeType.PARALLEL_BRANCH_NODE]: { color: '#14bb83', svg: parallelSvg }
+}
+
+// 只有只有状态是 -1、0、1 才展示头像右小角状态小icon
+const onlyStatusIconShow = [-1, 0, 1]
+
+// timeline时间线上icon图标
+const getApprovalNodeImg = (nodeType: NodeType) => {
+  return nodeTypeSvgMap[nodeType]?.svg
 }
 
 const getApprovalNodeIcon = (taskStatus: number, nodeType: NodeType) => {
@@ -202,7 +250,11 @@ const getApprovalNodeIcon = (taskStatus: number, nodeType: NodeType) => {
     return statusIconMap[taskStatus]?.icon
   }
 
-  if (nodeType === NodeType.START_USER_NODE || nodeType === NodeType.USER_TASK_NODE) {
+  if (
+    nodeType === NodeType.START_USER_NODE ||
+    nodeType === NodeType.USER_TASK_NODE ||
+    nodeType === NodeType.END_EVENT_NODE
+  ) {
     return statusIconMap[taskStatus]?.icon
   }
 }
@@ -212,22 +264,29 @@ const getApprovalNodeColor = (taskStatus: number) => {
 }
 
 const getApprovalNodeTime = (node: ProcessInstanceApi.ApprovalNodeInfo) => {
+  if (node.nodeType === NodeType.START_USER_NODE && node.startTime) {
+    return `${formatDate(node.startTime)}`
+  }
   if (node.endTime) {
-    return `结束时间:${formatDate(node.endTime)}`
+    return `${formatDate(node.endTime)}`
   }
   if (node.startTime) {
-    return `创建时间:${formatDate(node.startTime)}`
+    return `${formatDate(node.startTime)}`
   }
 }
 
-/** 重新刷新审批详情 */
-const refresh = () => {
-  getApprovalDetail()
+// 选择自定义审批人
+const userSelectFormRef = ref()
+const handleSelectUser = (activityId, selectedList) => {
+  userSelectFormRef.value.open(activityId, selectedList)
+}
+const emit = defineEmits<{
+  selectUserConfirm: [id: any, userList: any[]]
+}>()
+const customApproveUsers: any = ref({}) // key:activityId,value:用户列表
+// 选择完成
+const handleUserSelectConfirm = (activityId: string, userList: any[]) => {
+  customApproveUsers.value[activityId] = userList || []
+  emit('selectUserConfirm', activityId, userList)
 }
-
-defineExpose({ refresh })
-
-onMounted(async () => {
-  await getApprovalDetail()
-})
 </script>

+ 0 - 89
src/views/bpm/processInstance/detail/dialog/TaskDelegateForm.vue

@@ -1,89 +0,0 @@
-<template>
-  <Dialog v-model="dialogVisible" title="委派任务" width="500">
-    <el-form
-      ref="formRef"
-      v-loading="formLoading"
-      :model="formData"
-      :rules="formRules"
-      label-width="110px"
-    >
-      <el-form-item label="接收人" prop="delegateUserId">
-        <el-select v-model="formData.delegateUserId" clearable style="width: 100%">
-          <el-option
-            v-for="item in userList"
-            :key="item.id"
-            :label="item.nickname"
-            :value="item.id"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="委派理由" prop="reason">
-        <el-input v-model="formData.reason" clearable placeholder="请输入委派理由" />
-      </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>
-</template>
-<script lang="ts" setup>
-import * as TaskApi from '@/api/bpm/task'
-import * as UserApi from '@/api/system/user'
-
-defineOptions({ name: 'BpmTaskDelegateForm' })
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const formLoading = ref(false) // 表单的加载中
-const formData = ref({
-  id: '',
-  delegateUserId: undefined,
-  reason: ''
-})
-const formRules = ref({
-  delegateUserId: [{ required: true, message: '接收人不能为空', trigger: 'change' }],
-  reason: [{ required: true, message: '委派理由不能为空', trigger: 'blur' }]
-})
-
-const formRef = ref() // 表单 Ref
-const userList = ref<any[]>([]) // 用户列表
-
-/** 打开弹窗 */
-const open = async (id: string) => {
-  dialogVisible.value = true
-  resetForm()
-  formData.value.id = id
-  // 获得用户列表
-  userList.value = await UserApi.getSimpleUserList()
-}
-defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
-  // 提交请求
-  formLoading.value = true
-  try {
-    await TaskApi.delegateTask(formData.value)
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: '',
-    delegateUserId: undefined,
-    reason: ''
-  }
-  formRef.value?.resetFields()
-}
-</script>

+ 0 - 90
src/views/bpm/processInstance/detail/dialog/TaskReturnForm.vue

@@ -1,90 +0,0 @@
-<template>
-  <Dialog v-model="dialogVisible" title="回退任务" width="500">
-    <el-form
-      ref="formRef"
-      v-loading="formLoading"
-      :model="formData"
-      :rules="formRules"
-      label-width="110px"
-    >
-      <el-form-item label="退回节点" prop="targetTaskDefinitionKey">
-        <el-select v-model="formData.targetTaskDefinitionKey" clearable style="width: 100%">
-          <el-option
-            v-for="item in returnList"
-            :key="item.taskDefinitionKey"
-            :label="item.name"
-            :value="item.taskDefinitionKey"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="回退理由" prop="reason">
-        <el-input v-model="formData.reason" clearable placeholder="请输入回退理由" />
-      </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>
-</template>
-<script lang="ts" name="TaskRollbackDialogForm" setup>
-import * as TaskApi from '@/api/bpm/task'
-
-const message = useMessage() // 消息弹窗
-const dialogVisible = ref(false) // 弹窗的是否展示
-const formLoading = ref(false) // 表单的加载中
-const formData = ref({
-  id: '',
-  targetTaskDefinitionKey: undefined,
-  reason: ''
-})
-const formRules = ref({
-  targetTaskDefinitionKey: [{ required: true, message: '必须选择回退节点', trigger: 'change' }],
-  reason: [{ required: true, message: '回退理由不能为空', trigger: 'blur' }]
-})
-
-const formRef = ref() // 表单 Ref
-const returnList = ref([] as any)
-/** 打开弹窗 */
-const open = async (id: string) => {
-  returnList.value = await TaskApi.getTaskListByReturn(id)
-  if (returnList.value.length === 0) {
-    message.warning('当前没有可回退的节点')
-    return false
-  }
-  dialogVisible.value = true
-  resetForm()
-  formData.value.id = id
-}
-defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
-  // 提交请求
-  formLoading.value = true
-  try {
-    await TaskApi.returnTask(formData.value)
-    message.success('回退成功')
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: '',
-    targetTaskDefinitionKey: undefined,
-    reason: ''
-  }
-  formRef.value?.resetFields()
-}
-</script>

+ 0 - 99
src/views/bpm/processInstance/detail/dialog/TaskSignCreateForm.vue

@@ -1,99 +0,0 @@
-<template>
-  <Dialog v-model="dialogVisible" title="加签" width="500">
-    <el-form
-      ref="formRef"
-      v-loading="formLoading"
-      :model="formData"
-      :rules="formRules"
-      label-width="110px"
-    >
-      <el-form-item label="加签处理人" prop="userIds">
-        <el-select v-model="formData.userIds" multiple clearable style="width: 100%">
-          <el-option
-            v-for="item in userList"
-            :key="item.id"
-            :label="item.nickname"
-            :value="item.id"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="加签理由" prop="reason">
-        <el-input v-model="formData.reason" clearable placeholder="请输入加签理由" />
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm('before')">
-        向前加签
-      </el-button>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm('after')">
-        向后加签
-      </el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import * as TaskApi from '@/api/bpm/task'
-import * as UserApi from '@/api/system/user'
-
-defineOptions({ name: 'TaskSignCreateForm' })
-
-const message = useMessage() // 消息弹窗
-const dialogVisible = ref(false) // 弹窗的是否展示
-const formLoading = ref(false) // 表单的加载中
-const formData = ref({
-  id: '',
-  userIds: [],
-  type: '',
-  reason: ''
-})
-const formRules = ref({
-  userIds: [{ required: true, message: '加签处理人不能为空', trigger: 'change' }],
-  reason: [{ required: true, message: '加签理由不能为空', trigger: 'change' }]
-})
-
-const formRef = ref() // 表单 Ref
-const userList = ref<any[]>([]) // 用户列表
-
-/** 打开弹窗 */
-const open = async (id: string) => {
-  dialogVisible.value = true
-  resetForm()
-  formData.value.id = id
-  // 获得用户列表
-  userList.value = await UserApi.getSimpleUserList()
-}
-defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async (type: string) => {
-  // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
-  // 提交请求
-  formLoading.value = true
-  formData.value.type = type
-  try {
-    await TaskApi.signCreateTask(formData.value)
-    message.success('加签成功')
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: '',
-    userIds: [],
-    type: '',
-    reason: ''
-  }
-  formRef.value?.resetFields()
-}
-</script>

+ 0 - 89
src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue

@@ -1,89 +0,0 @@
-<template>
-  <Dialog v-model="dialogVisible" title="减签" width="500">
-    <el-form
-      ref="formRef"
-      v-loading="formLoading"
-      :model="formData"
-      :rules="formRules"
-      label-width="110px"
-    >
-      <el-form-item label="减签任务" prop="id">
-        <el-radio-group v-model="formData.id">
-          <el-radio-button v-for="item in childrenTaskList" :key="item.id" :value="item.id">
-            {{ item.name }}
-            ({{ item.assigneeUser?.deptName || item.ownerUser?.deptName }} -
-            {{ item.assigneeUser?.nickname || item.ownerUser?.nickname }})
-          </el-radio-button>
-        </el-radio-group>
-      </el-form-item>
-      <el-form-item label="减签理由" prop="reason">
-        <el-input v-model="formData.reason" clearable placeholder="请输入减签理由" />
-      </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>
-</template>
-<script lang="ts" setup>
-import * as TaskApi from '@/api/bpm/task'
-import { isEmpty } from '@/utils/is'
-
-defineOptions({ name: 'TaskSignDeleteForm' })
-
-const message = useMessage() // 消息弹窗
-const dialogVisible = ref(false) // 弹窗的是否展示
-const formLoading = ref(false) // 表单的加载中
-const formData = ref({
-  id: '',
-  reason: ''
-})
-const formRules = ref({
-  id: [{ required: true, message: '必须选择减签任务', trigger: 'change' }],
-  reason: [{ required: true, message: '减签理由不能为空', trigger: 'blur' }]
-})
-
-const formRef = ref() // 表单 Ref
-const childrenTaskList = ref([])
-/** 打开弹窗 */
-const open = async (id: string) => {
-  childrenTaskList.value = await TaskApi.getChildrenTaskList(id)
-  if (isEmpty(childrenTaskList.value)) {
-    message.warning('当前没有可减签的任务')
-    return false
-  }
-  dialogVisible.value = true
-  resetForm()
-}
-defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
-  // 提交请求
-  formLoading.value = true
-  try {
-    await TaskApi.signDeleteTask(formData.value)
-    message.success('减签成功')
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: '',
-    reason: ''
-  }
-  formRef.value?.resetFields()
-}
-</script>

+ 0 - 106
src/views/bpm/processInstance/detail/dialog/TaskSignList.vue

@@ -1,106 +0,0 @@
-<template>
-  <el-drawer v-model="drawerVisible" title="子任务" size="880px">
-    <!-- 当前任务 -->
-    <template #header>
-      <h4>【{{ parentTask.name }} 】审批人:{{ parentTask?.assigneeUser?.nickname }}</h4>
-      <el-button
-        style="margin-left: 5px"
-        v-if="isSignDeleteButtonVisible(parentTask)"
-        type="danger"
-        plain
-        @click="handleSignDelete(parentTask)"
-      >
-        <Icon icon="ep:remove" /> 减签
-      </el-button>
-    </template>
-    <!-- 子任务列表 -->
-    <el-table :data="parentTask.children" style="width: 100%" row-key="id" border>
-      <el-table-column prop="assigneeUser.nickname" label="审批人" min-width="100">
-        <template #default="scope">
-          {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
-        </template>
-      </el-table-column>
-      <el-table-column prop="assigneeUser.deptName" label="所在部门" min-width="100">
-        <template #default="scope">
-          {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
-        </template>
-      </el-table-column>
-      <el-table-column label="审批状态" prop="status" width="120">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
-        </template>
-      </el-table-column>
-      <el-table-column
-        label="提交时间"
-        align="center"
-        prop="createTime"
-        width="180"
-        :formatter="dateFormatter"
-      />
-      <el-table-column
-        label="结束时间"
-        align="center"
-        prop="endTime"
-        width="180"
-        :formatter="dateFormatter"
-      />
-      <el-table-column label="操作" prop="operation" width="90">
-        <template #default="scope">
-          <el-button
-            v-if="isSignDeleteButtonVisible(scope.row)"
-            type="danger"
-            plain
-            size="small"
-            @click="handleSignDelete(scope.row)"
-          >
-            <Icon icon="ep:remove" /> 减签
-          </el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-
-    <!-- 减签 -->
-    <TaskSignDeleteForm ref="taskSignDeleteFormRef" @success="handleSignDeleteSuccess" />
-  </el-drawer>
-</template>
-<script lang="ts" setup>
-import { isEmpty } from '@/utils/is'
-import { DICT_TYPE } from '@/utils/dict'
-import { dateFormatter } from '@/utils/formatTime'
-import TaskSignDeleteForm from './TaskSignDeleteForm.vue'
-
-defineOptions({ name: 'TaskSignList' })
-
-const message = useMessage() // 消息弹窗
-const drawerVisible = ref(false) // 抽屉的是否展示
-const parentTask = ref({} as any)
-
-/** 打开弹窗 */
-const open = async (task: any) => {
-  if (isEmpty(task.children)) {
-    message.warning('该任务没有子任务')
-    return
-  }
-  parentTask.value = task
-  // 展开抽屉
-  drawerVisible.value = true
-}
-defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
-
-/** 发起减签 */
-const taskSignDeleteFormRef = ref()
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const handleSignDelete = (item: any) => {
-  taskSignDeleteFormRef.value.open(item.id)
-}
-const handleSignDeleteSuccess = () => {
-  emit('success')
-  // 关闭抽屉
-  drawerVisible.value = false
-}
-
-/** 是否显示减签按钮 */
-const isSignDeleteButtonVisible = (task: any) => {
-  return task && task.children && !isEmpty(task.children)
-}
-</script>

+ 0 - 89
src/views/bpm/processInstance/detail/dialog/TaskTransferForm.vue

@@ -1,89 +0,0 @@
-<template>
-  <Dialog v-model="dialogVisible" title="转派任务" width="500">
-    <el-form
-      ref="formRef"
-      v-loading="formLoading"
-      :model="formData"
-      :rules="formRules"
-      label-width="110px"
-    >
-      <el-form-item label="新审批人" prop="assigneeUserId">
-        <el-select v-model="formData.assigneeUserId" clearable style="width: 100%">
-          <el-option
-            v-for="item in userList"
-            :key="item.id"
-            :label="item.nickname"
-            :value="item.id"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="转派理由" prop="reason">
-        <el-input v-model="formData.reason" clearable placeholder="请输入转派理由" />
-      </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>
-</template>
-<script lang="ts" setup>
-import * as TaskApi from '@/api/bpm/task'
-import * as UserApi from '@/api/system/user'
-
-defineOptions({ name: 'TaskTransferForm' })
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const formLoading = ref(false) // 表单的加载中
-const formData = ref({
-  id: '',
-  assigneeUserId: undefined,
-  reason: ''
-})
-const formRules = ref({
-  assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }],
-  reason: [{ required: true, message: '转派理由不能为空', trigger: 'blur' }]
-})
-
-const formRef = ref() // 表单 Ref
-const userList = ref<any[]>([]) // 用户列表
-
-/** 打开弹窗 */
-const open = async (id: string) => {
-  dialogVisible.value = true
-  resetForm()
-  formData.value.id = id
-  // 获得用户列表
-  userList.value = await UserApi.getSimpleUserList()
-}
-defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
-  // 提交请求
-  formLoading.value = true
-  try {
-    await TaskApi.transferTask(formData.value)
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: '',
-    assigneeUserId: undefined,
-    reason: ''
-  }
-  formRef.value?.resetFields()
-}
-</script>

+ 258 - 395
src/views/bpm/processInstance/detail/index.vue

@@ -1,222 +1,167 @@
 <template>
-  <ContentWrap>
-    <!-- 审批信息 -->
-    <el-card
-      v-for="(item, index) in runningTasks"
-      :key="index"
-      v-loading="processInstanceLoading"
-      class="box-card"
-    >
-      <template #header>
-        <span class="el-icon-picture-outline">审批任务【{{ item.name }}】</span>
-      </template>
-      <el-col :offset="6" :span="16">
-        <el-form
-          :ref="'form' + index"
-          :model="auditForms[index]"
-          :rules="auditRule"
-          label-width="100px"
-        >
-          <el-form-item v-if="processInstance && processInstance.name" label="流程名">
-            {{ processInstance.name }}
-          </el-form-item>
-          <el-form-item v-if="processInstance && processInstance.startUser" label="流程发起人">
-            {{ processInstance?.startUser.nickname }}
-            <el-tag size="small" type="info">{{ processInstance?.startUser.deptName }}</el-tag>
-          </el-form-item>
-          <el-card v-if="runningTasks[index].formId > 0" class="mb-15px !-mt-10px">
-            <template #header>
-              <span class="el-icon-picture-outline">
-                填写表单【{{ runningTasks[index]?.formName }}】
-              </span>
-            </template>
-            <form-create
-              v-model="approveForms[index].value"
-              v-model:api="approveFormFApis[index]"
-              :option="approveForms[index].option"
-              :rule="approveForms[index].rule"
-            />
-          </el-card>
-          <el-form-item label="审批建议" prop="reason">
-            <el-input
-              v-model="auditForms[index].reason"
-              placeholder="请输入审批建议"
-              type="textarea"
-            />
-          </el-form-item>
-          <el-form-item label="抄送人" prop="copyUserIds">
-            <el-select v-model="auditForms[index].copyUserIds" multiple placeholder="请选择抄送人">
-              <el-option
-                v-for="itemx in userOptions"
-                :key="itemx.id"
-                :label="itemx.nickname"
-                :value="itemx.id"
-              />
-            </el-select>
-          </el-form-item>
-        </el-form>
-        <div style="margin-bottom: 20px; margin-left: 10%; font-size: 14px">
-          <!-- TODO @jason:建议搞个 if 来判断,替代现有的 !item.buttonsSetting || item.buttonsSetting[OpsButtonType.APPROVE]?.enable -->
-          <el-button
-            type="success"
-            v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.APPROVE]?.enable"
-            @click="handleAudit(item, true)"
-          >
-            <Icon icon="ep:select" />
-            <!-- TODO @jason:这个也是类似哈,搞个方法来生成名字 -->
-            {{
-              item.buttonsSetting?.[OperationButtonType.APPROVE]?.displayName ||
-              OPERATION_BUTTON_NAME.get(OperationButtonType.APPROVE)
-            }}
-          </el-button>
-          <el-button
-            v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.REJECT]?.enable"
-            type="danger"
-            @click="handleAudit(item, false)"
-          >
-            <Icon icon="ep:close" />
-            {{
-              item.buttonsSetting?.[OperationButtonType.REJECT].displayName ||
-              OPERATION_BUTTON_NAME.get(OperationButtonType.REJECT)
-            }}
-          </el-button>
-          <el-button
-            v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.TRANSFER]?.enable"
-            type="primary"
-            @click="openTaskUpdateAssigneeForm(item.id)"
-          >
-            <Icon icon="ep:edit" />
-            {{
-              item.buttonsSetting?.[OperationButtonType.TRANSFER]?.displayName ||
-              OPERATION_BUTTON_NAME.get(OperationButtonType.TRANSFER)
-            }}
-          </el-button>
-          <el-button
-            v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.DELEGATE]?.enable"
-            type="primary"
-            @click="handleDelegate(item)"
-          >
-            <Icon icon="ep:position" />
-            {{
-              item.buttonsSetting?.[OperationButtonType.DELEGATE]?.displayName ||
-              OPERATION_BUTTON_NAME.get(OperationButtonType.DELEGATE)
-            }}
-          </el-button>
-          <el-button
-            v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.ADD_SIGN]?.enable"
-            type="primary"
-            @click="handleSign(item)"
-          >
-            <Icon icon="ep:plus" />
-            {{
-              item.buttonsSetting?.[OperationButtonType.ADD_SIGN]?.displayName ||
-              OPERATION_BUTTON_NAME.get(OperationButtonType.ADD_SIGN)
-            }}
-          </el-button>
-          <el-button
-            v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.RETURN]?.enable"
-            type="warning"
-            @click="handleBack(item)"
+  <ContentWrap :bodyStyle="{ padding: '10px 20px 0' }" class="position-relative">
+    <div class="processInstance-wrap-main">
+      <el-scrollbar>
+        <img
+          class="position-absolute right-20px"
+          width="150"
+          :src="auditIconsMap[processInstance.status]"
+          alt=""
+        />
+        <div class="text-#878c93 h-15px">编号:{{ id }}</div>
+        <el-divider class="!my-8px" />
+        <div class="flex items-center gap-5 mb-10px h-40px">
+          <div class="text-26px font-bold mb-5px">{{ processInstance.name }}</div>
+          <dict-tag
+            v-if="processInstance.status"
+            :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS"
+            :value="processInstance.status"
+          />
+        </div>
+
+        <div class="flex items-center gap-5 mb-10px text-13px h-35px">
+          <div
+            class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600"
           >
-            <Icon icon="ep:back" />
-            {{
-              item.buttonsSetting?.[OperationButtonType.RETURN]?.displayName ||
-              OPERATION_BUTTON_NAME.get(OperationButtonType.RETURN)
-            }}
-          </el-button>
+            <el-avatar
+              :size="28"
+              v-if="processInstance?.startUser?.avatar"
+              :src="processInstance?.startUser?.avatar"
+            />
+            <el-avatar :size="28" v-else-if="processInstance?.startUser?.nickname">
+              {{ processInstance?.startUser?.nickname.substring(0, 1) }}
+            </el-avatar>
+            {{ processInstance?.startUser?.nickname }}
+          </div>
+          <div class="text-#878c93"> {{ formatDate(processInstance.startTime) }} 提交 </div>
         </div>
-      </el-col>
-    </el-card>
 
-    <!-- 申请信息 -->
-    <el-card v-loading="processInstanceLoading" class="box-card">
-      <template #header>
-        <span class="el-icon-document">申请信息【{{ processInstance.name }}】</span>
-      </template>
-      <!-- 情况一:流程表单 -->
-      <el-col v-if="processInstance?.processDefinition?.formType === 10" :offset="6" :span="16">
-        <form-create
-          v-model="detailForm.value"
-          v-model:api="fApi"
-          :option="detailForm.option"
-          :rule="detailForm.rule"
-        />
-      </el-col>
-      <!-- 情况二:业务表单 -->
-      <div v-if="processInstance?.processDefinition?.formType === 20">
-        <BusinessFormComponent :id="processInstance.businessKey" />
-      </div>
-    </el-card>
+        <el-tabs v-model="activeTab">
+          <!-- 表单信息 -->
+          <el-tab-pane label="审批详情" name="form">
+            <div class="form-scroll-area">
+              <el-scrollbar>
+                <el-row>
+                  <el-col :span="17" class="!flex !flex-col formCol">
+                    <!-- 表单信息 -->
+                    <div
+                      v-loading="processInstanceLoading"
+                      class="form-box flex flex-col mb-30px flex-1"
+                    >
+                      <!-- 情况一:流程表单 -->
+                      <el-col v-if="processDefinition?.formType === 10">
+                        <form-create
+                          v-model="detailForm.value"
+                          v-model:api="fApi"
+                          :option="detailForm.option"
+                          :rule="detailForm.rule"
+                        />
+                      </el-col>
+                      <!-- 情况二:业务表单 -->
+                      <div v-if="processDefinition?.formType === 20">
+                        <BusinessFormComponent :id="processInstance.businessKey" />
+                      </div>
+                    </div>
+                  </el-col>
+                  <el-col :span="7">
+                    <!-- 审批记录时间线 -->
+                    <ProcessInstanceTimeline :activity-nodes="activityNodes" />
+                  </el-col>
+                </el-row>
+              </el-scrollbar>
+            </div>
+          </el-tab-pane>
+
+          <!-- 流程图 -->
+          <el-tab-pane label="流程图" name="diagram">
+            <div class="form-scroll-area">
+              <ProcessInstanceSimpleViewer
+                v-show="
+                  processDefinition.modelType && processDefinition.modelType === BpmModelType.SIMPLE
+                "
+                :loading="processInstanceLoading"
+                :model-view="processModelView"
+              />
+              <ProcessInstanceBpmnViewer
+                v-show="
+                  processDefinition.modelType && processDefinition.modelType === BpmModelType.BPMN
+                "
+                :loading="processInstanceLoading"
+                :model-view="processModelView"
+              />
+            </div>
+          </el-tab-pane>
 
-    <!-- 审批记录 -->
-    <ProcessInstanceTaskList
-      :loading="tasksLoad"
-      :process-instance="processInstance"
-      :tasks="tasks"
-      @refresh="getTaskList"
-    />
+          <!-- 流转记录 -->
+          <el-tab-pane label="流转记录" name="record">
+            <div class="form-scroll-area">
+              <el-scrollbar>
+                <ProcessInstanceTaskList :loading="processInstanceLoading" :id="id" />
+              </el-scrollbar>
+            </div>
+          </el-tab-pane>
 
-    <!-- 高亮流程图 -->
-    <ProcessInstanceBpmnViewer
-      :id="`${id}`"
-      :bpmn-xml="bpmnXml"
-      :loading="processInstanceLoading"
-      :process-instance="processInstance"
-      :tasks="tasks"
-    />
+          <!-- 流转评论 TODO 待开发 -->
+          <el-tab-pane label="流转评论" name="comment" v-if="false">
+            <div class="form-scroll-area">
+              <el-scrollbar> 流转评论 </el-scrollbar>
+            </div>
+          </el-tab-pane>
+        </el-tabs>
 
-    <!-- 弹窗:转派审批人 -->
-    <TaskTransferForm ref="taskTransferFormRef" @success="getDetail" />
-    <!-- 弹窗:回退节点 -->
-    <TaskReturnForm ref="taskReturnFormRef" @success="getDetail" />
-    <!-- 弹窗:委派,将任务委派给别人处理,处理完成后,会重新回到原审批人手中-->
-    <TaskDelegateForm ref="taskDelegateForm" @success="getDetail" />
-    <!-- 弹窗:加签,当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 -->
-    <TaskSignCreateForm ref="taskSignCreateFormRef" @success="getDetail" />
+        <div class="b-t-solid border-t-1px border-[var(--el-border-color)]">
+          <!-- 操作栏按钮 -->
+          <ProcessInstanceOperationButton
+            ref="operationButtonRef"
+            :process-instance="processInstance"
+            :process-definition="processDefinition"
+            :userOptions="userOptions"
+            @success="refresh"
+          />
+        </div>
+      </el-scrollbar>
+    </div>
   </ContentWrap>
 </template>
 <script lang="ts" setup>
-import { useUserStore } from '@/store/modules/user'
+import { formatDate } from '@/utils/formatTime'
+import { DICT_TYPE } from '@/utils/dict'
+import { BpmModelType } from '@/utils/constants'
 import { setConfAndFields2 } from '@/utils/formCreate'
+import { registerComponent } from '@/utils/routerHelper'
 import type { ApiAttrs } from '@form-create/element-ui/types/config'
-import * as DefinitionApi from '@/api/bpm/definition'
 import * as ProcessInstanceApi from '@/api/bpm/processInstance'
-import * as TaskApi from '@/api/bpm/task'
+import * as UserApi from '@/api/system/user'
 import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue'
+import ProcessInstanceSimpleViewer from './ProcessInstanceSimpleViewer.vue'
 import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue'
-import TaskReturnForm from './dialog/TaskReturnForm.vue'
-import TaskDelegateForm from './dialog/TaskDelegateForm.vue'
-import TaskTransferForm from './dialog/TaskTransferForm.vue'
-import TaskSignCreateForm from './dialog/TaskSignCreateForm.vue'
-import { registerComponent } from '@/utils/routerHelper'
-import { isEmpty } from '@/utils/is'
-import * as UserApi from '@/api/system/user'
-import {
-  OperationButtonType,
-  OPERATION_BUTTON_NAME
-} from '@/components/SimpleProcessDesignerV2/src/consts'
+import ProcessInstanceOperationButton from './ProcessInstanceOperationButton.vue'
+import ProcessInstanceTimeline from './ProcessInstanceTimeline.vue'
+import { FieldPermissionType } from '@/components/SimpleProcessDesignerV2/src/consts'
+import { TaskStatusEnum } from '@/api/bpm/task'
+import runningSvg from '@/assets/svgs/bpm/running.svg'
+import approveSvg from '@/assets/svgs/bpm/approve.svg'
+import rejectSvg from '@/assets/svgs/bpm/reject.svg'
+import cancelSvg from '@/assets/svgs/bpm/cancel.svg'
 
 defineOptions({ name: 'BpmProcessInstanceDetail' })
-
-const { query } = useRoute() // 查询参数
+const props = defineProps<{
+  id: string // 流程实例的编号
+  taskId?: string // 任务编号
+  activityId?: string //流程活动编号,用于抄送查看
+}>()
 const message = useMessage() // 消息弹窗
-const { proxy } = getCurrentInstance() as any
-
-const userId = useUserStore().getUser.id // 当前登录的编号
-const id = query.id as unknown as string // 流程实例的编号
 const processInstanceLoading = ref(false) // 流程实例的加载中
 const processInstance = ref<any>({}) // 流程实例
-const bpmnXml = ref('') // BPMN XML
-const tasksLoad = ref(true) // 任务的加载中
-const tasks = ref<any[]>([]) // 任务列表
-// ========== 审批信息 ==========
-const runningTasks = ref<any[]>([]) // 运行中的任务
-const auditForms = ref<any[]>([]) // 审批任务的表单
-const auditRule = reactive({
-  reason: [{ required: true, message: '审批建议不能为空', trigger: 'blur' }]
-})
-const approveForms = ref<any[]>([]) // 审批通过时,额外的补充信息
-const approveFormFApis = ref<ApiAttrs[]>([]) // approveForms 的 fAPi
+const processDefinition = ref<any>({}) // 流程定义
+const processModelView = ref<any>({}) // 流程模型视图
+const operationButtonRef = ref() // 操作按钮组件 ref
+const auditIconsMap = {
+  [TaskStatusEnum.RUNNING]: runningSvg,
+  [TaskStatusEnum.APPROVE]: approveSvg,
+  [TaskStatusEnum.REJECT]: rejectSvg,
+  [TaskStatusEnum.CANCEL]: cancelSvg
+}
 
 // ========== 申请信息 ==========
 const fApi = ref<ApiAttrs>() //
@@ -226,134 +171,62 @@ const detailForm = ref({
   value: {}
 }) // 流程实例的表单详情
 
-/** 监听 approveFormFApis,实现它对应的 form-create 初始化后,隐藏掉对应的表单提交按钮 */
-watch(
-  () => approveFormFApis.value,
-  (value) => {
-    value?.forEach((api) => {
-      api.btn.show(false)
-      api.resetBtn.show(false)
-    })
-  },
-  {
-    deep: true
-  }
-)
-
-/** 处理审批通过和不通过的操作 */
-const handleAudit = async (task, pass) => {
-  // 1.1 获得对应表单
-  const index = runningTasks.value.indexOf(task)
-  const auditFormRef = proxy.$refs['form' + index][0]
-  // 1.2 校验表单
-  const elForm = unref(auditFormRef)
-  if (!elForm) return
-  let valid = await elForm.validate()
-  if (!valid) return
-  // 校验申请表单(可编辑字段)
-  // TODO @jason:之前这里是 if (!fApi.value) return;针对业务表单的情况下,会导致没办法审核,可能要看下。我这里改了点,看看是不是还有别的地方兼容性
-  if (fApi.value) {
-    valid = await fApi.value.validate()
-    if (!valid) return
-  }
-
-  // 2.1 提交审批
-  const data = {
-    id: task.id,
-    reason: auditForms.value[index].reason,
-    copyUserIds: auditForms.value[index].copyUserIds
-  }
-  if (pass) {
-    // 审批通过,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交
-    const formCreateApi = approveFormFApis.value[index]
-    if (formCreateApi) {
-      await formCreateApi.validate()
-      data.variables = approveForms.value[index].value
-    }
-    // 获取表单可编辑字段的值
-    if (fApi.value && task.fieldsPermission !== null) {
-      data.variables = getWritableValueOfForm(task.fieldsPermission)
-    }
-
-    await TaskApi.approveTask(data)
-    message.success('审批通过成功')
-  } else {
-    await TaskApi.rejectTask(data)
-    message.success('审批不通过成功')
-  }
-  // 2.2 加载最新数据
-  getDetail()
-}
-
-/** 转派审批人 */
-const taskTransferFormRef = ref()
-const openTaskUpdateAssigneeForm = (id: string) => {
-  taskTransferFormRef.value.open(id)
-}
-
-/** 处理审批退回的操作 */
-const taskDelegateForm = ref()
-const handleDelegate = async (task) => {
-  taskDelegateForm.value.open(task.id)
-}
-
-/** 处理审批退回的操作 */
-const taskReturnFormRef = ref()
-const handleBack = async (task: any) => {
-  taskReturnFormRef.value.open(task.id)
-}
-
-/** 处理审批加签的操作 */
-const taskSignCreateFormRef = ref()
-const handleSign = async (task: any) => {
-  taskSignCreateFormRef.value.open(task.id)
-}
-
 /** 获得详情 */
-const getDetail = async () => {
-  // 1. 获得流程任务列表(审批记录)。 需要先获取任务,表单的权限设置需要根据任务来设置
-  await getTaskList()
-  // 2. 获得流程实例相关
-  getProcessInstance()
+const getDetail = () => {
+  getApprovalDetail()
+
+  getProcessModelView()
 }
 
 /** 加载流程实例 */
-const BusinessFormComponent = ref(null) // 异步组件
-const getProcessInstance = async () => {
+const BusinessFormComponent = ref<any>(null) // 异步组件
+/** 获取审批详情 */
+const getApprovalDetail = async () => {
+  processInstanceLoading.value = true
   try {
-    processInstanceLoading.value = true
-    const data = await ProcessInstanceApi.getProcessInstance(id)
+    const param = {
+      processInstanceId: props.id,
+      activityId: props.activityId,
+      taskId: props.taskId
+    }
+    const data = await ProcessInstanceApi.getApprovalDetail(param)
     if (!data) {
+      message.error('查询不到审批详情信息!')
+      return
+    }
+    if (!data.processDefinition || !data.processInstance) {
       message.error('查询不到流程信息!')
       return
     }
-    processInstance.value = data
+    processInstance.value = data.processInstance
+    processDefinition.value = data.processDefinition
 
     // 设置表单信息
-    const processDefinition = data.processDefinition
-    if (processDefinition.formType === 10) {
+    if (processDefinition.value.formType === 10) {
+      // 获取表单字段权限
+      const formFieldsPermission = data.formFieldsPermission
+
       if (detailForm.value.rule.length > 0) {
-        detailForm.value.value = data.formVariables
+        // 避免刷新 form-create 显示不了
+        detailForm.value.value = processInstance.value.formVariables
       } else {
         setConfAndFields2(
           detailForm,
-          processDefinition.formConf,
-          processDefinition.formFields,
-          data.formVariables
+          processDefinition.value.formConf,
+          processDefinition.value.formFields,
+          processInstance.value.formVariables
         )
       }
       nextTick().then(() => {
         fApi.value?.btn.show(false)
         fApi.value?.resetBtn.show(false)
+        //@ts-ignore
         fApi.value?.disabled(true)
-        // 设置表单权限。后续需要改造成。只处理一个运行中的任务
-        if (runningTasks.value.length > 0) {
-          const task = runningTasks.value.at(0)
-          if (task.fieldsPermission) {
-            Object.keys(task.fieldsPermission).forEach((item) => {
-              setFieldPermission(item, task.fieldsPermission[item])
-            })
-          }
+        // 设置表单字段权限
+        if (formFieldsPermission) {
+          Object.keys(data.formFieldsPermission).forEach((item) => {
+            setFieldPermission(item, formFieldsPermission[item])
+          })
         }
       })
     } else {
@@ -361,118 +234,61 @@ const getProcessInstance = async () => {
       BusinessFormComponent.value = registerComponent(data.processDefinition.formCustomViewPath)
     }
 
-    // 加载流程图
-    bpmnXml.value = (
-      await DefinitionApi.getProcessDefinition(processDefinition.id as number)
-    )?.bpmnXml
-  } finally {
-    processInstanceLoading.value = false
-  }
-}
-
-/** 加载任务列表 */
-const getTaskList = async () => {
-  runningTasks.value = []
-  auditForms.value = []
-  approveForms.value = []
-  approveFormFApis.value = []
-  try {
-    // 获得未取消的任务
-    tasksLoad.value = true
-    const data = await TaskApi.getTaskListByProcessInstanceId(id)
-    tasks.value = []
-    // 1.1 移除已取消的审批
-    data.forEach((task) => {
-      if (task.status !== 4) {
-        tasks.value.push(task)
-      }
-    })
-    // 1.2 排序,将未完成的排在前面,已完成的排在后面;
-    tasks.value.sort((a, b) => {
-      // 有已完成的情况,按照完成时间倒序
-      if (a.endTime && b.endTime) {
-        return b.endTime - a.endTime
-      } else if (a.endTime) {
-        return 1
-      } else if (b.endTime) {
-        return -1
-        // 都是未完成,按照创建时间倒序
-      } else {
-        return b.createTime - a.createTime
-      }
-    })
+    // 获取审批节点,显示 Timeline 的数据
+    activityNodes.value = data.activityNodes
 
-    // 获得需要自己审批的任务
-    loadRunningTask(tasks.value)
+    // 获取待办任务显示操作按钮
+    operationButtonRef.value?.loadTodoTask(data.todoTask)
   } finally {
-    tasksLoad.value = false
+    processInstanceLoading.value = false
   }
 }
 
-/**
- * 设置 runningTasks 中的任务
- */
-const loadRunningTask = (tasks) => {
-  tasks.forEach((task) => {
-    if (!isEmpty(task.children)) {
-      loadRunningTask(task.children)
-    }
-    // 2.1 只有待处理才需要
-    if (task.status !== 1 && task.status !== 6) {
-      return
-    }
-    // 2.2 自己不是处理人
-    if (!task.assigneeUser || task.assigneeUser.id !== userId) {
-      return
-    }
-
-    // 2.3 添加到处理任务
-    runningTasks.value.push({ ...task })
-    auditForms.value.push({
-      reason: '',
-      copyUserIds: []
-    })
-
-    // 2.4 处理 approve 表单
-    if (task.formId && task.formConf) {
-      const approveForm = {}
-      setConfAndFields2(approveForm, task.formConf, task.formFields, task.formVariables)
-      approveForms.value.push(approveForm)
-    } else {
-      approveForms.value.push({}) // 占位,避免为空
+/** 获取流程模型视图*/
+const getProcessModelView = async () => {
+  if (BpmModelType.BPMN === processDefinition.value?.modelType) {
+    // 重置,解决 BPMN 流程图刷新不会重新渲染问题
+    processModelView.value = {
+      bpmnXml: ''
     }
-  })
+  }
+  const data = await ProcessInstanceApi.getProcessInstanceBpmnModelView(props.id)
+  if (data) {
+    processModelView.value = data
+  }
 }
 
+// 审批节点信息
+const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([])
 /**
  * 设置表单权限
  */
 const setFieldPermission = (field: string, permission: string) => {
-  if (permission === '1') {
+  if (permission === FieldPermissionType.READ) {
+    //@ts-ignore
     fApi.value?.disabled(true, field)
   }
-  if (permission === '2') {
+  if (permission === FieldPermissionType.WRITE) {
+    //@ts-ignore
     fApi.value?.disabled(false, field)
   }
-  if (permission === '3') {
+  if (permission === FieldPermissionType.NONE) {
+    //@ts-ignore
     fApi.value?.hidden(true, field)
   }
 }
+
 /**
- * 获取可以编辑字段的值
+ * 操作成功后刷新
  */
-const getWritableValueOfForm = (fieldsPermission: Object) => {
-  const fieldsValue = {}
-  if (fieldsPermission && fApi.value) {
-    Object.keys(fieldsPermission).forEach((item) => {
-      if (fieldsPermission[item] === '2') {
-        fieldsValue[item] = fApi.value.getValue(item)
-      }
-    })
-  }
-  return fieldsValue
+const refresh = () => {
+  // 重新获取详情
+  getDetail()
 }
 
+/** 当前的Tab */
+const activeTab = ref('form')
+
 /** 初始化 */
 const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
 onMounted(async () => {
@@ -481,3 +297,50 @@ onMounted(async () => {
   userOptions.value = await UserApi.getSimpleUserList()
 })
 </script>
+
+<style lang="scss" scoped>
+$wrap-padding-height: 20px;
+$wrap-margin-height: 15px;
+$button-height: 51px;
+$process-header-height: 194px;
+
+.processInstance-wrap-main {
+  height: calc(
+    100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
+  );
+  max-height: calc(
+    100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
+  );
+  overflow: auto;
+
+  .form-scroll-area {
+    height: calc(
+      100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
+        $process-header-height - 40px
+    );
+    max-height: calc(
+      100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
+        $process-header-height - 40px
+    );
+    overflow: auto;
+    display: flex;
+    flex-direction: column;
+
+    :deep(.box-card) {
+      height: 100%;
+      flex: 1;
+
+      .el-card__body {
+        height: 100%;
+        padding: 0;
+      }
+    }
+  }
+}
+
+.form-box {
+  :deep(.el-card) {
+    border: none;
+  }
+}
+</style>

+ 0 - 318
src/views/bpm/processInstance/detail/index_new.vue

@@ -1,318 +0,0 @@
-<template>
-  <ContentWrap :bodyStyle="{ padding: '10px 20px' }" class="position-relative">
-    <div class="processInstance-wrap-main">
-      <el-scrollbar>
-        <img
-          class="position-absolute right-20px"
-          width="150"
-          :src="auditIcons[processInstance.status]"
-          alt=""
-        />
-        <div class="text-#878c93 h-15px">编号:{{ id }}</div>
-        <el-divider class="!my-8px" />
-        <div class="flex items-center gap-5 mb-10px h-40px">
-          <div class="text-26px font-bold mb-5px">{{ processInstance.name }}</div>
-          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="processInstance.status" />
-        </div>
-
-        <div class="flex items-center gap-5 mb-10px text-13px h-35px">
-          <div
-            class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600"
-          >
-            <img class="rounded-full h-28px" src="@/assets/imgs/avatar.jpg" alt="" />
-            {{ processInstance?.startUser?.nickname }}
-          </div>
-          <div class="text-#878c93"> {{ formatDate(processInstance.startTime) }} 提交 </div>
-        </div>
-
-        <el-tabs v-model="activeTab">
-          <!-- 表单信息 -->
-          <el-tab-pane label="审批详情" name="form">
-            <div class="form-scroll-area">
-              <el-scrollbar>
-                <el-row :gutter="10">
-                  <el-col :span="18" class="!flex !flex-col formCol">
-                    <!-- 表单信息 -->
-                    <div
-                      v-loading="processInstanceLoading"
-                      class="form-box flex flex-col mb-30px flex-1"
-                    >
-                      <!-- 情况一:流程表单 -->
-                      <el-col
-                        v-if="processInstance?.processDefinition?.formType === 10"
-                        :offset="6"
-                        :span="16"
-                      >
-                        <form-create
-                          v-model="detailForm.value"
-                          v-model:api="fApi"
-                          :option="detailForm.option"
-                          :rule="detailForm.rule"
-                        />
-                      </el-col>
-                      <!-- 情况二:业务表单 -->
-                      <div v-if="processInstance?.processDefinition?.formType === 20">
-                        <BusinessFormComponent :id="processInstance.businessKey" />
-                      </div>
-                    </div>
-                  </el-col>
-                  <el-col :span="6">
-                    <!-- 审批记录时间线 -->
-                    <ProcessInstanceTimeline ref="timelineRef" :process-instance-id="id" />
-                  </el-col>
-                </el-row>
-              </el-scrollbar>
-            </div>
-          </el-tab-pane>
-          <!-- 流程图 -->
-          <el-tab-pane label="流程图" name="diagram">
-            <ProcessInstanceBpmnViewer
-              :id="`${id}`"
-              :bpmn-xml="bpmnXml"
-              :loading="processInstanceLoading"
-              :process-instance="processInstance"
-              :tasks="tasks"
-            />
-          </el-tab-pane>
-          <!-- 流转记录 -->
-          <el-tab-pane label="流转记录" name="record">
-            <ProcessInstanceTaskList
-              :loading="tasksLoad"
-              :process-instance="processInstance"
-              :tasks="tasks"
-              @refresh="getTaskList"
-            />
-          </el-tab-pane>
-          <!-- 流转评论 TODO 待开发 -->
-          <el-tab-pane label="流转评论" name="comment"> 流转评论 </el-tab-pane>
-        </el-tabs>
-
-        <div
-          class="b-t-solid border-t-1px border-[var(--el-border-color)]"
-          v-if="activeTab === 'form'"
-        >
-          <!-- 操作栏按钮 -->
-          <ProcessInstanceOperationButton
-            ref="operationButtonRef"
-            :processInstance="processInstance"
-            :userOptions="userOptions"
-            @success="refresh"
-          />
-        </div>
-      </el-scrollbar>
-    </div>
-  </ContentWrap>
-</template>
-<script lang="ts" setup>
-import { formatDate } from '@/utils/formatTime'
-import { DICT_TYPE } from '@/utils/dict'
-import { setConfAndFields2 } from '@/utils/formCreate'
-import type { ApiAttrs } from '@form-create/element-ui/types/config'
-import * as DefinitionApi from '@/api/bpm/definition'
-import * as ProcessInstanceApi from '@/api/bpm/processInstance'
-import * as TaskApi from '@/api/bpm/task'
-import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue'
-import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue'
-import ProcessInstanceOperationButton from './ProcessInstanceOperationButton.vue'
-import ProcessInstanceTimeline from './ProcessInstanceTimeline.vue'
-import { registerComponent } from '@/utils/routerHelper'
-import * as UserApi from '@/api/system/user'
-import { FieldPermissionType } from '@/components/SimpleProcessDesignerV2/src/consts'
-import audit1 from '@/assets/svgs/bpm/audit1.svg'
-import audit2 from '@/assets/svgs/bpm/audit2.svg'
-import audit3 from '@/assets/svgs/bpm/audit3.svg'
-
-defineOptions({ name: 'BpmProcessInstanceDetail' })
-const props = defineProps<{
-  id: string // 流程实例的编号
-  taskId?: string // 任务编号
-  activityId?: string //流程活动编号,用于抄送查看
-}>()
-const message = useMessage() // 消息弹窗
-const processInstanceLoading = ref(false) // 流程实例的加载中
-const processInstance = ref<any>({}) // 流程实例
-const operationButtonRef = ref()
-const timelineRef = ref()
-const bpmnXml = ref('') // BPMN XML
-const tasksLoad = ref(true) // 任务的加载中
-const tasks = ref<any[]>([]) // 任务列表
-const auditIcons = {
-  1: audit1,
-  2: audit2,
-  3: audit3
-}
-
-// ========== 申请信息 ==========
-const fApi = ref<ApiAttrs>() //
-const detailForm = ref({
-  rule: [],
-  option: {},
-  value: {}
-}) // 流程实例的表单详情
-
-/** 获得详情 */
-const getDetail = () => {
-  // 1. 获得流程实例相关
-  getProcessInstance()
-  // 2. 获得流程任务列表(审批记录)
-  getTaskList()
-}
-
-/** 加载流程实例 */
-const BusinessFormComponent = ref<any>(null) // 异步组件
-const getProcessInstance = async () => {
-  try {
-    processInstanceLoading.value = true
-    const data = await ProcessInstanceApi.getProcessInstance(props.id)
-    if (!data) {
-      message.error('查询不到流程信息!')
-      return
-    }
-    processInstance.value = data
-
-    // 设置表单信息
-    const processDefinition = data.processDefinition
-    if (processDefinition.formType === 10) {
-      // 获取表单字段权限
-      let fieldsPermission = undefined
-      if (props.taskId || props.activityId) {
-        fieldsPermission = await ProcessInstanceApi.getFormFieldsPermission({
-          processInstanceId: props.id,
-          taskId: props.taskId,
-          activityId: props.activityId
-        })
-      }
-      setConfAndFields2(
-        detailForm,
-        processDefinition.formConf,
-        processDefinition.formFields,
-        data.formVariables
-      )
-      nextTick().then(() => {
-        fApi.value?.btn.show(false)
-        fApi.value?.resetBtn.show(false)
-        fApi.value?.disabled(true)
-        if (fieldsPermission) {
-          Object.keys(fieldsPermission).forEach((item) => {
-            setFieldPermission(item, fieldsPermission[item])
-          })
-        }
-      })
-    } else {
-      // 注意:data.processDefinition.formCustomViewPath 是组件的全路径,例如说:/crm/contract/detail/index.vue
-      BusinessFormComponent.value = registerComponent(data.processDefinition.formCustomViewPath)
-    }
-
-    // 加载流程图
-    bpmnXml.value = (await DefinitionApi.getProcessDefinition(processDefinition.id))?.bpmnXml
-  } finally {
-    processInstanceLoading.value = false
-  }
-}
-
-/**
- * 设置表单权限
- */
-const setFieldPermission = (field: string, permission: string) => {
-  if (permission === FieldPermissionType.READ) {
-    fApi.value?.disabled(true, field)
-  }
-  if (permission === FieldPermissionType.WRITE) {
-    fApi.value?.disabled(false, field)
-  }
-  if (permission === FieldPermissionType.NONE) {
-    fApi.value?.hidden(true, field)
-  }
-}
-
-/** 加载任务列表 */
-const getTaskList = async () => {
-  try {
-    // 获得未取消的任务
-    tasksLoad.value = true
-    const data = await TaskApi.getTaskListByProcessInstanceId(props.id)
-    tasks.value = []
-    // 1.1 移除已取消的审批
-    data.forEach((task: any) => {
-      if (task.status !== 4) {
-        tasks.value.push(task)
-      }
-    })
-    // 1.2 排序,将未完成的排在前面,已完成的排在后面;
-    tasks.value.sort((a, b) => {
-      // 有已完成的情况,按照完成时间倒序
-      if (a.endTime && b.endTime) {
-        return b.endTime - a.endTime
-      } else if (a.endTime) {
-        return 1
-      } else if (b.endTime) {
-        return -1
-        // 都是未完成,按照创建时间倒序
-      } else {
-        return b.createTime - a.createTime
-      }
-    })
-
-    // 获得需要自己审批的任务
-    operationButtonRef.value?.loadRunningTask(tasks.value)
-  } finally {
-    tasksLoad.value = false
-  }
-}
-
-/**
- * 操作成功后刷新
- */
-const refresh = () => {
-  // 重新获取详情
-  getDetail()
-  // 刷新审批详情 Timeline
-  timelineRef.value?.refresh()
-}
-
-/** 当前的Tab */
-const activeTab = ref('form')
-
-/** 初始化 */
-const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
-onMounted(async () => {
-  getDetail()
-  // 获得用户列表
-  userOptions.value = await UserApi.getSimpleUserList()
-})
-</script>
-
-<style lang="scss" scoped>
-$wrap-padding-height: 30px;
-$wrap-margin-height: 15px;
-$button-height: 51px;
-$process-header-height: 194px;
-
-.processInstance-wrap-main {
-  height: calc(
-    100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px
-  );
-  max-height: calc(
-    100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px
-  );
-  overflow: auto;
-
-  .form-scroll-area {
-    height: calc(
-      100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px -
-        $process-header-height - 40px
-    );
-    max-height: calc(
-      100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px -
-        $process-header-height - 40px
-    );
-    overflow: auto;
-  }
-}
-
-.form-box {
-  :deep(.el-card) {
-    border: none;
-  }
-}
-</style>

+ 104 - 53
src/views/bpm/processInstance/index.vue

@@ -10,7 +10,7 @@
       :inline="true"
       label-width="68px"
     >
-      <el-form-item label="流程名称" prop="name">
+      <el-form-item label="" prop="name">
         <el-input
           v-model="queryParams.name"
           placeholder="请输入流程名称"
@@ -19,21 +19,19 @@
           class="!w-240px"
         />
       </el-form-item>
-      <el-form-item label="所属流程" prop="processDefinitionKey">
-        <el-input
-          v-model="queryParams.processDefinitionKey"
-          placeholder="请输入流程定义的标识"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
+
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
       </el-form-item>
-      <el-form-item label="流程分类" prop="category">
+
+      <!-- TODO @ tuituji:style 可以使用 unocss -->
+      <el-form-item label="" prop="category" :style="{ position: 'absolute', right: '130px' }">
+        <!-- TODO @tuituji:应该选择好分类,就触发搜索啦。 -->
         <el-select
           v-model="queryParams.category"
           placeholder="请选择流程分类"
           clearable
-          class="!w-240px"
+          class="!w-155px"
         >
           <el-option
             v-for="category in categoryList"
@@ -43,43 +41,79 @@
           />
         </el-select>
       </el-form-item>
-      <el-form-item label="流程状态" prop="status">
-        <el-select
-          v-model="queryParams.status"
-          placeholder="请选择流程状态"
-          clearable
-          class="!w-240px"
-        >
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="发起时间" prop="createTime">
-        <el-date-picker
-          v-model="queryParams.createTime"
-          value-format="YYYY-MM-DD HH:mm:ss"
-          type="daterange"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item>
-        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
-        <el-button
-          type="primary"
-          plain
-          v-hasPermi="['bpm:process-instance:query']"
-          @click="handleCreate(undefined)"
-        >
-          <Icon icon="ep:plus" class="mr-5px" /> 发起流程
+
+      <!-- 高级筛选 -->
+      <!-- TODO @ tuituji:style 可以使用 unocss -->
+      <el-form-item :style="{ position: 'absolute', right: '0px' }">
+        <el-button v-popover="popoverRef" v-click-outside="onClickOutside" :icon="List">
+          高级筛选
         </el-button>
+        <el-popover
+          ref="popoverRef"
+          trigger="click"
+          virtual-triggering
+          persistent
+          :width="400"
+          :show-arrow="false"
+          placement="bottom-end"
+        >
+          <el-form-item label="流程发起人" class="bold-label" label-position="top" prop="category">
+            <el-select
+              v-model="queryParams.category"
+              placeholder="请选择流程发起人"
+              clearable
+              class="!w-390px"
+            >
+              <el-option
+                v-for="category in categoryList"
+                :key="category.code"
+                :label="category.name"
+                :value="category.code"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item
+            label="所属流程"
+            class="bold-label"
+            label-position="top"
+            prop="processDefinitionKey"
+          >
+            <el-input
+              v-model="queryParams.processDefinitionKey"
+              placeholder="请输入流程定义的标识"
+              clearable
+              @keyup.enter="handleQuery"
+              class="!w-390px"
+            />
+          </el-form-item>
+          <el-form-item label="流程状态" class="bold-label" label-position="top" prop="status">
+            <el-select
+              v-model="queryParams.status"
+              placeholder="请选择流程状态"
+              clearable
+              class="!w-390px"
+            >
+              <el-option
+                v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="发起时间" class="bold-label" label-position="top" prop="createTime">
+            <el-date-picker
+              v-model="queryParams.createTime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              type="daterange"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+              class="!w-240px"
+            />
+          </el-form-item>
+        </el-popover>
+        <!-- TODO @tuituji:这里应该有确认,和取消、清空搜索条件,三个按钮。 -->
       </el-form-item>
     </el-form>
   </ContentWrap>
@@ -95,6 +129,8 @@
         min-width="100"
         fixed="left"
       />
+      <!-- TODO @芋艿:摘要 -->
+      <!-- TODO @tuituji:流程状态。可见需求文档里  -->
       <el-table-column label="流程状态" prop="status" width="120">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
@@ -114,7 +150,7 @@
         width="180"
         :formatter="dateFormatter"
       />
-      <el-table-column align="center" label="耗时" prop="durationInMillis" width="160">
+      <!--<el-table-column align="center" label="耗时" prop="durationInMillis" width="160">
         <template #default="scope">
           {{ scope.row.durationInMillis > 0 ? formatPast2(scope.row.durationInMillis) : '-' }}
         </template>
@@ -126,7 +162,7 @@
           </el-button>
         </template>
       </el-table-column>
-      <el-table-column label="流程编号" align="center" prop="id" min-width="320px" />
+      -->
       <el-table-column label="操作" align="center" fixed="right" width="180">
         <template #default="scope">
           <el-button
@@ -162,11 +198,13 @@
   </ContentWrap>
 </template>
 <script lang="ts" setup>
+// TODO @tuituji:List 改成 <Icon icon="ep:plus" class="mr-5px" /> 类似这种组件哈。
+import { List } from '@element-plus/icons-vue'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { dateFormatter, formatPast2 } from '@/utils/formatTime'
+import { dateFormatter } from '@/utils/formatTime'
 import { ElMessageBox } from 'element-plus'
 import * as ProcessInstanceApi from '@/api/bpm/processInstance'
-import { CategoryApi } from '@/api/bpm/category'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
 import { ProcessInstanceVO } from '@/api/bpm/processInstance'
 import * as DefinitionApi from '@/api/bpm/definition'
 
@@ -189,7 +227,7 @@ const queryParams = reactive({
   createTime: []
 })
 const queryFormRef = ref() // 搜索的表单
-const categoryList = ref([]) // 流程分类列表
+const categoryList = ref<CategoryVO[]>([]) // 流程分类列表
 
 /** 查询列表 */
 const getList = async () => {
@@ -222,7 +260,6 @@ const handleCreate = async (row?: ProcessInstanceVO) => {
     const processDefinitionDetail = await DefinitionApi.getProcessDefinition(
       row.processDefinitionId
     )
-    debugger
     if (processDefinitionDetail.formType === 20) {
       message.error('重新发起流程失败,原因:该流程使用业务表单,不支持重新发起')
       return
@@ -261,6 +298,15 @@ const handleCancel = async (row) => {
   await getList()
 }
 
+// TODO @tuituji:这个 import 是不是没用哈?
+import { ClickOutside as vClickOutside } from 'element-plus'
+
+// TODO @tuituji:onClickAdvancedSearch。方法名叫这个,会更好一些哇?打开高级搜索。
+const popoverRef = ref()
+const onClickOutside = () => {
+  unref(popoverRef).popperRef?.delayHide?.()
+}
+
 /** 激活时 **/
 onActivated(() => {
   getList()
@@ -272,3 +318,8 @@ onMounted(async () => {
   categoryList.value = await CategoryApi.getCategorySimpleList()
 })
 </script>
+<style>
+.bold-label .el-form-item__label {
+  font-weight: bold; /* 将字体加粗 */
+}
+</style>

+ 8 - 2
src/views/bpm/simpleWorkflow/index.vue → src/views/bpm/simple/SimpleModelDesign.vue

@@ -1,13 +1,19 @@
 <template>
-  <SimpleProcessDesigner :model-id="modelId" />
+  <ContentWrap :bodyStyle="{ padding: '20px 16px' }">
+    <SimpleProcessDesigner :model-id="modelId" @success="close" />
+  </ContentWrap>
 </template>
 <script setup lang="ts">
 import { SimpleProcessDesigner } from '@/components/SimpleProcessDesignerV2/src/'
 
 defineOptions({
-  name: 'SimpleWorkflowDesignEditor'
+  name: 'SimpleModelDesign'
 })
+const router = useRouter() // 路由
 const { query } = useRoute() // 路由的查询
 const modelId = query.modelId as string
+const close = () => {
+  router.push({ path: '/bpm/manager/model' })
+}
 </script>
 <style lang="scss" scoped></style>

+ 11 - 3
src/views/bpm/task/copy/index.vue

@@ -45,7 +45,12 @@
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
       <el-table-column align="center" label="流程名" prop="processInstanceName" min-width="180" />
-      <el-table-column align="center" label="流程发起人" prop="startUserName" min-width="100" />
+      <el-table-column
+        align="center"
+        label="流程发起人"
+        prop="startUser.nickname"
+        min-width="100"
+      />
       <el-table-column
         :formatter="dateFormatter"
         align="center"
@@ -53,8 +58,11 @@
         prop="processInstanceStartTime"
         width="180"
       />
-      <el-table-column align="center" label="抄送任务" prop="taskName" min-width="180" />
-      <el-table-column align="center" label="抄送人" prop="creatorName" min-width="100" />
+      <el-table-column align="center" label="抄送节点" prop="activityName" min-width="180" />
+      <el-table-column align="center" label="抄送人" min-width="100">
+        <template #default="scope"> {{ scope.row.createUser?.nickname || '系统' }} </template>
+      </el-table-column>
+      <el-table-column align="center" label="抄送意见" prop="reason" width="150" />
       <el-table-column
         align="center"
         label="抄送时间"

+ 5 - 1
src/views/infra/build/index.vue

@@ -49,7 +49,11 @@ const designerConfig = ref({
   switchType: [], // 是否可以切换组件类型,或者可以相互切换的字段
   autoActive: true, // 是否自动选中拖入的组件
   useTemplate: false, // 是否生成vue2语法的模板组件
-  formOptions: {}, // 定义表单配置默认值
+  formOptions: {
+    form: {
+      labelWidth: '100px' // 设置默认的 label 宽度为 100px
+    }
+  }, // 定义表单配置默认值
   fieldReadonly: false, // 配置field是否可以编辑
   hiddenDragMenu: false, // 隐藏拖拽操作按钮
   hiddenDragBtn: false, // 隐藏拖拽按钮

+ 132 - 24
src/views/mall/trade/delivery/pickUpOrder/index.vue

@@ -26,9 +26,8 @@
         <el-select
           v-model="queryParams.pickUpStoreId"
           class="!w-280px"
-          clearable
-          multiple
           placeholder="全部"
+          @change="handleQuery"
         >
           <el-option
             v-for="item in pickUpStoreList"
@@ -73,10 +72,22 @@
           <Icon class="mr-5px" icon="ep:refresh" />
           重置
         </el-button>
-        <el-button @click="handlePickup" type="success" plain v-hasPermi="['trade:order:pick-up']">
+        <el-button
+          @click="handlePickup"
+          type="success"
+          plain
+          v-hasPermi="['trade:order:pick-up']"
+          :disabled="isUse"
+        >
           <Icon class="mr-5px" icon="ep:check" />
           核销
         </el-button>
+        <el-button type="primary" @click="connectToSerialPort" :disabled="serialPort || isUse">
+          连接扫描枪
+        </el-button>
+        <el-button type="danger" @click="cutPort" :disabled="!serialPort || isUse">
+          断开扫描枪
+        </el-button>
       </el-form-item>
     </el-form>
   </ContentWrap>
@@ -216,18 +227,20 @@ import { DeliveryTypeEnum } from '@/utils/constants'
 import { TradeOrderSummaryRespVO } from '@/api/mall/trade/order'
 import { DeliveryPickUpStoreVO } from '@/api/mall/trade/delivery/pickUpStore'
 import OrderPickUpForm from '@/views/mall/trade/order/form/OrderPickUpForm.vue'
+import { ref, onMounted } from 'vue'
+import { useUserStore } from '@/store/modules/user'
+const message = useMessage() // 消息弹窗
+
+const port = ref('')
+const ports = ref([])
+const reader = ref('')
 
 defineOptions({ name: 'PickUpOrder' })
 
-// 列表的加载中
-const loading = ref(true)
-// 列表的总页数
-const total = ref(2)
-// 列表的数据
-const list = ref<TradeOrderApi.OrderVO[]>([])
-// 搜索的表单
-const queryFormRef = ref<FormInstance>()
-// 初始表单参数
+const loading = ref(true) // 列表的加载中
+const total = ref(2) // 列表的总页数
+const list = ref<TradeOrderApi.OrderVO[]>([]) // 列表的数据
+const queryFormRef = ref<FormInstance>() // 搜索的表单
 const INIT_QUERY_PARAMS = {
   // 页数
   pageNo: 1,
@@ -238,14 +251,15 @@ const INIT_QUERY_PARAMS = {
   // 配送方式
   deliveryType: DeliveryTypeEnum.PICK_UP.type,
   // 自提门店
-  pickUpStoreId: undefined
-}
-// 表单搜索
-const queryParams = ref({ ...INIT_QUERY_PARAMS })
-// 订单搜索类型 queryParam
-const queryType = reactive({ queryParam: 'no' })
-// 订单统计数据
-const summary = ref<TradeOrderSummaryRespVO>()
+  pickUpStoreId: -1
+} // 初始表单参数
+
+const queryParams = ref({ ...INIT_QUERY_PARAMS }) // 表单搜索
+const queryType = reactive({ queryParam: 'no' }) // 订单搜索类型 queryParam
+const summary = ref<TradeOrderSummaryRespVO>() // 订单统计数据
+
+const serialPort = ref(false) // 是否连接扫码枪
+const isUse = ref(true) // 是否可核销
 
 // 订单聚合搜索 select 类型配置(动态搜索)
 const dynamicSearchList = ref([
@@ -294,13 +308,21 @@ const handleQuery = async () => {
 const resetQuery = () => {
   queryFormRef.value?.resetFields()
   queryParams.value = { ...INIT_QUERY_PARAMS }
+  if (pickUpStoreList.value.length > 0) {
+    queryParams.value.pickUpStoreId = pickUpStoreList.value[0].id
+  }
   handleQuery()
 }
 
 /** 自提门店精简列表 */
 const pickUpStoreList = ref<DeliveryPickUpStoreVO[]>([])
 const getPickUpStoreList = async () => {
-  pickUpStoreList.value = await PickUpStoreApi.getListAllSimple()
+  pickUpStoreList.value = await PickUpStoreApi.getSimpleDeliveryPickUpStoreList()
+  // 移除自己无法核销的门店
+  const userId = useUserStore().getUser.id
+  pickUpStoreList.value = pickUpStoreList.value.filter((item) =>
+    item.verifyUserIds?.includes(userId)
+  )
 }
 
 /** 显示核销表单 */
@@ -309,10 +331,96 @@ const handlePickup = () => {
   pickUpForm.value.open()
 }
 
+/** 连接扫码枪 */
+const connectToSerialPort = async () => {
+  try {
+    // 判断浏览器支持串口通信
+    if (
+      'serial' in navigator &&
+      navigator.serial != null &&
+      typeof navigator.serial === 'object' &&
+      'requestPort' in navigator.serial
+    ) {
+      // 提示用户选择一个串口
+      port.value = await navigator.serial.requestPort()
+    } else {
+      message.error('浏览器不支持扫码枪连接,请更换浏览器重试')
+      return
+    }
+
+    // 获取用户之前授予该网站访问权限的所有串口。
+    ports.value = await navigator.serial.getPorts()
+
+    // console.log(port.value, ports.value);
+    // console.log(port.value)
+    // 等待串口打开
+    await port.value.open({ baudRate: 9600, dataBits: 8, stopBits: 2 })
+
+    // console.log(typeof port.value);
+    message.success('成功连接扫码枪')
+    serialPort.value = true
+    // readData(port.value);
+    readData()
+  } catch (error) {
+    // 处理连接串口出错的情况
+    console.log('Error connecting to serial port:', error)
+  }
+}
+
+/** 监听扫码枪输入 */
+const readData = async () => {
+  reader.value = port.value.readable.getReader()
+  let data = '' //扫码数据
+  // 监听来自串口的数据
+  while (true) {
+    const { value, done } = await reader.value.read()
+    if (done) {
+      // 允许稍后关闭串口
+      reader.value.releaseLock()
+      break
+    }
+    // 获取发送的数据
+    const serialData = new TextDecoder().decode(value)
+    data = `${data}${serialData}`
+    if (serialData.includes('\r')) {
+      //读取结束
+      let codeData = data.replace('\r', '')
+      data = '' //清空下次读取不会叠加
+      console.log(`二维码数据:${codeData}`)
+      //处理拿到数据逻辑
+      pickUpForm.value.open(codeData)
+    }
+  }
+}
+
+/** 断开扫码枪 */
+const cutPort = async () => {
+  if (port.value !== '') {
+    await reader.value.cancel()
+    await port.value.close()
+    port.value = ''
+    console.log('断开扫码枪连接')
+    message.success('已成功断开扫码枪连接')
+    serialPort.value = false
+  } else {
+    message.warning('请先连接或打开扫码枪')
+  }
+}
+
 /** 初始化 **/
-onMounted(() => {
-  getList()
-  getPickUpStoreList()
+onMounted(async () => {
+  await getPickUpStoreList()
+  if (pickUpStoreList.value.length === 0) {
+    message.error('当前登录人没绑定任何自提点')
+    loading.value = false
+    isUse.value = true
+    return
+  }
+
+  // 查询
+  queryParams.value.pickUpStoreId = pickUpStoreList.value[0].id
+  isUse.value = false
+  await getList()
 })
 </script>
 <style lang="scss" scoped>

+ 143 - 0
src/views/mall/trade/delivery/pickUpStore/DeliveryPickUpStoreBindForm.vue

@@ -0,0 +1,143 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible" width="20%">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+      v-loading="formLoading"
+    >
+      <el-row>
+        <el-col :span="24">
+          <el-form-item label="门店名称" prop="name">
+            <el-input v-model="formData.name" placeholder="请输入门店名称" readonly />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="24">
+          <el-form-item label="门店店员" prop="verifyUserIds">
+            <el-button type="primary" @click="storeStaffTableSelect.open()">选择店员</el-button>
+          </el-form-item>
+          <!-- 店员列表 -->
+          <ContentWrap v-if="formData.verifyUsers.length > 0">
+            <el-table :data="formData.verifyUsers">
+              <el-table-column label="编号" align="center" prop="id" />
+              <el-table-column
+                label="用户昵称"
+                align="center"
+                prop="nickname"
+                :show-overflow-tooltip="true"
+              />
+              <el-table-column label="状态" align="center" key="status">
+                <template #default="scope">
+                  <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+                </template>
+              </el-table-column>
+              <el-table-column align="center" label="操作">
+                <template #default="scope">
+                  <el-button
+                    v-hasPermi="['trade:delivery:pick-up-store:delete']"
+                    link
+                    type="danger"
+                    @click="handleDelete(scope.row.id)"
+                  >
+                    删除
+                  </el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </ContentWrap>
+        </el-col>
+      </el-row>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+
+  <!-- 选择员工弹窗 -->
+  <StoreStaffTableSelect ref="storeStaffTableSelect" @change="handleSelect" />
+</template>
+<script setup lang="ts">
+import * as DeliveryPickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
+import StoreStaffTableSelect from './components/StoreStaffTableSelect.vue'
+import { DICT_TYPE } from '@/utils/dict'
+
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+  id: undefined,
+  name: '',
+  verifyUserIds: [],
+  verifyUsers: []
+})
+const formRules = reactive({})
+const formRef = ref() // 表单 Ref
+const storeStaffTableSelect = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (id: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = '绑定自提门店员工'
+  resetForm()
+  formLoading.value = true
+  try {
+    formData.value = await DeliveryPickUpStoreApi.getDeliveryPickUpStore(id)
+  } finally {
+    formLoading.value = false
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = {
+      id: formData.value.id,
+      verifyUserIds: formData.value.verifyUsers.map((item: any) => item.id)
+    }
+    await DeliveryPickUpStoreApi.bindStoreStaffId(data)
+    message.success('绑定成功')
+    dialogVisible.value = false
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 处理选择员工操作 */
+const handleSelect = (checkedUsers: []) => {
+  formData.value.verifyUsers = checkedUsers
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  const index = formData.value.verifyUsers.findIndex((item: any) => {
+    if (item.id == id) {
+      return true
+    }
+  })
+  formData.value.verifyUsers.splice(index, 1)
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: '',
+    verifyUserIds: [],
+    verifyUsers: []
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 265 - 0
src/views/mall/trade/delivery/pickUpStore/components/StoreStaffTableSelect.vue

@@ -0,0 +1,265 @@
+<!-- TODO 芋艿:这块后续抽个独立的组件出来 -->
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible" width="60%">
+    <el-row :gutter="20">
+      <!-- 左侧部门树 -->
+      <el-col :span="4" :xs="24">
+        <ContentWrap class="h-1/1">
+          <DeptTree @node-click="handleDeptNodeClick" />
+        </ContentWrap>
+      </el-col>
+      <el-col :span="20" :xs="24">
+        <!-- 搜索 -->
+        <ContentWrap>
+          <el-form
+            class="-mb-15px"
+            :model="queryParams"
+            ref="queryFormRef"
+            :inline="true"
+            label-width="68px"
+          >
+            <el-form-item label="用户名称" prop="username">
+              <el-input
+                v-model="queryParams.username"
+                placeholder="请输入用户名称"
+                clearable
+                @keyup.enter="handleQuery"
+                class="!w-240px"
+              />
+            </el-form-item>
+            <el-form-item label="手机号码" prop="mobile">
+              <el-input
+                v-model="queryParams.mobile"
+                placeholder="请输入手机号码"
+                clearable
+                @keyup.enter="handleQuery"
+                class="!w-240px"
+              />
+            </el-form-item>
+            <el-form-item label="状态" prop="status">
+              <el-select
+                v-model="queryParams.status"
+                placeholder="用户状态"
+                clearable
+                class="!w-240px"
+              >
+                <el-option
+                  v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="创建时间" prop="createTime">
+              <el-date-picker
+                v-model="queryParams.createTime"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                type="datetimerange"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                class="!w-240px"
+              />
+            </el-form-item>
+            <el-form-item>
+              <el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button>
+              <el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button>
+            </el-form-item>
+          </el-form>
+        </ContentWrap>
+        <ContentWrap>
+          <el-table v-loading="loading" :data="list">
+            <el-table-column width="55">
+              <template #header>
+                <el-checkbox
+                  v-model="isCheckAll"
+                  :indeterminate="isIndeterminate"
+                  @change="handleCheckAll"
+                />
+              </template>
+              <template #default="{ row }">
+                <el-checkbox
+                  v-model="checkedStatus[row.id]"
+                  @change="(checked: boolean) => handleCheckOne(checked, row, true)"
+                />
+              </template>
+            </el-table-column>
+            <el-table-column label="用户编号" align="center" key="id" prop="id" />
+            <el-table-column
+              label="用户名称"
+              align="center"
+              prop="username"
+              :show-overflow-tooltip="true"
+            />
+            <el-table-column
+              label="用户昵称"
+              align="center"
+              prop="nickname"
+              :show-overflow-tooltip="true"
+            />
+            <el-table-column
+              label="部门"
+              align="center"
+              key="deptName"
+              prop="deptName"
+              :show-overflow-tooltip="true"
+            />
+            <el-table-column label="手机号码" align="center" prop="mobile" width="120" />
+            <el-table-column label="状态" key="status">
+              <template #default="scope">
+                <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+              </template>
+            </el-table-column>
+            <el-table-column
+              label="创建时间"
+              align="center"
+              prop="createTime"
+              :formatter="dateFormatter"
+              width="180"
+            />
+          </el-table>
+          <Pagination
+            :total="total"
+            v-model:page="queryParams.pageNo"
+            v-model:limit="queryParams.pageSize"
+            @pagination="getList"
+          />
+        </ContentWrap>
+      </el-col>
+    </el-row>
+    <template #footer>
+      <el-button type="primary" @click="handleEmitChange">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as UserApi from '@/api/system/user'
+import DeptTree from '@/views/system/user/DeptTree.vue'
+
+// 是否全选
+const isCheckAll = ref(false)
+// 全选框是否处于中间状态:不是全部选中 && 任意一个选中
+const isIndeterminate = ref(false)
+// 选中的活动
+const checkedUsers = ref([])
+// 选中状态:key为用户ID,value为是否选中
+const checkedStatus = ref<Record<string, boolean>>({})
+
+const dialogTitle = '选择店员'
+const dialogVisible = ref(false)
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  username: undefined,
+  mobile: undefined,
+  status: undefined,
+  deptId: undefined,
+  roleId: 5,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await UserApi.getUserPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 处理部门被点击 */
+const handleDeptNodeClick = async (row) => {
+  queryParams.deptId = row.id
+  await getList()
+}
+
+/** 打开弹窗 */
+const open = async () => {
+  dialogVisible.value = true
+  loading.value = true
+  try {
+    await getList()
+  } finally {
+    loading.value = false
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 全选/全不选 */
+const handleCheckAll = (checked: boolean) => {
+  isCheckAll.value = checked
+  isIndeterminate.value = false
+
+  list.value.forEach((combinationActivity) => handleCheckOne(checked, combinationActivity, false))
+}
+
+/**
+ * 选中一行
+ * @param checked 是否选中
+ * @param combinationActivity 活动
+ * @param isCalcCheckAll 是否计算全选
+ */
+const handleCheckOne = (checked: boolean, combinationActivity, isCalcCheckAll: boolean) => {
+  if (checked) {
+    checkedUsers.value.push(combinationActivity as never)
+    checkedStatus.value[combinationActivity.id] = true
+  } else {
+    const index = findCheckedIndex(combinationActivity)
+    if (index > -1) {
+      checkedUsers.value.splice(index, 1)
+      checkedStatus.value[combinationActivity.id] = false
+      isCheckAll.value = false
+    }
+  }
+
+  // 计算全选框状态
+  if (isCalcCheckAll) {
+    calculateIsCheckAll()
+  }
+}
+
+// 查找活动在已选中活动列表中的索引
+const findCheckedIndex = (user) => checkedUsers.value.findIndex((item) => item.id === user.id)
+
+// 计算全选框状态
+const calculateIsCheckAll = () => {
+  isCheckAll.value = list.value.every((user) => checkedStatus.value[user.id])
+  // 计算中间状态:不是全部选中 && 任意一个选中
+  isIndeterminate.value =
+    !isCheckAll.value && list.value.some((user) => checkedStatus.value[user.id])
+}
+
+/** 多选完成 */
+const handleEmitChange = () => {
+  // 关闭弹窗
+  dialogVisible.value = false
+  emits('change', [...checkedUsers.value])
+}
+
+/** 确认选择时的触发事件 */
+const emits = defineEmits<{
+  change: [CombinationActivityApi: any]
+}>()
+</script>

+ 18 - 1
src/views/mall/trade/delivery/pickUpStore/index.vue

@@ -93,7 +93,7 @@
         prop="createTime"
         width="180"
       />
-      <el-table-column align="center" label="操作">
+      <el-table-column align="center" label="操作" min-width="110">
         <template #default="scope">
           <el-button
             v-hasPermi="['trade:delivery:pick-up-store:update']"
@@ -103,6 +103,14 @@
           >
             编辑
           </el-button>
+          <el-button
+            v-hasPermi="['trade:delivery:pick-up-store:update']"
+            link
+            type="primary"
+            @click="openFormBind(scope.row.id)"
+          >
+            绑定店员
+          </el-button>
           <el-button
             v-hasPermi="['trade:delivery:pick-up-store:delete']"
             link
@@ -115,12 +123,16 @@
       </el-table-column>
     </el-table>
   </ContentWrap>
+
   <!-- 表单弹窗:添加/修改 -->
   <DeliveryPickUpStoreForm ref="formRef" @success="getList" />
+  <!-- 表单弹窗:绑定店员 -->
+  <DeliveryPickUpStoreBindForm ref="formBindRef" />
 </template>
 <script lang="ts" name="DeliveryPickUpStore" setup>
 import * as DeliveryPickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
 import DeliveryPickUpStoreForm from './PickUpStoreForm.vue'
+import DeliveryPickUpStoreBindForm from './DeliveryPickUpStoreBindForm.vue'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
 
@@ -146,6 +158,11 @@ const openForm = (type: string, id?: number) => {
   formRef.value.open(type, id)
 }
 
+const formBindRef = ref()
+const openFormBind = (id?: number) => {
+  formBindRef.value.open(id)
+}
+
 /** 删除按钮操作 */
 const handleDelete = async (id: number) => {
   try {

+ 14 - 6
src/views/mall/trade/order/form/OrderPickUpForm.vue

@@ -13,7 +13,7 @@
       </el-form-item>
     </el-form>
     <template #footer>
-      <el-button type="primary" :disabled="formLoading" @click="getOrderByPickUpVerifyCode">
+      <el-button type="primary" :disabled="formLoading" @click="getOrderByPickUpVerifyCodeClick">
         查询
       </el-button>
       <el-button @click="dialogVisible = false">取 消</el-button>
@@ -52,9 +52,14 @@ const formRef = ref() // 表单 Ref
 const orderDetails = ref<OrderVO>({})
 
 /** 打开弹窗 */
-const open = async () => {
+const open = async (pickUpVerifyCode: string) => {
   resetForm()
-  dialogVisible.value = true
+  if(pickUpVerifyCode != null){
+    formData.value.pickUpVerifyCode = pickUpVerifyCode;
+    await getOrderByPickUpVerifyCode()
+  }else{
+    dialogVisible.value = true
+  }
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
@@ -83,18 +88,21 @@ const resetForm = () => {
   formRef.value?.resetFields()
 }
 
-/** 查询核销码对应的订单 */
-const getOrderByPickUpVerifyCode = async () => {
+const getOrderByPickUpVerifyCodeClick = async () => {
   // 校验表单
   if (!formRef) return
   const valid = await formRef.value.validate()
   if (!valid) return
+  await getOrderByPickUpVerifyCode()
+}
 
+/** 查询核销码对应的订单 */
+const getOrderByPickUpVerifyCode = async () => {
   formLoading.value = true
   const data = await TradeOrderApi.getOrderByPickUpVerifyCode(formData.value.pickUpVerifyCode)
   formLoading.value = false
   if (data?.deliveryType !== DeliveryTypeEnum.PICK_UP.type) {
-    message.error('请输入正确的核销码')
+    message.error('未查询到订单')
     return
   }
   if (data?.status !== TradeOrderStatusEnum.UNDELIVERED.status) {

+ 1 - 1
src/views/mall/trade/order/index.vue

@@ -351,7 +351,7 @@ const deliveryExpressList = ref<DeliveryExpressApi.DeliveryExpressVO[]>([]) // 
 /** 初始化 **/
 onMounted(async () => {
   await getList()
-  pickUpStoreList.value = await PickUpStoreApi.getListAllSimple()
+  pickUpStoreList.value = await PickUpStoreApi.getSimpleDeliveryPickUpStoreList()
   deliveryExpressList.value = await DeliveryExpressApi.getSimpleDeliveryExpressList()
 })
 </script>

+ 1 - 1
src/views/member/user/detail/UserOrderList.vue

@@ -273,7 +273,7 @@ const openDetail = (id: number) => {
 /** 初始化 **/
 onMounted(async () => {
   await getList()
-  pickUpStoreList.value = await PickUpStoreApi.getListAllSimple()
+  pickUpStoreList.value = await PickUpStoreApi.getSimpleDeliveryPickUpStoreList()
   deliveryExpressList.value = await DeliveryExpressApi.getSimpleDeliveryExpressList()
 })
 </script>

+ 6 - 1
src/views/pay/app/components/AppForm.vue

@@ -30,6 +30,9 @@
       <el-form-item label="退款结果的回调地址" prop="refundNotifyUrl">
         <el-input v-model="formData.refundNotifyUrl" placeholder="请输入退款结果的回调地址" />
       </el-form-item>
+      <el-form-item label="转账结果的回调地址" prop="transferNotifyUrl">
+        <el-input v-model="formData.transferNotifyUrl" placeholder="请输入转账结果的回调地址" />
+      </el-form-item>
       <el-form-item label="备注" prop="remark">
         <el-input v-model="formData.remark" placeholder="请输入备注" />
       </el-form-item>
@@ -62,7 +65,8 @@ const formData = ref({
   status: CommonStatusEnum.ENABLE,
   remark: undefined,
   orderNotifyUrl: undefined,
-  refundNotifyUrl: undefined
+  refundNotifyUrl: undefined,
+  transferNotifyUrl: undefined
 })
 const formRules = reactive({
   name: [{ required: true, message: '应用名不能为空', trigger: 'blur' }],
@@ -126,6 +130,7 @@ const resetForm = () => {
     remark: undefined,
     orderNotifyUrl: undefined,
     refundNotifyUrl: undefined,
+    transferNotifyUrl: undefined,
     appKey: undefined
   }
   formRef.value?.resetFields()

+ 0 - 1
src/views/pay/app/components/channel/WeixinChannelForm.vue

@@ -257,7 +257,6 @@ const resetForm = (appId, code) => {
 const fileBeforeUpload = (file, fileAccept) => {
   let format = '.' + file.name.split('.')[1]
   if (format !== fileAccept) {
-    debugger
     message.error('请上传指定格式"' + fileAccept + '"文件')
     return false
   }

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff