Răsfoiți Sursa

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

# Conflicts:
#	pnpm-lock.yaml
#	src/utils/index.ts
YunaiV 5 luni în urmă
părinte
comite
e29d6f910a
100 a modificat fișierele cu 4747 adăugiri și 1138 ștergeri
  1. BIN
      .image/common/ai-feature.png
  2. 70 42
      README.md
  3. 2 1
      build/vite/optimize.ts
  4. 4 2
      package.json
  5. 195 192
      pnpm-lock.yaml
  6. 8 1
      src/api/ai/chat/message/index.ts
  7. 2 3
      src/api/ai/image/index.ts
  8. 54 0
      src/api/ai/knowledge/document/index.ts
  9. 44 0
      src/api/ai/knowledge/knowledge/index.ts
  10. 75 0
      src/api/ai/knowledge/segment/index.ts
  11. 0 53
      src/api/ai/model/chatModel/index.ts
  12. 2 0
      src/api/ai/model/chatRole/index.ts
  13. 54 0
      src/api/ai/model/model/index.ts
  14. 42 0
      src/api/ai/model/tool/index.ts
  15. 6 0
      src/api/bpm/definition/index.ts
  16. 4 0
      src/api/bpm/model/index.ts
  17. 7 1
      src/api/bpm/processInstance/index.ts
  18. 1 1
      src/api/login/index.ts
  19. 1 1
      src/api/mall/product/spu.ts
  20. 1 0
      src/assets/svgs/bpm/child-process.svg
  21. 1 0
      src/assets/svgs/bpm/transactor.svg
  22. 3 2
      src/components/DiyEditor/components/ComponentContainerProperty.vue
  23. 2 2
      src/components/DiyEditor/components/mobile/Carousel/property.vue
  24. 22 7
      src/components/DiyEditor/components/mobile/CouponCard/property.vue
  25. 2 2
      src/components/DiyEditor/components/mobile/Divider/property.vue
  26. 2 2
      src/components/DiyEditor/components/mobile/FloatingActionButton/property.vue
  27. 2 2
      src/components/DiyEditor/components/mobile/HotZone/property.vue
  28. 2 2
      src/components/DiyEditor/components/mobile/ImageBar/property.vue
  29. 2 2
      src/components/DiyEditor/components/mobile/MagicCube/property.vue
  30. 2 2
      src/components/DiyEditor/components/mobile/MenuGrid/property.vue
  31. 2 2
      src/components/DiyEditor/components/mobile/MenuList/property.vue
  32. 2 2
      src/components/DiyEditor/components/mobile/MenuSwiper/property.vue
  33. 12 7
      src/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue
  34. 3 3
      src/components/DiyEditor/components/mobile/NavigationBar/index.vue
  35. 2 2
      src/components/DiyEditor/components/mobile/NavigationBar/property.vue
  36. 2 2
      src/components/DiyEditor/components/mobile/NoticeBar/property.vue
  37. 2 2
      src/components/DiyEditor/components/mobile/PageConfig/property.vue
  38. 2 2
      src/components/DiyEditor/components/mobile/Popover/property.vue
  39. 2 2
      src/components/DiyEditor/components/mobile/ProductCard/property.vue
  40. 2 2
      src/components/DiyEditor/components/mobile/ProductList/property.vue
  41. 2 2
      src/components/DiyEditor/components/mobile/PromotionArticle/property.vue
  42. 2 2
      src/components/DiyEditor/components/mobile/PromotionCombination/property.vue
  43. 2 2
      src/components/DiyEditor/components/mobile/PromotionPoint/property.vue
  44. 2 2
      src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue
  45. 2 2
      src/components/DiyEditor/components/mobile/SearchBar/property.vue
  46. 2 2
      src/components/DiyEditor/components/mobile/TabBar/property.vue
  47. 2 2
      src/components/DiyEditor/components/mobile/TitleBar/property.vue
  48. 2 2
      src/components/DiyEditor/components/mobile/UserCard/property.vue
  49. 2 2
      src/components/DiyEditor/components/mobile/UserCoupon/property.vue
  50. 2 2
      src/components/DiyEditor/components/mobile/UserOrder/property.vue
  51. 2 2
      src/components/DiyEditor/components/mobile/UserWallet/property.vue
  52. 2 2
      src/components/DiyEditor/components/mobile/VideoPlayer/property.vue
  53. 31 13
      src/components/DiyEditor/index.vue
  54. 0 29
      src/components/DiyEditor/util.ts
  55. 11 4
      src/components/Draggable/index.vue
  56. 1 1
      src/components/Editor/src/Editor.vue
  57. 1 1
      src/components/Icon/src/Icon.vue
  58. 4 28
      src/components/InputWithColor/index.vue
  59. 103 14
      src/components/SimpleProcessDesignerV2/src/NodeHandler.vue
  60. 26 1
      src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue
  61. 12 116
      src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue
  62. 124 7
      src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue
  63. 0 1
      src/components/SimpleProcessDesignerV2/src/SimpleProcessViewer.vue
  64. 308 14
      src/components/SimpleProcessDesignerV2/src/consts.ts
  65. 123 18
      src/components/SimpleProcessDesignerV2/src/node.ts
  66. 610 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/ChildProcessNodeConfig.vue
  67. 75 282
      src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue
  68. 23 5
      src/components/SimpleProcessDesignerV2/src/nodes-config/CopyTaskNodeConfig.vue
  69. 1 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/DelayTimerNodeConfig.vue
  70. 201 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/RouterNodeConfig.vue
  71. 22 4
      src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue
  72. 524 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/TriggerNodeConfig.vue
  73. 274 129
      src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue
  74. 276 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/components/Condition.vue
  75. 308 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/components/ConditionDialog.vue
  76. 188 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestParamSetting.vue
  77. 127 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestSetting.vue
  78. 88 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/components/UserTaskListener.vue
  79. 106 0
      src/components/SimpleProcessDesignerV2/src/nodes/ChildProcessNode.vue
  80. 1 2
      src/components/SimpleProcessDesignerV2/src/nodes/DelayTimerNode.vue
  81. 1 1
      src/components/SimpleProcessDesignerV2/src/nodes/EndEventNode.vue
  82. 7 4
      src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue
  83. 8 5
      src/components/SimpleProcessDesignerV2/src/nodes/InclusiveNode.vue
  84. 97 0
      src/components/SimpleProcessDesignerV2/src/nodes/RouterNode.vue
  85. 2 2
      src/components/SimpleProcessDesignerV2/src/nodes/StartUserNode.vue
  86. 97 0
      src/components/SimpleProcessDesignerV2/src/nodes/TriggerNode.vue
  87. 10 3
      src/components/SimpleProcessDesignerV2/src/nodes/UserTaskNode.vue
  88. BIN
      src/components/SimpleProcessDesignerV2/theme/iconfont.ttf
  89. BIN
      src/components/SimpleProcessDesignerV2/theme/iconfont.woff
  90. BIN
      src/components/SimpleProcessDesignerV2/theme/iconfont.woff2
  91. 88 17
      src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss
  92. 1 0
      src/components/Table/src/TableSelectForm.vue
  93. 17 27
      src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue
  94. 39 0
      src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json
  95. 1 1
      src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue
  96. 3 0
      src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
  97. 83 18
      src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue
  98. 21 12
      src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue
  99. 31 1
      src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
  100. 6 8
      src/components/bpmnProcessDesigner/package/theme/process-designer.scss

BIN
.image/common/ai-feature.png


+ 70 - 42
README.md

@@ -81,16 +81,13 @@
 
 系统内置多种多种业务功能,可以用于快速你的业务系统:
 
-* 系统功能
-* 基础设施
-* 工作流程
-* 支付系统
-* 会员中心
-* 数据报表
-* 商城系统
-* 微信公众号
-* ERP 系统
-* CRM 系统
+系统内置多种多种业务功能,可以用于快速你的业务系统:
+
+![功能分层](/.image/common/ruoyi-vue-pro-biz.png)
+
+* 通用模块(必选):系统功能、基础设施
+* 通用模块(可选):工作流程、支付系统、数据报表、会员中心
+* 业务系统(按需):ERP 系统、CRM 系统、商城系统、微信公众号、AI 大模型
 
 ### 系统功能
 
@@ -120,54 +117,77 @@
 
 ### 工作流程
 
-|    | 功能    | 描述                                      |
-|----|-------|-----------------------------------------|
-| 🚀 | 流程模型  | 配置工作流的流程模型,支持 BPMN 和仿钉钉/飞书设计器           |
-| 🚀 | 流程表单  | 拖动表单元素生成相应的工作流表单,覆盖 Element UI 所有的表单组件  |
-| 🚀 | 用户分组  | 自定义用户分组,可用于工作流的审批分组                     |
-| 🚀 | 我的流程  | 查看我发起的工作流程,支持新建、取消流程等操作,高亮流程图、审批时间线     |
-| 🚀 | 待办任务  | 查看自己【未】审批的工作任务,支持通过、不通过、转派、委派、退回、加减签等操作 |
-| 🚀 | 已办任务  | 查看自己【已】审批的工作任务,支持流程预测,展示未来审批人信息         |
-| 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批  |
-
 ![功能图](/.image/common/bpm-feature.png)
 
+基于 Flowable 构建,可支持信创(国产)数据库,满足中国特色流程操作:
+
 | BPMN 设计器                     | 钉钉/飞书设计器                       |
 |------------------------------|--------------------------------|
 | ![](/.image/工作流设计器-bpmn.jpg) | ![](/.image/工作流设计器-simple.jpg) |
 
+> 历经头部企业生产验证,工作流引擎须标配仿钉钉/飞书 + BPMN 双设计器!!!
+>
+> 前者支持轻量配置简单流程,后者实现复杂场景深度编排
+
+| 功能列表       | 功能描述                                                                                | 是否完成 |
+|------------|-------------------------------------------------------------------------------------|------|
+| SIMPLE 设计器 | 仿钉钉/飞书设计器,支持拖拽搭建表单流程,10 分钟快速完成审批流程配置                                                | ✅    |
+| BPMN 设计器   | 基于 BPMN 标准开发,适配复杂业务场景,满足多层级审批及流程自动化需求                                               | ✅    |
+| 会签         | 同一个审批节点设置多个人(如 A、B、C 三人,三人会同时收到待办任务),需全部同意之后,审批才可到下一审批节点                            | ✅    |
+| 或签         | 同一个审批节点设置多个人,任意一个人处理后,就能进入下一个节点                                                     | ✅    |
+| 依次审批       | (顺序会签)同一个审批节点设置多个人(如 A、B、C 三人),三人按顺序依次收到待办,即 A 先审批,A 提交后 B 才能审批,需全部同意之后,审批才可到下一审批节点 | ✅    |
+| 抄送         | 将审批结果通知给抄送人,同一个审批默认排重,不重复抄送给同一人                                                     | ✅    |
+| 驳回         | (退回)将审批重置发送给某节点,重新审批。可驳回至发起人、上一节点、任意节点                                              | ✅    |
+| 转办         | A 转给其 B 审批,B 审批后,进入下一节点                                                             | ✅    |
+| 委派         | A 转给其 B 审批,B 审批后,转给 A,A 继续审批后进入下一节点                                                 | ✅    |
+| 加签         | 允许当前审批人根据需要,自行增加当前节点的审批人,支持向前、向后加签                                                  | ✅    |
+| 减签         | (取消加签)在当前审批人操作之前,减少审批人                                                              | ✅    |
+| 撤销         | (取消流程)流程发起人,可以对流程进行撤销处理                                                             | ✅    |
+| 终止         | 系统管理员,在任意节点终止流程实例                                                                   | ✅    |
+| 表单权限       | 支持拖拉拽配置表单,每个审批节点可配置只读、编辑、隐藏权限                                                       | ✅    |
+| 超时审批       | 配置超时审批时间,超时后自动触发审批通过、不通过、驳回等操作                                                      | ✅    |
+| 自动提醒       | 配置提醒时间,到达时间后自动触发短信、邮箱、站内信等通知提醒,支持自定义重复提醒频次                                          | ✅    |
+| 父子流程       | 主流程设置子流程节点,子流程节点会自动触发子流程。子流程结束后,主流程才会执行(继续往下下执行),支持同步子流程、异步子流程                      | ✅    |
+| 条件分支       | (排它分支)用于在流程中实现决策,即根据条件选择一个分支执行                                                      | ✅    |
+| 并行分支       | 允许将流程分成多条分支,不进行条件判断,所有分支都会执行                                                        | ✅    |
+| 包容分支       | (条件分支 + 并行分支的结合体)允许基于条件选择多条分支执行,但如果没有任何一个分支满足条件,则可以选择默认分支                           | ✅    |
+| 路由分支       | 根据条件选择一个分支执行(重定向到指定配置节点),也可以选择默认分支执行(继续往下执行)                                        | ✅    |
+| 触发节点       | 执行到该节点,触发 HTTP 请求、HTTP 回调、更新数据、删除数据等                                                | ✅    |
+| 延迟节点       | 执行到该节点,审批等待一段时间再执行,支持固定时长、固定日期等                                                     | ✅    |
+| 拓展设置       | 流程前置/后置通知,节点(任务)前置、后置通知,流程报表,自动审批去重,自定流程编号、标题、摘要,流程报表等                              | ✅    |
+
 ### 支付系统
 
 |     | 功能   | 描述                        |
 |-----|------|---------------------------|
-| 🚀  | 商户信息 | 管理商户信息,支持 Saas 场景下的多商户功能  |
 | 🚀  | 应用信息 | 配置商户的应用信息,对接支付宝、微信等多个支付渠道 |
 | 🚀  | 支付订单 | 查看用户发起的支付宝、微信等的【支付】订单     |
 | 🚀  | 退款订单 | 查看用户发起的支付宝、微信等的【退款】订单     |
-
-ps:核心功能已经实现,正在对接微信小程序中...
+| 🚀  | 回调通知 | 查看支付回调业务的【支付】【退款】的通知结果    |
+| 🚀  | 接入示例 | 提供接入支付系统的【支付】【退款】的功能实战    |
 
 ### 基础设施
 
-|    | 功能       | 描述                                           |
-|----|----------|----------------------------------------------|
-| 🚀 | 代码生成     | 前后端代码的生成(Java、Vue、SQL、单元测试),支持 CRUD 下载       |
-| 🚀 | 系统接口     | 基于 Swagger 自动生成相关的 RESTful API 接口文档          |
-| 🚀 | 数据库文档    | 基于 Screw 自动生成数据库文档,支持导出 Word、HTML、MD 格式      |
-|    | 表单构建     | 拖动表单元素生成相应的 HTML 代码,支持导出 JSON、Vue 文件         |
-| 🚀 | 配置管理     | 对系统动态配置常用参数,支持 SpringBoot 加载                 |
-| ⭐️ | 定时任务     | 在线(添加、修改、删除)任务调度包含执行结果日志                     |
-| 🚀 | 文件服务     | 支持将文件存储到 S3(MinIO、阿里云、腾讯云、七牛云)、本地、FTP、数据库等   |
-| 🚀 | API 日志   | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题   |
-|    | MySQL 监控 | 监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈              |
-|    | Redis 监控 | 监控 Redis 数据库的使用情况,使用的 Redis Key 管理           |
-| 🚀 | 消息队列     | 基于 Redis 实现消息队列,Stream 提供集群消费,Pub/Sub 提供广播消费 |
-| 🚀 | Java 监控  | 基于 Spring Boot Admin 实现 Java 应用的监控           |
-| 🚀 | 链路追踪     | 接入 SkyWalking 组件,实现链路追踪                      |
-| 🚀 | 日志中心     | 接入 SkyWalking 组件,实现日志中心                      |
-| 🚀 | 服务保障     | 基于 Redis 实现分布式锁、幂等、限流功能,满足高并发场景              |
-| 🚀 | 日志服务     | 轻量级日志中心,查看远程服务器的日志                           |
-| 🚀 | 单元测试     | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等    |
+|     | 功能        | 描述                                           |
+|-----|-----------|----------------------------------------------|
+| 🚀  | 代码生成      | 前后端代码的生成(Java、Vue、SQL、单元测试),支持 CRUD 下载       |
+| 🚀  | 系统接口      | 基于 Swagger 自动生成相关的 RESTful API 接口文档          |
+| 🚀  | 数据库文档     | 基于 Screw 自动生成数据库文档,支持导出 Word、HTML、MD 格式      |
+|     | 表单构建      | 拖动表单元素生成相应的 HTML 代码,支持导出 JSON、Vue 文件         |
+| 🚀  | 配置管理      | 对系统动态配置常用参数,支持 SpringBoot 加载                 |
+| ⭐️  | 定时任务      | 在线(添加、修改、删除)任务调度包含执行结果日志                     |
+| 🚀  | 文件服务      | 支持将文件存储到 S3(MinIO、阿里云、腾讯云、七牛云)、本地、FTP、数据库等   | 
+| 🚀  | WebSocket | 提供 WebSocket 接入示例,支持一对一、一对多发送方式              | 
+| 🚀  | API 日志    | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题   |
+|     | MySQL 监控  | 监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈              |
+|     | Redis 监控  | 监控 Redis 数据库的使用情况,使用的 Redis Key 管理           |
+| 🚀  | 消息队列      | 基于 Redis 实现消息队列,Stream 提供集群消费,Pub/Sub 提供广播消费 |
+| 🚀  | Java 监控   | 基于 Spring Boot Admin 实现 Java 应用的监控           |
+| 🚀  | 链路追踪      | 接入 SkyWalking 组件,实现链路追踪                      |
+| 🚀  | 日志中心      | 接入 SkyWalking 组件,实现日志中心                      |
+| 🚀  | 服务保障      | 基于 Redis 实现分布式锁、幂等、限流功能,满足高并发场景              |
+| 🚀  | 日志服务      | 轻量级日志中心,查看远程服务器的日志                           |
+| 🚀  | 单元测试      | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等    |
 
 ![功能图](/.image/common/infra-feature.png)
 
@@ -213,6 +233,14 @@ ps:核心功能已经实现,正在对接微信小程序中...
 
 ![功能图](/.image/common/crm-feature.png)
 
+### AI 大模型
+
+演示地址:<https://doc.iocoder.cn/ai-preview/>
+
+![功能图](/.image/common/ai-feature.png)
+
+![功能图](/.image/common/ai-preview.gif)
+
 ## 🐷 演示图
 
 ### 系统功能

+ 2 - 1
build/vite/optimize.ts

@@ -114,7 +114,8 @@ const include = [
   'element-plus/es/components/segmented/style/css',
   '@element-plus/icons-vue',
   'element-plus/es/components/footer/style/css',
-  'element-plus/es/components/empty/style/css'
+  'element-plus/es/components/empty/style/css',
+  'element-plus/es/components/mention/style/css'
 ]
 
 const exclude = ['@iconify/json']

+ 4 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "yudao-ui-admin-vue3",
-  "version": "2.4.0-snapshot",
+  "version": "2.4.1-snapshot",
   "description": "基于vue3、vite4、element-plus、typesScript",
   "author": "xingyu",
   "private": false,
@@ -38,7 +38,7 @@
     "animate.css": "^4.1.1",
     "axios": "^1.6.8",
     "benz-amr-recorder": "^1.1.5",
-    "bpmn-js-token-simulation": "^0.10.0",
+    "bpmn-js-token-simulation": "^0.36.0",
     "camunda-bpmn-moddle": "^7.0.1",
     "cropperjs": "^1.6.1",
     "crypto-js": "^4.2.0",
@@ -74,6 +74,7 @@
     "vue-i18n": "9.10.2",
     "vue-router": "4.4.5",
     "vue-types": "^5.1.1",
+    "vue3-signature": "^0.2.4",
     "vuedraggable": "^4.1.0",
     "web-storage-cache": "^1.1.1",
     "xml-js": "^1.6.11"
@@ -92,6 +93,7 @@
     "@typescript-eslint/eslint-plugin": "^7.1.0",
     "@typescript-eslint/parser": "^7.1.0",
     "@unocss/eslint-config": "^0.57.4",
+    "@unocss/eslint-plugin": "66.1.0-beta.5",
     "@unocss/transformer-variant-group": "^0.58.5",
     "@vitejs/plugin-legacy": "^5.3.1",
     "@vitejs/plugin-vue": "^5.0.4",

Fișier diff suprimat deoarece este prea mare
+ 195 - 192
pnpm-lock.yaml


+ 8 - 1
src/api/ai/chat/message/index.ts

@@ -14,9 +14,16 @@ export interface ChatMessageVO {
   modelId: number // 模型编号
   content: string // 聊天内容
   tokens: number // 消耗 Token 数量
+  segmentIds?: number[] // 段落编号
+  segments?: {
+    id: number // 段落编号
+    content: string // 段落内容
+    documentId: number // 文档编号
+    documentName: string // 文档名称
+  }[]
   createTime: Date // 创建时间
   roleAvatar: string // 角色头像
-  userAvatar: string // 创建时间
+  userAvatar: string // 用户头像
 }
 
 // AI chat 聊天

+ 2 - 3
src/api/ai/image/index.ts

@@ -20,9 +20,8 @@ export interface ImageVO {
 }
 
 export interface ImageDrawReqVO {
-  platform: string // 平台
   prompt: string // 提示词
-  model: string // 模型
+  modelId: number // 模型
   style: string // 图像生成的风格
   width: string // 图片宽度
   height: string // 图片高度
@@ -31,7 +30,7 @@ export interface ImageDrawReqVO {
 
 export interface ImageMidjourneyImagineReqVO {
   prompt: string // 提示词
-  model: string // 模型 mj nijj
+  modelId: number // 模型
   base64Array: string[] // size不能为空
   width: string // 图片宽度
   height: string // 图片高度

+ 54 - 0
src/api/ai/knowledge/document/index.ts

@@ -0,0 +1,54 @@
+import request from '@/config/axios'
+
+// AI 知识库文档 VO
+export interface KnowledgeDocumentVO {
+  id: number // 编号
+  knowledgeId: number // 知识库编号
+  name: string // 文档名称
+  contentLength: number // 字符数
+  tokens: number // token 数
+  segmentMaxTokens: number // 分片最大 token 数
+  retrievalCount: number // 召回次数
+  status: number // 是否启用
+}
+
+// AI 知识库文档 API
+export const KnowledgeDocumentApi = {
+  // 查询知识库文档分页
+  getKnowledgeDocumentPage: async (params: any) => {
+    return await request.get({ url: `/ai/knowledge/document/page`, params })
+  },
+
+  // 查询知识库文档详情
+  getKnowledgeDocument: async (id: number) => {
+    return await request.get({ url: `/ai/knowledge/document/get?id=` + id })
+  },
+
+  // 新增知识库文档(单个)
+  createKnowledgeDocument: async (data: any) => {
+    return await request.post({ url: `/ai/knowledge/document/create`, data })
+  },
+
+  // 新增知识库文档(多个)
+  createKnowledgeDocumentList: async (data: any) => {
+    return await request.post({ url: `/ai/knowledge/document/create-list`, data })
+  },
+
+  // 修改知识库文档
+  updateKnowledgeDocument: async (data: any) => {
+    return await request.put({ url: `/ai/knowledge/document/update`, data })
+  },
+
+  // 修改知识库文档状态
+  updateKnowledgeDocumentStatus: async (data: any) => {
+    return await request.put({
+      url: `/ai/knowledge/document/update-status`,
+      data
+    })
+  },
+
+  // 删除知识库文档
+  deleteKnowledgeDocument: async (id: number) => {
+    return await request.delete({ url: `/ai/knowledge/document/delete?id=` + id })
+  }
+}

+ 44 - 0
src/api/ai/knowledge/knowledge/index.ts

@@ -0,0 +1,44 @@
+import request from '@/config/axios'
+
+// AI 知识库 VO
+export interface KnowledgeVO {
+  id: number // 编号
+  name: string // 知识库名称
+  description: string // 知识库描述
+  embeddingModelId: number // 嵌入模型编号,高质量模式时维护
+  topK: number // topK
+  similarityThreshold: number // 相似度阈值
+}
+
+// AI 知识库 API
+export const KnowledgeApi = {
+  // 查询知识库分页
+  getKnowledgePage: async (params: any) => {
+    return await request.get({ url: `/ai/knowledge/page`, params })
+  },
+
+  // 查询知识库详情
+  getKnowledge: async (id: number) => {
+    return await request.get({ url: `/ai/knowledge/get?id=` + id })
+  },
+
+  // 新增知识库
+  createKnowledge: async (data: KnowledgeVO) => {
+    return await request.post({ url: `/ai/knowledge/create`, data })
+  },
+
+  // 修改知识库
+  updateKnowledge: async (data: KnowledgeVO) => {
+    return await request.put({ url: `/ai/knowledge/update`, data })
+  },
+
+  // 删除知识库
+  deleteKnowledge: async (id: number) => {
+    return await request.delete({ url: `/ai/knowledge/delete?id=` + id })
+  },
+
+  // 获取知识库简单列表
+  getSimpleKnowledgeList: async () => {
+    return await request.get({ url: `/ai/knowledge/simple-list` })
+  }
+}

+ 75 - 0
src/api/ai/knowledge/segment/index.ts

@@ -0,0 +1,75 @@
+import request from '@/config/axios'
+
+// AI 知识库分段 VO
+export interface KnowledgeSegmentVO {
+  id: number // 编号
+  documentId: number // 文档编号
+  knowledgeId: number // 知识库编号
+  vectorId: string // 向量库编号
+  content: string // 切片内容
+  contentLength: number // 切片内容长度
+  tokens: number // token 数量
+  retrievalCount: number // 召回次数
+  status: number // 文档状态
+  createTime: number // 创建时间
+}
+
+// AI 知识库分段 API
+export const KnowledgeSegmentApi = {
+  // 查询知识库分段分页
+  getKnowledgeSegmentPage: async (params: any) => {
+    return await request.get({ url: `/ai/knowledge/segment/page`, params })
+  },
+
+  // 查询知识库分段详情
+  getKnowledgeSegment: async (id: number) => {
+    return await request.get({ url: `/ai/knowledge/segment/get?id=` + id })
+  },
+
+  // 删除知识库分段
+  deleteKnowledgeSegment: async (id: number) => {
+    return await request.delete({ url: `/ai/knowledge/segment/delete?id=` + id })
+  },
+
+  // 新增知识库分段
+  createKnowledgeSegment: async (data: KnowledgeSegmentVO) => {
+    return await request.post({ url: `/ai/knowledge/segment/create`, data })
+  },
+
+  // 修改知识库分段
+  updateKnowledgeSegment: async (data: KnowledgeSegmentVO) => {
+    return await request.put({ url: `/ai/knowledge/segment/update`, data })
+  },
+
+  // 修改知识库分段状态
+  updateKnowledgeSegmentStatus: async (data: any) => {
+    return await request.put({
+      url: `/ai/knowledge/segment/update-status`,
+      data
+    })
+  },
+
+  // 切片内容
+  splitContent: async (url: string, segmentMaxTokens: number) => {
+    return await request.get({
+      url: `/ai/knowledge/segment/split`,
+      params: { url, segmentMaxTokens }
+    })
+  },
+
+  // 获取文档处理列表
+  getKnowledgeSegmentProcessList: async (documentIds: number[]) => {
+    return await request.get({
+      url: `/ai/knowledge/segment/get-process-list`,
+      params: { documentIds: documentIds.join(',') }
+    })
+  },
+
+  // 搜索知识库分段
+  searchKnowledgeSegment: async (params: any) => {
+    return await request.get({
+      url: `/ai/knowledge/segment/search`,
+      params
+    })
+  }
+}

+ 0 - 53
src/api/ai/model/chatModel/index.ts

@@ -1,53 +0,0 @@
-import request from '@/config/axios'
-
-// AI 聊天模型 VO
-export interface ChatModelVO {
-  id: number // 编号
-  keyId: number // API 秘钥编号
-  name: string // 模型名字
-  model: string // 模型标识
-  platform: string // 模型平台
-  sort: number // 排序
-  status: number // 状态
-  temperature: number // 温度参数
-  maxTokens: number // 单条回复的最大 Token 数量
-  maxContexts: number // 上下文的最大 Message 数量
-}
-
-// AI 聊天模型 API
-export const ChatModelApi = {
-  // 查询聊天模型分页
-  getChatModelPage: async (params: any) => {
-    return await request.get({ url: `/ai/chat-model/page`, params })
-  },
-
-  // 获得聊天模型列表
-  getChatModelSimpleList: async (status?: number) => {
-    return await request.get({
-      url: `/ai/chat-model/simple-list`,
-      params: {
-        status
-      }
-    })
-  },
-
-  // 查询聊天模型详情
-  getChatModel: async (id: number) => {
-    return await request.get({ url: `/ai/chat-model/get?id=` + id })
-  },
-
-  // 新增聊天模型
-  createChatModel: async (data: ChatModelVO) => {
-    return await request.post({ url: `/ai/chat-model/create`, data })
-  },
-
-  // 修改聊天模型
-  updateChatModel: async (data: ChatModelVO) => {
-    return await request.put({ url: `/ai/chat-model/update`, data })
-  },
-
-  // 删除聊天模型
-  deleteChatModel: async (id: number) => {
-    return await request.delete({ url: `/ai/chat-model/delete?id=` + id })
-  }
-}

+ 2 - 0
src/api/ai/model/chatRole/index.ts

@@ -13,6 +13,8 @@ export interface ChatRoleVO {
   welcomeMessage: string // 角色设定
   publicStatus: boolean // 是否公开
   status: number // 状态
+  knowledgeIds?: number[] // 引用的知识库 ID 列表
+  toolIds?: number[] // 引用的工具 ID 列表
 }
 
 // AI 聊天角色 分页请求 vo

+ 54 - 0
src/api/ai/model/model/index.ts

@@ -0,0 +1,54 @@
+import request from '@/config/axios'
+
+// AI 模型 VO
+export interface ModelVO {
+  id: number // 编号
+  keyId: number // API 秘钥编号
+  name: string // 模型名字
+  model: string // 模型标识
+  platform: string // 模型平台
+  type: number // 模型类型
+  sort: number // 排序
+  status: number // 状态
+  temperature?: number // 温度参数
+  maxTokens?: number // 单条回复的最大 Token 数量
+  maxContexts?: number // 上下文的最大 Message 数量
+}
+
+// AI 模型 API
+export const ModelApi = {
+  // 查询模型分页
+  getModelPage: async (params: any) => {
+    return await request.get({ url: `/ai/model/page`, params })
+  },
+
+  // 获得模型列表
+  getModelSimpleList: async (type?: number) => {
+    return await request.get({
+      url: `/ai/model/simple-list`,
+      params: {
+        type
+      }
+    })
+  },
+
+  // 查询模型详情
+  getModel: async (id: number) => {
+    return await request.get({ url: `/ai/model/get?id=` + id })
+  },
+
+  // 新增模型
+  createModel: async (data: ModelVO) => {
+    return await request.post({ url: `/ai/model/create`, data })
+  },
+
+  // 修改模型
+  updateModel: async (data: ModelVO) => {
+    return await request.put({ url: `/ai/model/update`, data })
+  },
+
+  // 删除模型
+  deleteModel: async (id: number) => {
+    return await request.delete({ url: `/ai/model/delete?id=` + id })
+  }
+}

+ 42 - 0
src/api/ai/model/tool/index.ts

@@ -0,0 +1,42 @@
+import request from '@/config/axios'
+
+// AI 工具 VO
+export interface ToolVO {
+  id: number // 工具编号
+  name: string // 工具名称
+  description: string // 工具描述
+  status: number // 状态
+}
+
+// AI 工具 API
+export const ToolApi = {
+  // 查询工具分页
+  getToolPage: async (params: any) => {
+    return await request.get({ url: `/ai/tool/page`, params })
+  },
+
+  // 查询工具详情
+  getTool: async (id: number) => {
+    return await request.get({ url: `/ai/tool/get?id=` + id })
+  },
+
+  // 新增工具
+  createTool: async (data: ToolVO) => {
+    return await request.post({ url: `/ai/tool/create`, data })
+  },
+
+  // 修改工具
+  updateTool: async (data: ToolVO) => {
+    return await request.put({ url: `/ai/tool/update`, data })
+  },
+
+  // 删除工具
+  deleteTool: async (id: number) => {
+    return await request.delete({ url: `/ai/tool/delete?id=` + id })
+  },
+
+  // 获取工具简单列表
+  getToolSimpleList: async () => {
+    return await request.get({ url: `/ai/tool/simple-list` })
+  }
+}

+ 6 - 0
src/api/bpm/definition/index.ts

@@ -20,3 +20,9 @@ export const getProcessDefinitionList = async (params) => {
     params
   })
 }
+
+export const getSimpleProcessDefinitionList = async () => {
+  return await request.get({
+    url: '/bpm/process-definition/simple-list'
+  })
+}

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

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

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

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

+ 1 - 1
src/api/login/index.ts

@@ -83,5 +83,5 @@ export const reqCheck = (data: any) => {
 
 // 通过短信重置密码
 export const smsResetPassword = (data: any) => {
-  return request.post({ url: '/system/auth/sms-reset-password', data })
+  return request.post({ url: '/system/auth/reset-password', data })
 }

+ 1 - 1
src/api/mall/product/spu.ts

@@ -101,7 +101,7 @@ export const deleteSpu = (id: number) => {
 }
 
 // 导出商品 Spu Excel
-export const exportSpu = async (params) => {
+export const exportSpu = async (params: any) => {
   return await request.download({ url: '/product/spu/export', params })
 }
 

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

@@ -0,0 +1 @@
+<svg t="1740116949537" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1153" width="200" height="200"><path d="M440.32 296.96h283.30496v145.92h66.56V230.4H440.32V17.92H17.92v424.96H440.32V296.96zM373.76 376.32H84.48v-291.84H373.76v291.84zM586.24 588.8v143.36512H298.66496V586.24h-66.56v212.48512H586.24V1013.76H1008.64v-424.96h-422.4z m355.84 358.4h-289.28v-291.84H942.08v291.84z" p-id="1154" fill="#ffffff"></path></svg>

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

@@ -0,0 +1 @@
+<svg t="1739406626368" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1300" width="200" height="200"><path d="M803.221 925.573H224.356c-68.568 0-124.352-55.784-124.352-124.353V222.356c0-68.568 55.784-124.352 124.352-124.352h355.311v64H224.356c-33.278 0-60.352 27.074-60.352 60.352V801.22c0 33.278 27.074 60.353 60.352 60.353H803.22c33.278 0 60.353-27.074 60.353-60.353V448.208h64V801.22c0 68.569-55.784 124.353-124.352 124.353z" fill="#ffffff" p-id="1301"></path><path d="M300.357 756.916l35.024-195.867L770.117 84.404c10.05-11.02 25.015-18.052 41.058-19.293 16.017-1.247 31.987 3.379 43.841 12.667l83.662 65.549c21.643 16.956 24.254 45.964 5.942 66.038l-437.613 479.8-206.65 67.751z m104.994-170.751l-13.14 73.487 69.671-22.842 415.465-455.517-59.909-46.939-412.087 451.811z" fill="#ffffff" p-id="1302"></path><path d="M732.25 220.897l41.144-49.023 81.151 68.11-41.145 49.023z" fill="#ffffff" p-id="1303"></path></svg>

+ 3 - 2
src/components/DiyEditor/components/ComponentContainerProperty.vue

@@ -51,7 +51,8 @@
 </template>
 
 <script setup lang="ts">
-import { ComponentStyle, usePropertyForm } from '@/components/DiyEditor/util'
+import { ComponentStyle } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 
 /**
  * 组件容器属性:目前右边部分
@@ -61,7 +62,7 @@ defineOptions({ name: 'ComponentContainer' })
 
 const props = defineProps<{ modelValue: ComponentStyle }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 
 const treeData = [
   {

+ 2 - 2
src/components/DiyEditor/components/mobile/Carousel/property.vue

@@ -93,14 +93,14 @@
 
 <script setup lang="ts">
 import { CarouselProperty } from './config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 
 // 轮播图属性面板
 defineOptions({ name: 'CarouselProperty' })
 
 const props = defineProps<{ modelValue: CarouselProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 </script>
 
 <style scoped lang="scss"></style>

+ 22 - 7
src/components/DiyEditor/components/mobile/CouponCard/property.vue

@@ -68,15 +68,20 @@
     </el-form>
   </ComponentContainerProperty>
   <!-- 优惠券选择 -->
-  <CouponSelect ref="couponSelectDialog" v-model:multiple-selection="couponList" />
+  <CouponSelect
+    ref="couponSelectDialog"
+    v-model:multiple-selection="couponList"
+    :take-type="CouponTemplateTakeTypeEnum.USER.type"
+    @change="handleCouponSelect"
+  />
 </template>
 
 <script setup lang="ts">
 import { CouponCardProperty } from './config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate'
 import { floatToFixed2 } from '@/utils'
-import { PromotionDiscountTypeEnum } from '@/utils/constants'
+import { CouponTemplateTakeTypeEnum, PromotionDiscountTypeEnum } from '@/utils/constants'
 import CouponSelect from '@/views/mall/promotion/coupon/components/CouponSelect.vue'
 
 // 优惠券卡片属性面板
@@ -84,7 +89,7 @@ defineOptions({ name: 'CouponCardProperty' })
 
 const props = defineProps<{ modelValue: CouponCardProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 
 // 优惠券列表
 const couponList = ref<CouponTemplateApi.CouponTemplateVO[]>([])
@@ -93,10 +98,20 @@ const couponSelectDialog = ref()
 const handleAddCoupon = () => {
   couponSelectDialog.value.open()
 }
+const handleCouponSelect = () => {
+  formData.value.couponIds = couponList.value.map((coupon) => coupon.id)
+}
+
 watch(
-  () => couponList.value,
-  () => {
-    formData.value.couponIds = couponList.value.map((coupon) => coupon.id)
+  () => formData.value.couponIds,
+  async () => {
+    if (formData.value.couponIds?.length > 0) {
+      couponList.value = await CouponTemplateApi.getCouponTemplateList(formData.value.couponIds)
+    }
+  },
+  {
+    immediate: true,
+    deep: true
   }
 )
 </script>

+ 2 - 2
src/components/DiyEditor/components/mobile/Divider/property.vue

@@ -45,12 +45,12 @@
 
 <script setup lang="ts">
 import { DividerProperty } from './config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 // 导航栏属性面板
 defineOptions({ name: 'DividerProperty' })
 const props = defineProps<{ modelValue: DividerProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 
 //线类型
 const BORDER_TYPES = [

+ 2 - 2
src/components/DiyEditor/components/mobile/FloatingActionButton/property.vue

@@ -31,14 +31,14 @@
 
 <script setup lang="ts">
 import { FloatingActionButtonProperty } from './config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 
 // 悬浮按钮属性面板
 defineOptions({ name: 'FloatingActionButtonProperty' })
 
 const props = defineProps<{ modelValue: FloatingActionButtonProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 </script>
 
 <style scoped lang="scss"></style>

+ 2 - 2
src/components/DiyEditor/components/mobile/HotZone/property.vue

@@ -20,7 +20,7 @@
 </template>
 
 <script setup lang="ts">
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 import { HotZoneProperty } from '@/components/DiyEditor/components/mobile/HotZone/config'
 import HotZoneEditDialog from './components/HotZoneEditDialog/index.vue'
 
@@ -29,7 +29,7 @@ defineOptions({ name: 'HotZoneProperty' })
 
 const props = defineProps<{ modelValue: HotZoneProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 
 // 热区编辑对话框
 const editDialogRef = ref()

+ 2 - 2
src/components/DiyEditor/components/mobile/ImageBar/property.vue

@@ -21,14 +21,14 @@
 
 <script setup lang="ts">
 import { ImageBarProperty } from './config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 
 // 图片展示属性面板
 defineOptions({ name: 'ImageBarProperty' })
 
 const props = defineProps<{ modelValue: ImageBarProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 </script>
 
 <style scoped lang="scss"></style>

+ 2 - 2
src/components/DiyEditor/components/mobile/MagicCube/property.vue

@@ -56,7 +56,7 @@
 </template>
 
 <script setup lang="ts">
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 import { MagicCubeProperty } from '@/components/DiyEditor/components/mobile/MagicCube/config'
 
 /** 广告魔方属性面板 */
@@ -64,7 +64,7 @@ defineOptions({ name: 'MagicCubeProperty' })
 
 const props = defineProps<{ modelValue: MagicCubeProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 
 // 选中的热区
 const selectedHotAreaIndex = ref(-1)

+ 2 - 2
src/components/DiyEditor/components/mobile/MenuGrid/property.vue

@@ -48,7 +48,7 @@
 </template>
 
 <script setup lang="ts">
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 import {
   EMPTY_MENU_GRID_ITEM_PROPERTY,
   MenuGridProperty
@@ -59,7 +59,7 @@ defineOptions({ name: 'MenuGridProperty' })
 
 const props = defineProps<{ modelValue: MenuGridProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 </script>
 
 <style scoped lang="scss"></style>

+ 2 - 2
src/components/DiyEditor/components/mobile/MenuList/property.vue

@@ -28,7 +28,7 @@
 </template>
 
 <script setup lang="ts">
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 import {
   EMPTY_MENU_LIST_ITEM_PROPERTY,
   MenuListProperty
@@ -39,7 +39,7 @@ defineOptions({ name: 'MenuListProperty' })
 
 const props = defineProps<{ modelValue: MenuListProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 </script>
 
 <style scoped lang="scss"></style>

+ 2 - 2
src/components/DiyEditor/components/mobile/MenuSwiper/property.vue

@@ -58,7 +58,7 @@
 </template>
 
 <script setup lang="ts">
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 import {
   EMPTY_MENU_SWIPER_ITEM_PROPERTY,
   MenuSwiperProperty
@@ -70,7 +70,7 @@ defineOptions({ name: 'MenuSwiperProperty' })
 
 const props = defineProps<{ modelValue: MenuSwiperProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 </script>
 
 <style scoped lang="scss"></style>

+ 12 - 7
src/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue

@@ -64,17 +64,22 @@
 
 <script lang="ts" setup>
 import { NavigationBarCellProperty } from '../config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 // 导航栏属性面板
 defineOptions({ name: 'NavigationBarCellProperty' })
 
-const props = defineProps<{
-  modelValue: NavigationBarCellProperty[]
-  isMp: boolean
-}>()
+const props = withDefaults(
+  defineProps<{
+    modelValue: NavigationBarCellProperty[]
+    isMp: boolean
+  }>(),
+  {
+    modelValue: () => [],
+    isMp: true
+  }
+)
 const emit = defineEmits(['update:modelValue'])
-const { formData: cellList } = usePropertyForm(props.modelValue, emit)
-if (!cellList.value) cellList.value = []
+const cellList = useVModel(props, 'modelValue', emit)
 
 // 单元格数量:小程序6个(右侧胶囊按钮占了2个),其它平台8个
 const cellCount = computed(() => (props.isMp ? 6 : 8))

+ 3 - 3
src/components/DiyEditor/components/mobile/NavigationBar/index.vue

@@ -4,7 +4,7 @@
       <div v-for="(cell, cellIndex) in cellList" :key="cellIndex" :style="getCellStyle(cell)">
         <span v-if="cell.type === 'text'">{{ cell.text }}</span>
         <img v-else-if="cell.type === 'image'" :src="cell.imgUrl" alt="" class="h-full w-full" />
-        <SearchBar v-else :property="getSearchProp" />
+        <SearchBar v-else :property="getSearchProp(cell)" />
       </div>
     </div>
     <img
@@ -51,14 +51,14 @@ const getCellStyle = (cell: NavigationBarCellProperty) => {
   } as StyleValue
 }
 // 获得搜索框属性
-const getSearchProp = (cell: NavigationBarCellProperty) => {
+const getSearchProp = computed(() => (cell: NavigationBarCellProperty) => {
   return {
     height: 30,
     showScan: false,
     placeholder: cell.placeholder,
     borderRadius: cell.borderRadius
   } as SearchProperty
-}
+})
 </script>
 <style lang="scss" scoped>
 .navigation-bar {

+ 2 - 2
src/components/DiyEditor/components/mobile/NavigationBar/property.vue

@@ -66,7 +66,7 @@
 
 <script setup lang="ts">
 import { NavigationBarProperty } from './config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 import NavigationBarCellProperty from '@/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue'
 // 导航栏属性面板
 defineOptions({ name: 'NavigationBarProperty' })
@@ -77,7 +77,7 @@ const rules = {
 
 const props = defineProps<{ modelValue: NavigationBarProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 if (!formData.value._local) {
   formData.value._local = { previewMp: true, previewOther: false }
 }

+ 2 - 2
src/components/DiyEditor/components/mobile/NoticeBar/property.vue

@@ -30,7 +30,7 @@
 
 <script setup lang="ts">
 import { NoticeBarProperty } from './config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 // 通知栏属性面板
 defineOptions({ name: 'NoticeBarProperty' })
 // 表单校验
@@ -40,7 +40,7 @@ const rules = {
 
 const props = defineProps<{ modelValue: NoticeBarProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 </script>
 
 <style scoped lang="scss"></style>

+ 2 - 2
src/components/DiyEditor/components/mobile/PageConfig/property.vue

@@ -20,7 +20,7 @@
 
 <script setup lang="ts">
 import { PageConfigProperty } from './config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 // 导航栏属性面板
 defineOptions({ name: 'PageConfigProperty' })
 // 表单校验
@@ -28,7 +28,7 @@ const rules = {}
 
 const props = defineProps<{ modelValue: PageConfigProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 </script>
 
 <style scoped lang="scss"></style>

+ 2 - 2
src/components/DiyEditor/components/mobile/Popover/property.vue

@@ -25,14 +25,14 @@
 
 <script setup lang="ts">
 import { PopoverProperty } from './config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 
 // 弹窗广告属性面板
 defineOptions({ name: 'PopoverProperty' })
 
 const props = defineProps<{ modelValue: PopoverProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 </script>
 
 <style scoped lang="scss"></style>

+ 2 - 2
src/components/DiyEditor/components/mobile/ProductCard/property.vue

@@ -135,7 +135,7 @@
 
 <script setup lang="ts">
 import { ProductCardProperty } from './config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 import SpuShowcase from '@/views/mall/product/spu/components/SpuShowcase.vue'
 
 // 商品卡片属性面板
@@ -143,7 +143,7 @@ defineOptions({ name: 'ProductCardProperty' })
 
 const props = defineProps<{ modelValue: ProductCardProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 </script>
 
 <style scoped lang="scss"></style>

+ 2 - 2
src/components/DiyEditor/components/mobile/ProductList/property.vue

@@ -85,7 +85,7 @@
 
 <script setup lang="ts">
 import { ProductListProperty } from './config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 import SpuShowcase from '@/views/mall/product/spu/components/SpuShowcase.vue'
 
 // 商品栏属性面板
@@ -93,7 +93,7 @@ defineOptions({ name: 'ProductListProperty' })
 
 const props = defineProps<{ modelValue: ProductListProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 </script>
 
 <style scoped lang="scss"></style>

+ 2 - 2
src/components/DiyEditor/components/mobile/PromotionArticle/property.vue

@@ -25,7 +25,7 @@
 
 <script setup lang="ts">
 import { PromotionArticleProperty } from './config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 import * as ArticleApi from '@/api/mall/promotion/article/index'
 
 // 营销文章属性面板
@@ -33,7 +33,7 @@ defineOptions({ name: 'PromotionArticleProperty' })
 
 const props = defineProps<{ modelValue: PromotionArticleProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 // 文章列表
 const articles = ref<ArticleApi.ArticleVO>([])
 

+ 2 - 2
src/components/DiyEditor/components/mobile/PromotionCombination/property.vue

@@ -140,7 +140,7 @@
 
 <script setup lang="ts">
 import { PromotionCombinationProperty } from './config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
 import { CommonStatusEnum } from '@/utils/constants'
 import CombinationShowcase from '@/views/mall/promotion/combination/components/CombinationShowcase.vue'
@@ -150,7 +150,7 @@ defineOptions({ name: 'PromotionCombinationProperty' })
 
 const props = defineProps<{ modelValue: PromotionCombinationProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 // 活动列表
 const activityList = ref<CombinationActivityApi.CombinationActivityVO[]>([])
 onMounted(async () => {

+ 2 - 2
src/components/DiyEditor/components/mobile/PromotionPoint/property.vue

@@ -140,7 +140,7 @@
 
 <script lang="ts" setup>
 import { PromotionPointProperty } from './config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 import PointShowcase from '@/views/mall/promotion/point/components/PointShowcase.vue'
 
 // 秒杀属性面板
@@ -148,7 +148,7 @@ defineOptions({ name: 'PromotionPointProperty' })
 
 const props = defineProps<{ modelValue: PromotionPointProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 </script>
 
 <style lang="scss" scoped></style>

+ 2 - 2
src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue

@@ -140,7 +140,7 @@
 
 <script setup lang="ts">
 import { PromotionSeckillProperty } from './config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
 import { CommonStatusEnum } from '@/utils/constants'
 import SeckillShowcase from '@/views/mall/promotion/seckill/components/SeckillShowcase.vue'
@@ -150,7 +150,7 @@ defineOptions({ name: 'PromotionSeckillProperty' })
 
 const props = defineProps<{ modelValue: PromotionSeckillProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 // 活动列表
 const activityList = ref<SeckillActivityApi.SeckillActivityVO[]>([])
 onMounted(async () => {

+ 2 - 2
src/components/DiyEditor/components/mobile/SearchBar/property.vue

@@ -59,7 +59,7 @@
 </template>
 
 <script setup lang="ts">
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 import { SearchProperty } from '@/components/DiyEditor/components/mobile/SearchBar/config'
 
 /** 搜索框属性面板 */
@@ -67,7 +67,7 @@ defineOptions({ name: 'SearchProperty' })
 
 const props = defineProps<{ modelValue: SearchProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 </script>
 
 <style scoped lang="scss"></style>

+ 2 - 2
src/components/DiyEditor/components/mobile/TabBar/property.vue

@@ -80,13 +80,13 @@
 
 <script setup lang="ts">
 import { TabBarProperty, component, THEME_LIST } from './config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 // 底部导航栏
 defineOptions({ name: 'TabBarProperty' })
 
 const props = defineProps<{ modelValue: TabBarProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 
 // 将数据库的值更新到右侧属性栏
 component.property.items = formData.value.items

+ 2 - 2
src/components/DiyEditor/components/mobile/TitleBar/property.vue

@@ -101,13 +101,13 @@
 </template>
 <script lang="ts" setup>
 import { TitleBarProperty } from './config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 // 导航栏属性面板
 defineOptions({ name: 'TitleBarProperty' })
 
 const props = defineProps<{ modelValue: TitleBarProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 
 // 表单校验
 const rules = {}

+ 2 - 2
src/components/DiyEditor/components/mobile/UserCard/property.vue

@@ -4,14 +4,14 @@
 
 <script setup lang="ts">
 import { UserCardProperty } from './config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 
 // 用户卡片属性面板
 defineOptions({ name: 'UserCardProperty' })
 
 const props = defineProps<{ modelValue: UserCardProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 </script>
 
 <style scoped lang="scss"></style>

+ 2 - 2
src/components/DiyEditor/components/mobile/UserCoupon/property.vue

@@ -4,14 +4,14 @@
 
 <script setup lang="ts">
 import { UserCouponProperty } from './config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 
 // 用户卡券属性面板
 defineOptions({ name: 'UserCouponProperty' })
 
 const props = defineProps<{ modelValue: UserCouponProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 </script>
 
 <style scoped lang="scss"></style>

+ 2 - 2
src/components/DiyEditor/components/mobile/UserOrder/property.vue

@@ -4,14 +4,14 @@
 
 <script setup lang="ts">
 import { UserOrderProperty } from './config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 
 // 用户订单属性面板
 defineOptions({ name: 'UserOrderProperty' })
 
 const props = defineProps<{ modelValue: UserOrderProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 </script>
 
 <style scoped lang="scss"></style>

+ 2 - 2
src/components/DiyEditor/components/mobile/UserWallet/property.vue

@@ -4,14 +4,14 @@
 
 <script setup lang="ts">
 import { UserWalletProperty } from './config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 
 // 用户资产属性面板
 defineOptions({ name: 'UserWalletProperty' })
 
 const props = defineProps<{ modelValue: UserWalletProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 </script>
 
 <style scoped lang="scss"></style>

+ 2 - 2
src/components/DiyEditor/components/mobile/VideoPlayer/property.vue

@@ -42,14 +42,14 @@
 
 <script setup lang="ts">
 import { VideoPlayerProperty } from './config'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 
 // 视频播放属性面板
 defineOptions({ name: 'VideoPlayerProperty' })
 
 const props = defineProps<{ modelValue: VideoPlayerProperty }>()
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 </script>
 
 <style scoped lang="scss"></style>

+ 31 - 13
src/components/DiyEditor/index.vue

@@ -110,7 +110,7 @@
           <el-tag
             v-if="showPageConfig"
             :effect="selectedComponent?.uid === pageConfigComponent.uid ? 'dark' : 'plain'"
-            :type="selectedComponent?.uid === pageConfigComponent.uid ? '' : 'info'"
+            :type="selectedComponent?.uid === pageConfigComponent.uid ? 'primary' : 'info'"
             size="large"
             @click="handleComponentSelected(pageConfigComponent)"
           >
@@ -121,7 +121,7 @@
             <el-tag
               v-if="component.position === 'fixed'"
               :effect="selectedComponent?.uid === component.uid ? 'dark' : 'plain'"
-              :type="selectedComponent?.uid === component.uid ? '' : 'info'"
+              :type="selectedComponent?.uid === component.uid ? 'primary' : 'info'"
               closable
               size="large"
               @click="handleComponentSelected(component)"
@@ -191,7 +191,7 @@ import { cloneDeep, includes } from 'lodash-es'
 import { component as PAGE_CONFIG_COMPONENT } from '@/components/DiyEditor/components/mobile/PageConfig/config'
 import { component as NAVIGATION_BAR_COMPONENT } from './components/mobile/NavigationBar/config'
 import { component as TAB_BAR_COMPONENT } from './components/mobile/TabBar/config'
-import { isString } from '@/utils/is'
+import { isEmpty, isString } from '@/utils/is'
 import { DiyComponent, DiyComponentLibrary, PageConfig } from '@/components/DiyEditor/util'
 import { componentConfigs } from '@/components/DiyEditor/components/mobile'
 import { array, oneOfType } from 'vue-types'
@@ -238,24 +238,42 @@ const props = defineProps({
 watch(
   () => props.modelValue,
   () => {
-    const modelValue = isString(props.modelValue)
-      ? (JSON.parse(props.modelValue) as PageConfig)
-      : props.modelValue
-    pageConfigComponent.value.property = modelValue?.page || PAGE_CONFIG_COMPONENT.property
+    const modelValue =
+      isString(props.modelValue) && !isEmpty(props.modelValue)
+        ? (JSON.parse(props.modelValue) as PageConfig)
+        : props.modelValue
+    pageConfigComponent.value.property =
+      (typeof modelValue !== 'string' && modelValue?.page) || PAGE_CONFIG_COMPONENT.property
     navigationBarComponent.value.property =
-      modelValue?.navigationBar || NAVIGATION_BAR_COMPONENT.property
-    tabBarComponent.value.property = modelValue?.tabBar || TAB_BAR_COMPONENT.property
+      (typeof modelValue !== 'string' && modelValue?.navigationBar) ||
+      NAVIGATION_BAR_COMPONENT.property
+    tabBarComponent.value.property =
+      (typeof modelValue !== 'string' && modelValue?.tabBar) || TAB_BAR_COMPONENT.property
     // 查找对应的页面组件
-    pageComponents.value = (modelValue?.components || []).map((item) => {
-      const component = componentConfigs[item.id]
-      return { ...component, property: item.property }
-    })
+    pageComponents.value = ((typeof modelValue !== 'string' && modelValue?.components) || []).map(
+      (item) => {
+        const component = componentConfigs[item.id]
+        return { ...component, property: item.property }
+      }
+    )
   },
   {
     immediate: true
   }
 )
 
+/** 选择组件修改其属性后更新它的配置 */
+watch(
+  selectedComponent,
+  (val: any) => {
+    if (!val || selectedComponentIndex.value === -1) {
+      return
+    }
+    pageComponents.value[selectedComponentIndex.value] = selectedComponent.value!
+  },
+  { deep: true }
+)
+
 // 保存
 const handleSave = () => {
   // 发送保存通知

+ 0 - 29
src/components/DiyEditor/util.ts

@@ -1,4 +1,3 @@
-import { ref, Ref } from 'vue'
 import { PageConfigProperty } from '@/components/DiyEditor/components/mobile/PageConfig/config'
 import { NavigationBarProperty } from '@/components/DiyEditor/components/mobile/NavigationBar/config'
 import { TabBarProperty } from '@/components/DiyEditor/components/mobile/TabBar/config'
@@ -78,34 +77,6 @@ export interface PageConfig {
 // 页面组件,只保留组件ID,组件属性
 export interface PageComponent extends Pick<DiyComponent<any>, 'id' | 'property'> {}
 
-// 属性表单监听
-export function usePropertyForm<T>(modelValue: T, emit: Function): { formData: Ref<T> } {
-  const formData = ref<T>()
-  // 监听属性数据变动
-  watch(
-    () => modelValue,
-    () => {
-      formData.value = modelValue
-    },
-    {
-      deep: true,
-      immediate: true
-    }
-  )
-  // 监听表单数据变动
-  watch(
-    () => formData.value,
-    () => {
-      emit('update:modelValue', formData.value)
-    },
-    {
-      deep: true
-    }
-  )
-
-  return { formData } as { formData: Ref<T> }
-}
-
 // 页面组件库
 export const PAGE_LIBS = [
   {

+ 11 - 4
src/components/Draggable/index.vue

@@ -13,9 +13,16 @@
         class="mb-4px flex flex-col gap-4px border border-gray-2 border-rounded rounded border-solid p-8px"
       >
         <!-- 操作按钮区 -->
-        <div class="m--8px m-b-4px flex flex-row items-center justify-between p-8px" style="background-color: var(--app-content-bg-color);">
+        <div
+          class="m--8px m-b-4px flex flex-row items-center justify-between p-8px"
+          style="background-color: var(--app-content-bg-color)"
+        >
           <el-tooltip content="拖动排序">
-            <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" style="color: #8a909c;" />
+            <Icon
+              icon="ic:round-drag-indicator"
+              class="drag-icon cursor-move"
+              style="color: #8a909c"
+            />
           </el-tooltip>
           <el-tooltip content="删除">
             <Icon
@@ -47,7 +54,7 @@
 <script setup lang="ts">
 // 拖拽组件
 import VueDraggable from 'vuedraggable'
-import { usePropertyForm } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
 import { any, array } from 'vue-types'
 import { propTypes } from '@/utils/propTypes'
 import { cloneDeep } from 'lodash-es'
@@ -66,7 +73,7 @@ const props = defineProps({
 })
 // 定义事件
 const emit = defineEmits(['update:modelValue'])
-const { formData } = usePropertyForm(props.modelValue, emit)
+const formData = useVModel(props, 'modelValue', emit)
 
 // 处理添加
 const handleAdd = () => formData.value.push(cloneDeep(props.emptyItem || {}))

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

@@ -105,7 +105,7 @@ const editorConfig = computed((): IEditorConfig => {
           },
 
           // 超时时间,默认为 10 秒
-          timeout: 5 * 1000, // 5 秒
+          timeout: 15 * 1000, // 15 秒
 
           // form-data fieldName,后端接口参数名称,默认值wangeditor-uploaded-image
           fieldName: 'file',

+ 1 - 1
src/components/Icon/src/Icon.vue

@@ -75,7 +75,7 @@ watch(
 
 <template>
   <ElIcon :class="prefixCls" :color="color" :size="size">
-    <svg v-if="isLocal" :class="getSvgClass" aria-hidden="true">
+    <svg v-if="isLocal" :class="getSvgClass">
       <use :xlink:href="symbolId" />
     </svg>
 

+ 4 - 28
src/components/InputWithColor/index.vue

@@ -1,7 +1,7 @@
 <template>
-  <el-input v-model="valueRef" v-bind="$attrs">
+  <el-input v-model="modelValue" v-bind="$attrs">
     <template #append>
-      <el-color-picker v-model="colorRef" :predefine="PREDEFINE_COLORS" />
+      <el-color-picker v-model="color" :predefine="PREDEFINE_COLORS" />
     </template>
   </el-input>
 </template>
@@ -9,6 +9,7 @@
 <script lang="ts" setup>
 import { propTypes } from '@/utils/propTypes'
 import { PREDEFINE_COLORS } from '@/utils/color'
+import { useVModels } from '@vueuse/core'
 
 /**
  * 带颜色选择器输入框
@@ -19,33 +20,8 @@ const props = defineProps({
   modelValue: propTypes.string.def('').isRequired,
   color: propTypes.string.def('').isRequired
 })
-
-watch(
-  () => props.modelValue,
-  (val: string) => {
-    if (val === unref(valueRef)) return
-    valueRef.value = val
-  }
-)
-
 const emit = defineEmits(['update:modelValue', 'update:color'])
-
-// 输入框的值
-const valueRef = ref(props.modelValue)
-watch(
-  () => valueRef.value,
-  (val: string) => {
-    emit('update:modelValue', val)
-  }
-)
-// 颜色
-const colorRef = ref(props.color)
-watch(
-  () => colorRef.value,
-  (val: string) => {
-    emit('update:color', val)
-  }
-)
+const { modelValue, color } = useVModels(props, emit)
 </script>
 <style scoped lang="scss">
 :deep(.el-input-group__append) {

+ 103 - 14
src/components/SimpleProcessDesignerV2/src/NodeHandler.vue

@@ -15,6 +15,12 @@
             </div>
             <div class="handler-item-text">审批人</div>
           </div>
+          <div class="handler-item" @click="addNode(NodeType.TRANSACTOR_NODE)">
+            <div class="transactor handler-item-icon">
+              <span class="iconfont icon-transactor icon-size"></span>
+            </div>
+            <div class="handler-item-text">办理人</div>
+          </div>
           <div class="handler-item" @click="addNode(NodeType.COPY_TASK_NODE)">
             <div class="handler-item-icon copy">
               <span class="iconfont icon-size icon-copy"></span>
@@ -40,12 +46,29 @@
             <div class="handler-item-text">包容分支</div>
           </div>
           <div class="handler-item" @click="addNode(NodeType.DELAY_TIMER_NODE)">
-            <!-- TODO @芋艿 需要更换一下iconfont的图标 -->
-            <div class="handler-item-icon copy">
-              <span class="iconfont icon-size icon-copy"></span>
+            <div class="handler-item-icon delay">
+              <span class="iconfont icon-size icon-delay"></span>
             </div>
             <div class="handler-item-text">延迟器</div>
           </div>
+          <div class="handler-item" @click="addNode(NodeType.ROUTER_BRANCH_NODE)">
+            <div class="handler-item-icon router">
+              <span class="iconfont icon-size icon-router"></span>
+            </div>
+            <div class="handler-item-text">路由分支</div>
+          </div>
+          <div class="handler-item" @click="addNode(NodeType.TRIGGER_NODE)">
+            <div class="handler-item-icon trigger">
+              <span class="iconfont icon-size icon-trigger"></span>
+            </div>
+            <div class="handler-item-text">触发器</div>
+          </div>
+          <div class="handler-item" @click="addNode(NodeType.CHILD_PROCESS_NODE)">
+            <div class="handler-item-icon child-process">
+              <span class="iconfont icon-size icon-child-process"></span>
+            </div>
+            <div class="handler-item-text">子流程</div>
+          </div>
         </div>
         <template #reference>
           <div class="add-icon"><Icon icon="ep:plus" /></div>
@@ -60,10 +83,12 @@ import {
   ApproveMethodType,
   AssignEmptyHandlerType,
   AssignStartUserHandlerType,
+  ConditionType,
   NODE_DEFAULT_NAME,
   NodeType,
   RejectHandlerType,
-  SimpleFlowNode
+  SimpleFlowNode,
+  DEFAULT_CONDITION_GROUP_VALUE
 } from './consts'
 import { generateUUID } from '@/utils'
 
@@ -101,13 +126,13 @@ const addNode = (type: number) => {
   }
 
   popoverShow.value = false
-  if (type === NodeType.USER_TASK_NODE) {
+  if (type === NodeType.USER_TASK_NODE || type === NodeType.TRANSACTOR_NODE) {
     const id = 'Activity_' + generateUUID()
     const data: SimpleFlowNode = {
       id: id,
-      name: NODE_DEFAULT_NAME.get(NodeType.USER_TASK_NODE) as string,
+      name: NODE_DEFAULT_NAME.get(type) as string,
       showText: '',
-      type: NodeType.USER_TASK_NODE,
+      type: type,
       approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE,
       // 超时处理
       rejectHandler: {
@@ -120,7 +145,16 @@ const addNode = (type: number) => {
         type: AssignEmptyHandlerType.APPROVE
       },
       assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT,
-      childNode: props.childNode
+      childNode: props.childNode,
+      taskCreateListener: {
+        enable: false
+      },
+      taskAssignListener: {
+        enable: false
+      },
+      taskCompleteListener: {
+        enable: false
+      }
     }
     emits('update:childNode', data)
   }
@@ -147,8 +181,11 @@ const addNode = (type: number) => {
           showText: '',
           type: NodeType.CONDITION_NODE,
           childNode: undefined,
-          conditionType: 1,
-          defaultFlow: false
+          conditionSetting: {
+            defaultFlow: false,
+            conditionType: ConditionType.RULE,
+            conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
+          }
         },
         {
           id: 'Flow_' + generateUUID(),
@@ -156,8 +193,9 @@ const addNode = (type: number) => {
           showText: '未满足其它条件时,将进入此分支',
           type: NodeType.CONDITION_NODE,
           childNode: undefined,
-          conditionType: undefined,
-          defaultFlow: true
+          conditionSetting: {
+            defaultFlow: true
+          }
         }
       ]
     }
@@ -201,7 +239,11 @@ const addNode = (type: number) => {
           showText: '',
           type: NodeType.CONDITION_NODE,
           childNode: undefined,
-          defaultFlow: false
+          conditionSetting: {
+            defaultFlow: false,
+            conditionType: ConditionType.RULE,
+            conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
+          }
         },
         {
           id: 'Flow_' + generateUUID(),
@@ -209,7 +251,9 @@ const addNode = (type: number) => {
           showText: '未满足其它条件时,将进入此分支',
           type: NodeType.CONDITION_NODE,
           childNode: undefined,
-          defaultFlow: true
+          conditionSetting: {
+            defaultFlow: true
+          }
         }
       ]
     }
@@ -225,6 +269,51 @@ const addNode = (type: number) => {
     }
     emits('update:childNode', data)
   }
+  if (type === NodeType.ROUTER_BRANCH_NODE) {
+    const data: SimpleFlowNode = {
+      id: 'GateWay_' + generateUUID(),
+      name: NODE_DEFAULT_NAME.get(NodeType.ROUTER_BRANCH_NODE) as string,
+      showText: '',
+      type: NodeType.ROUTER_BRANCH_NODE,
+      childNode: props.childNode
+    }
+    emits('update:childNode', data)
+  }
+  if (type === NodeType.TRIGGER_NODE) {
+    const data: SimpleFlowNode = {
+      id: 'Activity_' + generateUUID(),
+      name: NODE_DEFAULT_NAME.get(NodeType.TRIGGER_NODE) as string,
+      showText: '',
+      type: NodeType.TRIGGER_NODE,
+      childNode: props.childNode
+    }
+    emits('update:childNode', data)
+  }
+  if (type === NodeType.CHILD_PROCESS_NODE) {
+    const data: SimpleFlowNode = {
+      id: 'Activity_' + generateUUID(),
+      name: NODE_DEFAULT_NAME.get(NodeType.CHILD_PROCESS_NODE) as string,
+      showText: '',
+      type: NodeType.CHILD_PROCESS_NODE,
+      childNode: props.childNode,
+      childProcessSetting: {
+        calledProcessDefinitionKey: '',
+        calledProcessDefinitionName: '',
+        async: false,
+        skipStartUserNode: false,
+        startUserSetting: {
+          type: 1
+        },
+        timeoutSetting: {
+          enable: false
+        },
+        multiInstanceSetting: {
+          enable: false
+        }
+      }
+    }
+    emits('update:childNode', data)
+  }
 }
 </script>
 

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

@@ -6,7 +6,11 @@
   />
   <!-- 审批节点 -->
   <UserTaskNode
-    v-if="currentNode && currentNode.type === NodeType.USER_TASK_NODE"
+    v-if="
+      currentNode &&
+      (currentNode.type === NodeType.USER_TASK_NODE ||
+        currentNode.type === NodeType.TRANSACTOR_NODE)
+    "
     :flow-node="currentNode"
     @update:flow-node="handleModelValueUpdate"
     @find:parent-node="findFromParentNode"
@@ -44,6 +48,24 @@
     :flow-node="currentNode"
     @update:flow-node="handleModelValueUpdate"
   />
+  <!-- 路由分支节点 -->
+  <RouterNode
+    v-if="currentNode && currentNode.type === NodeType.ROUTER_BRANCH_NODE"
+    :flow-node="currentNode"
+    @update:flow-node="handleModelValueUpdate"
+  />
+  <!-- 触发器节点 -->
+  <TriggerNode
+    v-if="currentNode && currentNode.type === NodeType.TRIGGER_NODE"
+    :flow-node="currentNode"
+    @update:flow-node="handleModelValueUpdate"
+  />
+  <!-- 子流程节点 -->
+  <ChildProcessNode
+    v-if="currentNode && currentNode.type === NodeType.CHILD_PROCESS_NODE"
+    :flow-node="currentNode"
+    @update:flow-node="handleModelValueUpdate"
+  />
   <!-- 递归显示孩子节点  -->
   <ProcessNodeTree
     v-if="currentNode && currentNode.childNode"
@@ -67,6 +89,9 @@ import ExclusiveNode from './nodes/ExclusiveNode.vue'
 import ParallelNode from './nodes/ParallelNode.vue'
 import InclusiveNode from './nodes/InclusiveNode.vue'
 import DelayTimerNode from './nodes/DelayTimerNode.vue'
+import RouterNode from './nodes/RouterNode.vue'
+import TriggerNode from './nodes/TriggerNode.vue'
+import ChildProcessNode from './nodes/ChildProcessNode.vue'
 import { SimpleFlowNode, NodeType } from './consts'
 import { useWatchNode } from './node'
 defineOptions({

+ 12 - 116
src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue

@@ -25,7 +25,6 @@
 
 <script setup lang="ts">
 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'
 import { getForm, FormVO } from '@/api/bpm/form'
@@ -35,12 +34,13 @@ import * as DeptApi from '@/api/system/dept'
 import * as PostApi from '@/api/system/post'
 import * as UserApi from '@/api/system/user'
 import * as UserGroupApi from '@/api/bpm/userGroup'
+import { BpmModelFormType } from '@/utils/constants'
 
 defineOptions({
   name: 'SimpleProcessDesigner'
 })
 
-const emits = defineEmits(['success', 'init-finished']) // 保存成功事件
+const emits = defineEmits(['success']) // 保存成功事件
 
 const props = defineProps({
   modelId: {
@@ -56,16 +56,13 @@ const props = defineProps({
     required: false
   },
   // 可发起流程的人员编号
-  startUserIds : {
+  startUserIds: {
     type: Array,
     required: false
-  },
-  value: {
-    type: [String, Object],
-    required: false
   }
 })
 
+const processData = inject('processData') as Ref
 const loading = ref(false)
 const formFields = ref<string[]>([])
 const formType = ref(20)
@@ -76,9 +73,6 @@ const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表
 const deptTreeOptions = ref()
 const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
 
-// 添加当前值的引用
-const currentValue = ref<SimpleFlowNode | undefined>()
-
 provide('formFields', formFields)
 provide('formType', formType)
 provide('roleList', roleOptions)
@@ -88,9 +82,11 @@ provide('deptList', deptOptions)
 provide('userGroupList', userGroupOptions)
 provide('deptTree', deptTreeOptions)
 provide('startUserIds', props.startUserIds)
-
+provide('tasks', [])
+provide('processInstance', {})
 const message = useMessage() // 国际化
 const processNodeTree = ref<SimpleFlowNode | undefined>()
+provide('processNodeTree', processNodeTree)
 const errorDialogVisible = ref(false)
 let errorNodes: SimpleFlowNode[] = []
 
@@ -112,70 +108,13 @@ const updateModel = () => {
   }
 }
 
-// 加载流程数据
-const loadProcessData = async (data: any) => {
-  try {
-    if (data) {
-      const parsedData = typeof data === 'string' ? JSON.parse(data) : data
-      processNodeTree.value = parsedData
-      currentValue.value = parsedData
-      // 确保数据加载后刷新视图
-      await nextTick()
-      if (simpleProcessModelRef.value?.refresh) {
-        await simpleProcessModelRef.value.refresh()
-      }
-    }
-  } catch (error) {
-    console.error('加载流程数据失败:', error)
-  }
-}
-
-// 监听属性变化
-watch(
-  () => props.value,
-  async (newValue, oldValue) => {
-    if (newValue && newValue !== oldValue) {
-      await loadProcessData(newValue)
-    }
-  },
-  { immediate: true, deep: true }
-)
-
-// 监听流程节点树变化,自动保存
-watch(
-  () => processNodeTree.value,
-  async (newValue, oldValue) => {
-    if (newValue && oldValue && JSON.stringify(newValue) !== JSON.stringify(oldValue)) {
-      await saveSimpleFlowModel(newValue)
-    }
-  },
-  { deep: true }
-)
-
 const saveSimpleFlowModel = async (simpleModelNode: SimpleFlowNode) => {
   if (!simpleModelNode) {
     return
   }
 
-  // 校验节点
-  errorNodes = []
-  validateNode(simpleModelNode, errorNodes)
-  if (errorNodes.length > 0) {
-    errorDialogVisible.value = true
-    return
-  }
-
   try {
-    if (props.modelId) {
-      // 编辑模式
-      const data = {
-        id: props.modelId,
-        simpleModel: simpleModelNode
-      }
-      await updateBpmSimpleModel(data)
-    }
-    // 无论是编辑还是新建模式,都更新当前值并触发事件
-    currentValue.value = simpleModelNode
+    processData.value = simpleModelNode
     emits('success', simpleModelNode)
   } catch (error) {
     console.error('保存失败:', error)
@@ -229,7 +168,7 @@ onMounted(async () => {
       const bpmnModel = await getModel(props.modelId)
       if (bpmnModel) {
         formType.value = bpmnModel.formType
-        if (formType.value === 10) {
+        if (formType.value === BpmModelFormType.NORMAL && bpmnModel.formId) {
           const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO
           formFields.value = bpmnForm?.fields
         }
@@ -246,61 +185,18 @@ onMounted(async () => {
     deptTreeOptions.value = handleTree(deptOptions.value as DeptApi.DeptVO[], 'id')
     // 获取用户组列表
     userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
-
     // 加载流程数据
-    if (props.modelId) {
-      // 获取 SIMPLE 设计器模型
-      const result = await getBpmSimpleModel(props.modelId)
-      if (result) {
-        await loadProcessData(result)
-      } else {
-        updateModel()
-      }
-    } else if (props.value) {
-      await loadProcessData(props.value)
+    if (processData.value) {
+      processNodeTree.value = processData?.value
     } else {
       updateModel()
     }
   } finally {
     loading.value = false
-    emits('init-finished')
   }
 })
 
 const simpleProcessModelRef = ref()
 
-/** 获取当前流程数据 */
-const getCurrentFlowData = async () => {
-  try {
-    if (simpleProcessModelRef.value) {
-      const data = await simpleProcessModelRef.value.getCurrentFlowData()
-      if (data) {
-        currentValue.value = data
-        return data
-      }
-    }
-    return currentValue.value
-  } catch (error) {
-    console.error('获取流程数据失败:', error)
-    return currentValue.value
-  }
-}
-
-// 刷新方法
-const refresh = async () => {
-  try {
-    if (currentValue.value) {
-      await loadProcessData(currentValue.value)
-    }
-  } catch (error) {
-    console.error('刷新失败:', error)
-  }
-}
-
-defineExpose({
-  getCurrentFlowData,
-  updateModel,
-  loadProcessData,
-  refresh
-})
+defineExpose({})
 </script>

+ 124 - 7
src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue

@@ -1,16 +1,41 @@
 <template>
   <div class="simple-process-model-container position-relative">
-    <div class="position-absolute top-0px right-0px bg-#fff">
+    <div class="position-absolute top-0px right-0px bg-#fff z-index-button-group">
       <el-row type="flex" justify="end">
         <el-button-group key="scale-control" size="default">
+          <el-button v-if="!readonly" size="default" @click="exportJson">
+            <Icon icon="ep:download" /> 导出
+          </el-button>
+          <el-button v-if="!readonly" size="default" @click="importJson">
+            <Icon icon="ep:upload" />导入
+          </el-button>
+          <!-- 用于打开本地文件-->
+          <input
+            v-if="!readonly"
+            type="file"
+            id="files"
+            ref="refFile"
+            style="display: none"
+            accept=".json"
+            @change="importLocalFile"
+          />
           <el-button size="default" :icon="ScaleToOriginal" @click="processReZoom()" />
           <el-button size="default" :plain="true" :icon="ZoomOut" @click="zoomOut()" />
           <el-button size="default" class="w-80px"> {{ scaleValue }}% </el-button>
           <el-button size="default" :plain="true" :icon="ZoomIn" @click="zoomIn()" />
+          <el-button size="default" @click="resetPosition">重置</el-button>
         </el-button-group>
       </el-row>
     </div>
-    <div class="simple-process-model" :style="`transform: scale(${scaleValue / 100});`">
+    <div
+      class="simple-process-model"
+      :style="`transform: translate(${currentX}px, ${currentY}px) scale(${scaleValue / 100});`"
+      @mousedown="startDrag"
+      @mousemove="onDrag"
+      @mouseup="stopDrag"
+      @mouseleave="stopDrag"
+      @mouseenter="setGrabCursor"
+    >
       <ProcessNodeTree v-if="processNodeTree" v-model:flow-node="processNodeTree" />
     </div>
   </div>
@@ -34,6 +59,8 @@ import ProcessNodeTree from './ProcessNodeTree.vue'
 import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from './consts'
 import { useWatchNode } from './node'
 import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue'
+import { isString } from '@/utils/is'
+import download from '@/utils/download'
 
 defineOptions({
   name: 'SimpleProcessModel'
@@ -52,17 +79,57 @@ const props = defineProps({
 })
 
 const emits = defineEmits<{
-  'save': [node: SimpleFlowNode | undefined]
+  save: [node: SimpleFlowNode | undefined]
 }>()
 
 const processNodeTree = useWatchNode(props)
 
 provide('readonly', props.readonly)
+
+// TODO 可优化:拖拽有点卡顿
+/** 拖拽、放大缩小等操作 */
 let scaleValue = ref(100)
 const MAX_SCALE_VALUE = 200
 const MIN_SCALE_VALUE = 50
+const isDragging = ref(false)
+const startX = ref(0)
+const startY = ref(0)
+const currentX = ref(0)
+const currentY = ref(0)
+const initialX = ref(0)
+const initialY = ref(0)
+
+const setGrabCursor = () => {
+  document.body.style.cursor = 'grab'
+}
+
+const resetCursor = () => {
+  document.body.style.cursor = 'default'
+}
+
+const startDrag = (e: MouseEvent) => {
+  isDragging.value = true
+  startX.value = e.clientX - currentX.value
+  startY.value = e.clientY - currentY.value
+  setGrabCursor() // 设置小手光标
+}
+
+const onDrag = (e: MouseEvent) => {
+  if (!isDragging.value) return
+  e.preventDefault() // 禁用文本选择
+
+  // 使用 requestAnimationFrame 优化性能
+  requestAnimationFrame(() => {
+    currentX.value = e.clientX - startX.value
+    currentY.value = e.clientY - startY.value
+  })
+}
+
+const stopDrag = () => {
+  isDragging.value = false
+  resetCursor() // 重置光标
+}
 
-// 放大
 const zoomIn = () => {
   if (scaleValue.value == MAX_SCALE_VALUE) {
     return
@@ -70,7 +137,6 @@ const zoomIn = () => {
   scaleValue.value += 10
 }
 
-// 缩小
 const zoomOut = () => {
   if (scaleValue.value == MIN_SCALE_VALUE) {
     return
@@ -82,10 +148,15 @@ const processReZoom = () => {
   scaleValue.value = 100
 }
 
+const resetPosition = () => {
+  currentX.value = initialX.value
+  currentY.value = initialY.value
+}
+
+/** 校验节点设置 */
 const errorDialogVisible = ref(false)
 let errorNodes: SimpleFlowNode[] = []
 
-// 校验节点设置。 暂时以 showText 为空 未节点错误配置
 const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
   if (node) {
     const { type, showText, conditionNodes } = node
@@ -143,6 +214,52 @@ const getCurrentFlowData = async () => {
 defineExpose({
   getCurrentFlowData
 })
+
+/** 导出 JSON */
+const exportJson = () => {
+  download.json(new Blob([JSON.stringify(processNodeTree.value)]), 'model.json')
+}
+
+/** 导入 JSON */
+const refFile = ref()
+const importJson = () => {
+  refFile.value.click()
+}
+const importLocalFile = () => {
+  const file = refFile.value.files[0]
+  const reader = new FileReader()
+  reader.readAsText(file)
+  reader.onload = function () {
+    if (isString(this.result)) {
+      processNodeTree.value = JSON.parse(this.result)
+      emits('save', processNodeTree.value)
+    }
+  }
+}
+
+// 在组件初始化时记录初始位置
+onMounted(() => {
+  initialX.value = currentX.value
+  initialY.value = currentY.value
+})
 </script>
 
-<style lang="scss" scoped></style>
+<style lang="scss" scoped>
+.simple-process-model-container {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  overflow: hidden;
+  user-select: none; // 禁用文本选择
+}
+
+.simple-process-model {
+  position: relative; // 确保相对定位
+  min-width: 100%; // 确保宽度为100%
+  min-height: 100%; // 确保高度为100%
+}
+
+.z-index-button-group {
+  z-index: 10;
+}
+</style>

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

@@ -45,4 +45,3 @@ watch(
 provide('tasks', approveTasks)
 provide('processInstance', currentProcessInstance)
 </script>
-p

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

@@ -23,11 +23,26 @@ export enum NodeType {
    */
   COPY_TASK_NODE = 12,
 
+  /**
+   * 办理人节点
+   */
+  TRANSACTOR_NODE = 13,
+
   /**
    * 延迟器节点
    */
   DELAY_TIMER_NODE = 14,
 
+  /**
+   * 触发器节点
+   */
+  TRIGGER_NODE = 15,
+
+  /**
+   * 子流程节点
+   */
+  CHILD_PROCESS_NODE = 20,
+
   /**
    * 条件节点
    */
@@ -44,7 +59,11 @@ export enum NodeType {
   /**
    * 包容分支节点 (对应包容网关)
    */
-  INCLUSIVE_BRANCH_NODE = 53
+  INCLUSIVE_BRANCH_NODE = 53,
+  /**
+   * 路由分支节点
+   */
+  ROUTER_BRANCH_NODE = 54
 }
 
 export enum NodeId {
@@ -93,18 +112,29 @@ export interface SimpleFlowNode {
   assignEmptyHandler?: AssignEmptyHandler
   // 审批节点的审批人与发起人相同时,对应的处理类型
   assignStartUserHandlerType?: number
-  // 条件类型
-  conditionType?: ConditionType
-  // 条件表达式
-  conditionExpression?: string
-  // 条件组
-  conditionGroups?: ConditionGroup
-  // 是否默认的条件
-  defaultFlow?: boolean
+  // 创建任务监听器
+  taskCreateListener?: ListenerHandler
+  // 创建任务监听器
+  taskAssignListener?: ListenerHandler
+  // 创建任务监听器
+  taskCompleteListener?: ListenerHandler
+  // 条件设置
+  conditionSetting?: ConditionSetting
   // 活动的状态,用于前端节点状态展示
   activityStatus?: TaskStatusEnum
   // 延迟设置
   delaySetting?: DelaySetting
+  // 路由分支
+  routerGroups?: RouterSetting[]
+  defaultFlowId?: string
+  // 签名
+  signEnable?: boolean
+  // 审批意见
+  reasonRequire?: boolean
+  // 触发器设置
+  triggerSetting?: TriggerSetting
+  // 子流程
+  childProcessSetting?: ChildProcessSetting
 }
 // 候选人策略枚举 ( 用于审批节点。抄送节点 )
 export enum CandidateStrategy {
@@ -132,6 +162,10 @@ export enum CandidateStrategy {
    * 指定用户
    */
   USER = 30,
+  /**
+   * 审批人自选
+   */
+  APPROVE_USER_SELECT = 34,
   /**
    * 发起人自选
    */
@@ -222,6 +256,41 @@ export type AssignEmptyHandler = {
   userIds?: number[]
 }
 
+/**
+ * 监听器的结构定义
+ */
+export type ListenerHandler = {
+  enable: boolean
+  path?: string
+  header?: HttpRequestParam[]
+  body?: HttpRequestParam[]
+}
+export type HttpRequestParam = {
+  key: string
+  type: number
+  value: string
+}
+export enum BpmHttpRequestParamTypeEnum {
+  /**
+   * 固定值
+   */
+  FIXED_VALUE = 1,
+  /**
+   * 表单
+   */
+  FROM_FORM = 2
+}
+export const BPM_HTTP_REQUEST_PARAM_TYPES = [
+  {
+    value: 1,
+    label: '固定值'
+  },
+  {
+    value: 2,
+    label: '表单'
+  }
+]
+
 // 审批拒绝类型枚举
 export enum RejectHandlerType {
   /**
@@ -315,6 +384,20 @@ export enum TimeUnitType {
   DAY = 3
 }
 
+/**
+ * 条件节点设置结构定义,用于条件节点
+ */
+export type ConditionSetting = {
+  // 条件类型
+  conditionType?: ConditionType
+  // 条件表达式
+  conditionExpression?: string
+  // 条件组
+  conditionGroups?: ConditionGroup
+  // 是否默认的条件
+  defaultFlow?: boolean
+}
+
 // 条件配置类型 ( 用于条件节点配置 )
 export enum ConditionType {
   /**
@@ -389,8 +472,6 @@ export enum OperationButtonType {
  * 条件规则结构定义
  */
 export type ConditionRule = {
-  type: number
-  opName: string
   opCode: string
   leftSide: string
   rightSide: string
@@ -405,6 +486,24 @@ export type ConditionGroup = {
   // 条件数组
   conditions: Condition[]
 }
+/**
+ * 条件组默认值
+ */
+export const DEFAULT_CONDITION_GROUP_VALUE = {
+  and: true,
+  conditions: [
+    {
+      and: true,
+      rules: [
+        {
+          opCode: '==',
+          leftSide: '',
+          rightSide: ''
+        }
+      ]
+    }
+  ]
+}
 
 /**
  * 条件结构定义
@@ -421,6 +520,10 @@ NODE_DEFAULT_TEXT.set(NodeType.COPY_TASK_NODE, '请配置抄送人')
 NODE_DEFAULT_TEXT.set(NodeType.CONDITION_NODE, '请设置条件')
 NODE_DEFAULT_TEXT.set(NodeType.START_USER_NODE, '请设置发起人')
 NODE_DEFAULT_TEXT.set(NodeType.DELAY_TIMER_NODE, '请设置延迟器')
+NODE_DEFAULT_TEXT.set(NodeType.ROUTER_BRANCH_NODE, '请设置路由节点')
+NODE_DEFAULT_TEXT.set(NodeType.TRIGGER_NODE, '请设置触发器')
+NODE_DEFAULT_TEXT.set(NodeType.TRANSACTOR_NODE, '请设置办理人')
+NODE_DEFAULT_TEXT.set(NodeType.CHILD_PROCESS_NODE, '请设置子流程')
 
 export const NODE_DEFAULT_NAME = new Map<number, string>()
 NODE_DEFAULT_NAME.set(NodeType.USER_TASK_NODE, '审批人')
@@ -428,15 +531,21 @@ NODE_DEFAULT_NAME.set(NodeType.COPY_TASK_NODE, '抄送人')
 NODE_DEFAULT_NAME.set(NodeType.CONDITION_NODE, '条件')
 NODE_DEFAULT_NAME.set(NodeType.START_USER_NODE, '发起人')
 NODE_DEFAULT_NAME.set(NodeType.DELAY_TIMER_NODE, '延迟器')
+NODE_DEFAULT_NAME.set(NodeType.ROUTER_BRANCH_NODE, '路由分支')
+NODE_DEFAULT_NAME.set(NodeType.TRIGGER_NODE, '触发器')
+NODE_DEFAULT_NAME.set(NodeType.TRANSACTOR_NODE, '办理人')
+NODE_DEFAULT_NAME.set(NodeType.CHILD_PROCESS_NODE, '子流程')
 
 // 候选人策略。暂时不从字典中取。 后续可能调整。控制显示顺序
 export const CANDIDATE_STRATEGY: DictDataVO[] = [
   { label: '指定成员', value: CandidateStrategy.USER },
   { label: '指定角色', value: CandidateStrategy.ROLE },
+  { label: '指定岗位', value: CandidateStrategy.POST },
   { label: '部门成员', value: CandidateStrategy.DEPT_MEMBER },
   { label: '部门负责人', value: CandidateStrategy.DEPT_LEADER },
   { label: '连续多级部门负责人', value: CandidateStrategy.MULTI_LEVEL_DEPT_LEADER },
   { label: '发起人自选', value: CandidateStrategy.START_USER_SELECT },
+  { label: '审批人自选', value: CandidateStrategy.APPROVE_USER_SELECT },
   { label: '发起人本人', value: CandidateStrategy.START_USER },
   { label: '发起人部门负责人', value: CandidateStrategy.START_USER_DEPT_LEADER },
   { label: '发起人连续部门负责人', value: CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER },
@@ -460,8 +569,8 @@ export const APPROVE_METHODS: DictDataVO[] = [
 ]
 
 export const CONDITION_CONFIG_TYPES: DictDataVO[] = [
-  { label: '条件表达式', value: ConditionType.EXPRESSION },
-  { label: '条件规则', value: ConditionType.RULE }
+  { label: '条件规则', value: ConditionType.RULE },
+  { label: '条件表达式', value: ConditionType.EXPRESSION }
 ]
 
 // 时间单位类型
@@ -540,6 +649,16 @@ export const DEFAULT_BUTTON_SETTING: ButtonSetting[] = [
   { id: OperationButtonType.RETURN, displayName: '退回', enable: true }
 ]
 
+// 办理人默认的按钮权限设置
+export const TRANSACTOR_DEFAULT_BUTTON_SETTING: ButtonSetting[] = [
+  { id: OperationButtonType.APPROVE, displayName: '办理', enable: true },
+  { id: OperationButtonType.REJECT, displayName: '拒绝', enable: false },
+  { 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 }
+]
+
 // 发起人的按钮权限。暂时定死,不可以编辑
 export const START_USER_BUTTON_SETTING: ButtonSetting[] = [
   { id: OperationButtonType.APPROVE, displayName: '提交', enable: true },
@@ -575,7 +694,15 @@ export enum ProcessVariableEnum {
   /**
    * 发起用户 ID
    */
-  START_USER_ID = 'PROCESS_START_USER_ID'
+  START_USER_ID = 'PROCESS_START_USER_ID',
+  /**
+   * 发起时间
+   */
+  START_TIME = 'PROCESS_START_TIME',
+  /**
+   * 流程定义名称
+   */
+  PROCESS_DEFINITION_NAME = 'PROCESS_DEFINITION_NAME'
 }
 
 /**
@@ -604,3 +731,170 @@ export const DELAY_TYPE = [
   { label: '固定时长', value: DelayTypeEnum.FIXED_TIME_DURATION },
   { label: '固定日期', value: DelayTypeEnum.FIXED_DATE_TIME }
 ]
+
+/**
+ * 路由分支结构定义
+ */
+export type RouterSetting = {
+  nodeId: string
+  conditionType: ConditionType
+  conditionExpression: string
+  conditionGroups: ConditionGroup
+}
+
+// ==================== 触发器相关定义 ====================
+/**
+ * 触发器节点结构定义
+ */
+export type TriggerSetting = {
+  type: TriggerTypeEnum
+  httpRequestSetting?: HttpRequestTriggerSetting
+  formSettings?: FormTriggerSetting[]
+}
+
+/**
+ * 触发器类型枚举
+ */
+export enum TriggerTypeEnum {
+  /**
+   * 发送 HTTP 请求触发器
+   */
+  HTTP_REQUEST = 1,
+  /**
+   * 接收 HTTP 回调请求触发器
+   */
+  HTTP_CALLBACK = 2,
+  /**
+   * 表单数据更新触发器
+   */
+  FORM_UPDATE = 10,
+  /**
+   * 表单数据删除触发器
+   */
+  FORM_DELETE = 11
+}
+
+/**
+ * HTTP 请求触发器结构定义
+ */
+export type HttpRequestTriggerSetting = {
+  // 请求 URL
+  url: string
+  // 请求头参数设置
+  header?: HttpRequestParam[]
+  // 请求体参数设置
+  body?: HttpRequestParam[]
+  // 请求响应设置
+  response?: Record<string, string>[]
+}
+
+/**
+ * 流程表单触发器配置结构定义
+ */
+export type FormTriggerSetting = {
+  // 条件类型
+  conditionType?: ConditionType
+  // 条件表达式
+  conditionExpression?: string
+  // 条件组
+  conditionGroups?: ConditionGroup
+  // 更新表单字段配置
+  updateFormFields?: Record<string, any>
+  // 删除表单字段配置
+  deleteFields?: string[]
+}
+
+export const TRIGGER_TYPES: DictDataVO[] = [
+  { label: '发送 HTTP 请求', value: TriggerTypeEnum.HTTP_REQUEST },
+  { label: '接收 HTTP 回调', value: TriggerTypeEnum.HTTP_CALLBACK },
+  { label: '修改表单数据', value: TriggerTypeEnum.FORM_UPDATE },
+  { label: '删除表单数据', value: TriggerTypeEnum.FORM_DELETE }
+]
+
+/**
+ * 子流程节点结构定义
+ */
+export type ChildProcessSetting = {
+  calledProcessDefinitionKey: string
+  calledProcessDefinitionName: string
+  async: boolean
+  inVariables?: IOParameter[]
+  outVariables?: IOParameter[]
+  skipStartUserNode: boolean
+  startUserSetting: StartUserSetting
+  timeoutSetting: TimeoutSetting
+  multiInstanceSetting: MultiInstanceSetting
+}
+export type IOParameter = {
+  source: string
+  target: string
+}
+export type StartUserSetting = {
+  type: ChildProcessStartUserTypeEnum
+  formField?: string
+  emptyType?: ChildProcessStartUserEmptyTypeEnum
+}
+export type TimeoutSetting = {
+  enable: boolean
+  type?: DelayTypeEnum
+  timeExpression?: string
+}
+export type MultiInstanceSetting = {
+  enable: boolean
+  sequential?: boolean
+  approveRatio?: number
+  sourceType?: ChildProcessMultiInstanceSourceTypeEnum
+  source?: string
+}
+export enum ChildProcessStartUserTypeEnum {
+  /**
+   * 同主流程发起人
+   */
+  MAIN_PROCESS_START_USER = 1,
+  /**
+   * 表单
+   */
+  FROM_FORM = 2
+}
+export const CHILD_PROCESS_START_USER_TYPE = [
+  { label: '同主流程发起人', value: ChildProcessStartUserTypeEnum.MAIN_PROCESS_START_USER },
+  { label: '表单', value: ChildProcessStartUserTypeEnum.FROM_FORM }
+]
+export enum ChildProcessStartUserEmptyTypeEnum {
+  /**
+   * 同主流程发起人
+   */
+  MAIN_PROCESS_START_USER = 1,
+  /**
+   * 子流程管理员
+   */
+  CHILD_PROCESS_ADMIN = 2,
+  /**
+   * 主流程管理员
+   */
+  MAIN_PROCESS_ADMIN = 3
+}
+export const CHILD_PROCESS_START_USER_EMPTY_TYPE = [
+  { label: '同主流程发起人', value: ChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER },
+  { label: '子流程管理员', value: ChildProcessStartUserEmptyTypeEnum.CHILD_PROCESS_ADMIN },
+  { label: '主流程管理员', value: ChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_ADMIN }
+]
+export enum ChildProcessMultiInstanceSourceTypeEnum {
+  /**
+   * 固定数量
+   */
+  FIXED_QUANTITY = 1,
+  /**
+   * 数字表单
+   */
+  NUMBER_FORM = 2,
+  /**
+   * 多选表单
+   */
+  MULTIPLE_FORM = 3
+}
+export const CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE = [
+  { label: '固定数量', value: ChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY },
+  { label: '数字表单', value: ChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM },
+  { label: '多选表单', value: ChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM }
+]

+ 123 - 18
src/components/SimpleProcessDesignerV2/src/node.ts

@@ -1,4 +1,3 @@
-import { cloneDeep } from 'lodash-es'
 import { TaskStatusEnum } from '@/api/bpm/task'
 import * as RoleApi from '@/api/system/role'
 import * as DeptApi from '@/api/system/dept'
@@ -14,9 +13,15 @@ import {
   NODE_DEFAULT_NAME,
   AssignStartUserHandlerType,
   AssignEmptyHandlerType,
-  FieldPermissionType
+  FieldPermissionType,
+  HttpRequestParam,
+  ProcessVariableEnum,
+  ConditionType,
+  ConditionGroup,
+  COMPARISON_OPERATORS
 } from './consts'
-import { parseFormFields } from '@/components/FormCreate/src/utils/index'
+import { parseFormFields } from '@/components/FormCreate/src/utils'
+
 export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> {
   const node = ref<SimpleFlowNode>(props.flowNode)
   watch(
@@ -46,9 +51,9 @@ export function useFormFieldsPermission(defaultPermission: FieldPermissionType)
   // 字段权限配置. 需要有 field, title,  permissioin 属性
   const fieldsPermissionConfig = ref<Array<Record<string, any>>>([])
 
-  const formType = inject<Ref<number>>('formType') // 表单类型
+  const formType = inject<Ref<number | undefined>>('formType', ref()) // 表单类型
 
-  const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
+  const formFields = inject<Ref<string[]>>('formFields', ref([])) // 流程表单字段
 
   const getNodeConfigFormFields = (nodeFormFields?: Array<Record<string, string>>) => {
     nodeFormFields = toRaw(nodeFormFields)
@@ -104,16 +109,32 @@ export function useFormFieldsPermission(defaultPermission: FieldPermissionType)
     getNodeConfigFormFields
   }
 }
+
 /**
- * @description 获取表单的字段
+ * @description 获取流程表单的字段
  */
 export function useFormFields() {
-  const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
+  const formFields = inject<Ref<string[]>>('formFields', ref([])) // 流程表单字段
   return parseFormCreateFields(unref(formFields))
 }
 
+// TODO @芋艿:后续需要把各种类似 useFormFieldsPermission 的逻辑,抽成一个通用方法。
+/**
+ * @description 获取流程表单的字段和发起人字段
+ */
+export function useFormFieldsAndStartUser() {
+  const injectFormFields = inject<Ref<string[]>>('formFields', ref([])) // 流程表单字段
+  const formFields = parseFormCreateFields(unref(injectFormFields))
+  // 添加发起人
+  formFields.unshift({
+    field: ProcessVariableEnum.START_USER_ID,
+    title: '发起人',
+    required: true
+  })
+  return formFields
+}
+
 export type UserTaskFormType = {
-  //candidateParamArray: any[]
   candidateStrategy: CandidateStrategy
   approveMethod: ApproveMethodType
   roleIds?: number[] // 角色
@@ -136,10 +157,29 @@ export type UserTaskFormType = {
   timeDuration?: number
   maxRemindCount?: number
   buttonsSetting: any[]
+  taskCreateListenerEnable?: boolean
+  taskCreateListenerPath?: string
+  taskCreateListener?: {
+    header: HttpRequestParam[]
+    body: HttpRequestParam[]
+  }
+  taskAssignListenerEnable?: boolean
+  taskAssignListenerPath?: string
+  taskAssignListener?: {
+    header: HttpRequestParam[]
+    body: HttpRequestParam[]
+  }
+  taskCompleteListenerEnable?: boolean
+  taskCompleteListenerPath?: string
+  taskCompleteListener?: {
+    header: HttpRequestParam[]
+    body: HttpRequestParam[]
+  }
+  signEnable: boolean
+  reasonRequire: boolean
 }
 
 export type CopyTaskFormType = {
-  // candidateParamArray: any[]
   candidateStrategy: CandidateStrategy
   roleIds?: number[] // 角色
   deptIds?: number[] // 部门
@@ -156,15 +196,15 @@ export type CopyTaskFormType = {
  * @description 节点表单数据。 用于审批节点、抄送节点
  */
 export function useNodeForm(nodeType: NodeType) {
-  const roleOptions = inject<Ref<RoleApi.RoleVO[]>>('roleList') // 角色列表
-  const postOptions = inject<Ref<PostApi.PostVO[]>>('postList') // 岗位列表
-  const userOptions = inject<Ref<UserApi.UserVO[]>>('userList') // 用户列表
-  const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList') // 部门列表
-  const userGroupOptions = inject<Ref<UserGroupApi.UserGroupVO[]>>('userGroupList') // 用户组列表
-  const deptTreeOptions = inject('deptTree') // 部门树
-  const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
+  const roleOptions = inject<Ref<RoleApi.RoleVO[]>>('roleList', ref([])) // 角色列表
+  const postOptions = inject<Ref<PostApi.PostVO[]>>('postList', ref([])) // 岗位列表
+  const userOptions = inject<Ref<UserApi.UserVO[]>>('userList', ref([])) // 用户列表
+  const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList', ref([])) // 部门列表
+  const userGroupOptions = inject<Ref<UserGroupApi.UserGroupVO[]>>('userGroupList', ref([])) // 用户组列表
+  const deptTreeOptions = inject('deptTree', ref()) // 部门树
+  const formFields = inject<Ref<string[]>>('formFields', ref([])) // 流程表单字段
   const configForm = ref<UserTaskFormType | CopyTaskFormType>()
-  if (nodeType === NodeType.USER_TASK_NODE) {
+  if (nodeType === NodeType.USER_TASK_NODE || nodeType === NodeType.TRANSACTOR_NODE) {
     configForm.value = {
       candidateStrategy: CandidateStrategy.USER,
       approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE,
@@ -270,6 +310,11 @@ export function useNodeForm(nodeType: NodeType) {
       showText = `表单内部门负责人`
     }
 
+    // 审批人自选
+    if (configForm.value?.candidateStrategy === CandidateStrategy.APPROVE_USER_SELECT) {
+      showText = `审批人自选`
+    }
+
     // 发起人自选
     if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER_SELECT) {
       showText = `发起人自选`
@@ -506,6 +551,66 @@ export function useTaskStatusClass(taskStatus: TaskStatusEnum | undefined): stri
   if (taskStatus === TaskStatusEnum.CANCEL) {
     return 'status-cancel'
   }
-
   return ''
 }
+
+/** 条件组件文字展示 */
+export function getConditionShowText(
+  conditionType: ConditionType | undefined,
+  conditionExpression: string | undefined,
+  conditionGroups: ConditionGroup | undefined,
+  fieldOptions: Array<Record<string, any>>
+) {
+  let showText = ''
+  if (conditionType === ConditionType.EXPRESSION) {
+    if (conditionExpression) {
+      showText = `表达式:${conditionExpression}`
+    }
+  }
+  if (conditionType === ConditionType.RULE) {
+    // 条件组是否为与关系
+    const groupAnd = conditionGroups?.and
+    let warningMessage: undefined | string = undefined
+    const conditionGroup = conditionGroups?.conditions.map((item) => {
+      return (
+        '(' +
+        item.rules
+          .map((rule) => {
+            if (rule.leftSide && rule.rightSide) {
+              return (
+                getFormFieldTitle(fieldOptions, rule.leftSide) +
+                ' ' +
+                getOpName(rule.opCode) +
+                ' ' +
+                rule.rightSide
+              )
+            } else {
+              // 有一条规则不完善。提示错误
+              warningMessage = '请完善条件规则'
+              return ''
+            }
+          })
+          .join(item.and ? ' 且 ' : ' 或 ') +
+        ' ) '
+      )
+    })
+    if (warningMessage) {
+      showText = ''
+    } else {
+      showText = conditionGroup!.join(groupAnd ? ' 且 ' : ' 或 ')
+    }
+  }
+  return showText
+}
+
+/** 获取表单字段名称*/
+const getFormFieldTitle = (fieldOptions: Array<Record<string, any>>, field: string) => {
+  const item = fieldOptions.find((item) => item.field === field)
+  return item?.title
+}
+
+/** 获取操作符名称 */
+const getOpName = (opCode: string): string => {
+  const opName = COMPARISON_OPERATORS.find((item: any) => item.value === opCode)
+  return opName?.label
+}

+ 610 - 0
src/components/SimpleProcessDesignerV2/src/nodes-config/ChildProcessNodeConfig.vue

@@ -0,0 +1,610 @@
+<template>
+  <el-drawer
+    :append-to-body="true"
+    v-model="settingVisible"
+    :show-close="false"
+    :size="550"
+    :before-close="saveConfig"
+  >
+    <template #header>
+      <div class="config-header">
+        <input
+          v-if="showInput"
+          type="text"
+          class="config-editable-input"
+          @blur="blurEvent()"
+          v-mountedFocus
+          v-model="nodeName"
+          :placeholder="nodeName"
+        />
+        <div v-else class="node-name">
+          {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
+        </div>
+        <div class="divide-line"></div>
+      </div>
+    </template>
+    <el-tabs type="border-card" v-model="activeTabName">
+      <el-tab-pane label="子流程" name="child">
+        <div>
+          <el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
+            <el-form-item label="是否异步" prop="async">
+              <el-switch v-model="configForm.async" active-text="异步" inactive-text="不异步" />
+            </el-form-item>
+            <el-form-item label="选择子流程" prop="calledProcessDefinitionKey">
+              <el-select
+                v-model="configForm.calledProcessDefinitionKey"
+                clearable
+                @change="handleCalledElementChange"
+              >
+                <el-option
+                  v-for="(item, index) in childProcessOptions"
+                  :key="index"
+                  :label="item.name"
+                  :value="item.key"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="是否自动跳过子流程发起节点" prop="skipStartUserNode">
+              <el-switch
+                v-model="configForm.skipStartUserNode"
+                active-text="跳过"
+                inactive-text="不跳过"
+              />
+            </el-form-item>
+            <el-form-item label="主→子变量传递" prop="inVariables">
+              <div class="flex pt-2" v-for="(item, index) in configForm.inVariables" :key="index">
+                <div class="mr-2">
+                  <el-form-item
+                    :prop="`inVariables.${index}.source`"
+                    :rules="{
+                      required: true,
+                      message: '变量不能为空',
+                      trigger: 'blur'
+                    }"
+                  >
+                    <el-select class="w-200px!" v-model="item.source">
+                      <el-option
+                        v-for="(field, fIdx) in formFieldOptions"
+                        :key="fIdx"
+                        :label="field.title"
+                        :value="field.field"
+                      />
+                    </el-select>
+                  </el-form-item>
+                </div>
+                <div class="mr-2">
+                  <el-form-item
+                    :prop="`inVariables.${index}.target`"
+                    :rules="{
+                      required: true,
+                      message: '变量不能为空',
+                      trigger: 'blur'
+                    }"
+                  >
+                    <el-select class="w-200px!" v-model="item.target">
+                      <el-option
+                        v-for="(field, fIdx) in childFormFieldOptions"
+                        :key="fIdx"
+                        :label="field.title"
+                        :value="field.field"
+                      />
+                    </el-select>
+                  </el-form-item>
+                </div>
+                <div class="mr-1 flex items-center">
+                  <Icon
+                    icon="ep:delete"
+                    :size="18"
+                    @click="deleteVariable(index, configForm.inVariables)"
+                  />
+                </div>
+              </div>
+              <el-button type="primary" text @click="addVariable(configForm.inVariables)">
+                <Icon icon="ep:plus" class="mr-5px" />添加一行
+              </el-button>
+            </el-form-item>
+            <el-form-item
+              v-if="configForm.async === false"
+              label="子→主变量传递"
+              prop="outVariables"
+            >
+              <div class="flex pt-2" v-for="(item, index) in configForm.outVariables" :key="index">
+                <div class="mr-2">
+                  <el-form-item
+                    :prop="`outVariables.${index}.source`"
+                    :rules="{
+                      required: true,
+                      message: '变量不能为空',
+                      trigger: 'blur'
+                    }"
+                  >
+                    <el-select class="w-200px!" v-model="item.source">
+                      <el-option
+                        v-for="(field, fIdx) in childFormFieldOptions"
+                        :key="fIdx"
+                        :label="field.title"
+                        :value="field.field"
+                      />
+                    </el-select>
+                  </el-form-item>
+                </div>
+                <div class="mr-2">
+                  <el-form-item
+                    :prop="`outVariables.${index}.target`"
+                    :rules="{
+                      required: true,
+                      message: '变量不能为空',
+                      trigger: 'blur'
+                    }"
+                  >
+                    <el-select class="w-200px!" v-model="item.target">
+                      <el-option
+                        v-for="(field, fIdx) in formFieldOptions"
+                        :key="fIdx"
+                        :label="field.title"
+                        :value="field.field"
+                      />
+                    </el-select>
+                  </el-form-item>
+                </div>
+                <div class="mr-1 flex items-center">
+                  <Icon
+                    icon="ep:delete"
+                    :size="18"
+                    @click="deleteVariable(index, configForm.outVariables)"
+                  />
+                </div>
+              </div>
+              <el-button type="primary" text @click="addVariable(configForm.outVariables)">
+                <Icon icon="ep:plus" class="mr-5px" />添加一行
+              </el-button>
+            </el-form-item>
+            <el-form-item label="子流程发起人" prop="startUserType">
+              <el-radio-group v-model="configForm.startUserType">
+                <el-radio
+                  v-for="item in CHILD_PROCESS_START_USER_TYPE"
+                  :key="item.value"
+                  :value="item.value"
+                >
+                  {{ item.label }}
+                </el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item
+              v-if="configForm.startUserType === ChildProcessStartUserTypeEnum.FROM_FORM"
+              label="当子流程发起人为空时"
+              prop="startUserType"
+            >
+              <el-radio-group v-model="configForm.startUserEmptyType">
+                <el-radio
+                  v-for="item in CHILD_PROCESS_START_USER_EMPTY_TYPE"
+                  :key="item.value"
+                  :value="item.value"
+                >
+                  {{ item.label }}
+                </el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item
+              v-if="configForm.startUserType === 2"
+              label="发起人表单"
+              prop="startUserFormField"
+            >
+              <el-select class="w-200px!" v-model="configForm.startUserFormField">
+                <el-option
+                  v-for="(field, fIdx) in formFieldOptions"
+                  :key="fIdx"
+                  :label="field.title"
+                  :value="field.field"
+                />
+              </el-select>
+            </el-form-item>
+
+            <el-divider content-position="left">超时设置</el-divider>
+            <el-form-item label="启用开关" prop="timeoutEnable">
+              <el-switch
+                v-model="configForm.timeoutEnable"
+                active-text="开启"
+                inactive-text="关闭"
+              />
+            </el-form-item>
+            <div v-if="configForm.timeoutEnable">
+              <el-form-item prop="timeoutType">
+                <el-radio-group v-model="configForm.timeoutType">
+                  <el-radio-button
+                    v-for="item in DELAY_TYPE"
+                    :key="item.value"
+                    :label="item.label"
+                    :value="item.value"
+                  />
+                </el-radio-group>
+              </el-form-item>
+              <el-form-item v-if="configForm.timeoutType === DelayTypeEnum.FIXED_TIME_DURATION">
+                <el-form-item prop="timeDuration">
+                  <el-input-number
+                    class="mr-2"
+                    :style="{ width: '100px' }"
+                    v-model="configForm.timeDuration"
+                    :min="1"
+                    controls-position="right"
+                  />
+                </el-form-item>
+                <el-select v-model="configForm.timeUnit" class="mr-2" :style="{ width: '100px' }">
+                  <el-option
+                    v-for="item in TIME_UNIT_TYPES"
+                    :key="item.value"
+                    :label="item.label"
+                    :value="item.value"
+                  />
+                </el-select>
+                <el-text>后进入下一节点</el-text>
+              </el-form-item>
+              <el-form-item
+                v-if="configForm.timeoutType === DelayTypeEnum.FIXED_DATE_TIME"
+                prop="dateTime"
+              >
+                <el-date-picker
+                  class="mr-2"
+                  v-model="configForm.dateTime"
+                  type="datetime"
+                  placeholder="请选择日期和时间"
+                  value-format="YYYY-MM-DDTHH:mm:ss"
+                />
+                <el-text>后进入下一节点</el-text>
+              </el-form-item>
+            </div>
+
+            <el-divider content-position="left">多实例设置</el-divider>
+            <el-form-item label="启用开关" prop="multiInstanceEnable">
+              <el-switch
+                v-model="configForm.multiInstanceEnable"
+                active-text="开启"
+                inactive-text="关闭"
+              />
+            </el-form-item>
+            <div v-if="configForm.multiInstanceEnable">
+              <el-form-item prop="sequential">
+                <el-switch
+                  v-model="configForm.sequential"
+                  active-text="串行"
+                  inactive-text="并行"
+                />
+              </el-form-item>
+              <el-form-item prop="approveRatio">
+                <el-text>完成比例(%)</el-text>
+                <el-input-number
+                  class="ml-10px"
+                  v-model="configForm.approveRatio"
+                  :min="10"
+                  :max="100"
+                  :step="10"
+                />
+              </el-form-item>
+              <el-form-item prop="multiInstanceSourceType">
+                <el-text>多实例来源</el-text>
+                <el-select
+                  class="ml-10px w-200px!"
+                  v-model="configForm.multiInstanceSourceType"
+                  @change="handleMultiInstanceSourceTypeChange"
+                >
+                  <el-option
+                    v-for="item in CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE"
+                    :key="item.value"
+                    :label="item.label"
+                    :value="item.value"
+                  />
+                </el-select>
+              </el-form-item>
+              <el-form-item v-if="configForm.multiInstanceSourceType === ChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY">
+                <el-input-number v-model="configForm.multiInstanceSource" :min="1" />
+              </el-form-item>
+              <el-form-item v-if="configForm.multiInstanceSourceType === ChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM">
+                <el-select class="w-200px!" v-model="configForm.multiInstanceSource">
+                  <el-option
+                    v-for="(field, fIdx) in digitalFormFieldOptions"
+                    :key="fIdx"
+                    :label="field.title"
+                    :value="field.field"
+                  />
+                </el-select>
+              </el-form-item>
+              <el-form-item v-if="configForm.multiInstanceSourceType === ChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM">
+                <el-select class="w-200px!" v-model="configForm.multiInstanceSource">
+                  <el-option
+                    v-for="(field, fIdx) in multiFormFieldOptions"
+                    :key="fIdx"
+                    :label="field.title"
+                    :value="field.field"
+                  />
+                </el-select>
+              </el-form-item>
+            </div>
+          </el-form>
+        </div>
+      </el-tab-pane>
+    </el-tabs>
+    <template #footer>
+      <el-divider />
+      <div>
+        <el-button type="primary" @click="saveConfig">确 定</el-button>
+        <el-button @click="closeDrawer">取 消</el-button>
+      </div>
+    </template>
+  </el-drawer>
+</template>
+<script setup lang="ts">
+import { getModelList } from '@/api/bpm/model'
+import { getForm } from '@/api/bpm/form'
+import {
+  SimpleFlowNode,
+  NodeType,
+  TIME_UNIT_TYPES,
+  TimeUnitType,
+  DelayTypeEnum,
+  DELAY_TYPE,
+  IOParameter,
+  ChildProcessStartUserTypeEnum,
+  CHILD_PROCESS_START_USER_TYPE,
+  ChildProcessStartUserEmptyTypeEnum,
+  CHILD_PROCESS_START_USER_EMPTY_TYPE,
+  CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE,
+  ChildProcessMultiInstanceSourceTypeEnum
+} from '../consts'
+import { useWatchNode, useDrawer, useNodeName, useFormFieldsAndStartUser } from '../node'
+import { parseFormFields } from '@/components/FormCreate/src/utils'
+import { convertTimeUnit } from '../utils'
+defineOptions({
+  name: 'ChildProcessNodeConfig'
+})
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  }
+})
+// 抽屉配置
+const { settingVisible, closeDrawer, openDrawer } = useDrawer()
+// 当前节点
+const currentNode = useWatchNode(props)
+// 节点名称
+const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.CHILD_PROCESS_NODE)
+// 激活的 Tab 标签页
+const activeTabName = ref('child')
+// 子流程表单配置
+const formRef = ref() // 表单 Ref
+// 表单校验规则
+const formRules = reactive({
+  async: [{ required: true, message: '是否异步不能为空', trigger: 'change' }],
+  calledProcessDefinitionKey: [{ required: true, message: '子流程不能为空', trigger: 'change' }],
+  skipStartUserNode: [
+    { required: true, message: '是否自动跳过子流程发起节点不能为空', trigger: 'change' }
+  ],
+  startUserType: [{ required: true, message: '子流程发起人不能为空', trigger: 'change' }],
+  startUserEmptyType: [
+    { required: true, message: '当子流程发起人为空时不能为空', trigger: 'change' }
+  ],
+  startUserFormField: [{ required: true, message: '发起人表单不能为空', trigger: 'change' }],
+  timeoutEnable: [{ required: true, message: '超时设置是否开启不能为空', trigger: 'change' }],
+  timeoutType: [{ required: true, message: '超时设置时间不能为空', trigger: 'change' }],
+  timeDuration: [{ required: true, message: '超时设置时间不能为空', trigger: 'change' }],
+  dateTime: [{ required: true, message: '超时设置时间不能为空', trigger: 'change' }],
+  multiInstanceEnable: [{ required: true, message: '多实例设置不能为空', trigger: 'change' }]
+})
+type ChildProcessFormType = {
+  async: boolean
+  calledProcessDefinitionKey: string
+  skipStartUserNode: boolean
+  inVariables?: IOParameter[]
+  outVariables?: IOParameter[]
+  startUserType: ChildProcessStartUserTypeEnum
+  startUserEmptyType: ChildProcessStartUserEmptyTypeEnum
+  startUserFormField: string
+  timeoutEnable: boolean
+  timeoutType: DelayTypeEnum
+  timeDuration: number
+  timeUnit: TimeUnitType
+  dateTime: string
+  multiInstanceEnable: boolean
+  sequential: boolean
+  approveRatio: number
+  multiInstanceSourceType: ChildProcessMultiInstanceSourceTypeEnum
+  multiInstanceSource: string
+}
+const configForm = ref<ChildProcessFormType>({
+  async: false,
+  calledProcessDefinitionKey: '',
+  skipStartUserNode: false,
+  inVariables: [],
+  outVariables: [],
+  startUserType: ChildProcessStartUserTypeEnum.MAIN_PROCESS_START_USER,
+  startUserEmptyType: ChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER,
+  startUserFormField: '',
+  timeoutEnable: false,
+  timeoutType: DelayTypeEnum.FIXED_TIME_DURATION,
+  timeDuration: 1,
+  timeUnit: TimeUnitType.HOUR,
+  dateTime: '',
+  multiInstanceEnable: false,
+  sequential: false,
+  approveRatio: 100,
+  multiInstanceSourceType: ChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY,
+  multiInstanceSource: ''
+})
+const childProcessOptions = ref()
+const formFieldOptions = useFormFieldsAndStartUser()
+const digitalFormFieldOptions = computed(() => {
+  return formFieldOptions.filter((item) => item.type === 'inputNumber')
+})
+const multiFormFieldOptions = computed(() => {
+  return formFieldOptions.filter((item) => item.type === 'select' || item.type === 'checkbox')
+})
+const childFormFieldOptions = ref()
+
+// 保存配置
+const saveConfig = async () => {
+  activeTabName.value = 'child'
+  if (!formRef) return false
+  const valid = await formRef.value.validate()
+  if (!valid) return false
+  const childInfo = childProcessOptions.value.find(
+    (option: any) => option.key === configForm.value.calledProcessDefinitionKey
+  )
+  currentNode.value.name = nodeName.value!
+  if (currentNode.value.childProcessSetting) {
+    // 1. 是否异步
+    currentNode.value.childProcessSetting.async = configForm.value.async
+    // 2. 调用流程
+    currentNode.value.childProcessSetting.calledProcessDefinitionKey = childInfo.key
+    currentNode.value.childProcessSetting.calledProcessDefinitionName = childInfo.name
+    // 3. 是否跳过发起人
+    currentNode.value.childProcessSetting.skipStartUserNode = configForm.value.skipStartUserNode
+    // 4. 主->子变量
+    currentNode.value.childProcessSetting.inVariables = configForm.value.inVariables
+    // 5. 子->主变量
+    currentNode.value.childProcessSetting.outVariables = configForm.value.outVariables
+    // 6. 发起人设置
+    currentNode.value.childProcessSetting.startUserSetting.type = configForm.value.startUserType
+    currentNode.value.childProcessSetting.startUserSetting.emptyType =
+      configForm.value.startUserEmptyType
+    currentNode.value.childProcessSetting.startUserSetting.formField =
+      configForm.value.startUserFormField
+    // 7. 超时设置
+    currentNode.value.childProcessSetting.timeoutSetting = {
+      enable: configForm.value.timeoutEnable
+    }
+    if (configForm.value.timeoutEnable) {
+      currentNode.value.childProcessSetting.timeoutSetting.type = configForm.value.timeoutType
+      if (configForm.value.timeoutType === DelayTypeEnum.FIXED_TIME_DURATION) {
+        currentNode.value.childProcessSetting.timeoutSetting.timeExpression = getIsoTimeDuration()
+      }
+      if (configForm.value.timeoutType === DelayTypeEnum.FIXED_DATE_TIME) {
+        currentNode.value.childProcessSetting.timeoutSetting.timeExpression =
+          configForm.value.dateTime
+      }
+    }
+    // 8. 多实例设置
+    currentNode.value.childProcessSetting.multiInstanceSetting = {
+      enable: configForm.value.multiInstanceEnable
+    }
+    if (configForm.value.multiInstanceEnable) {
+      currentNode.value.childProcessSetting.multiInstanceSetting.sequential =
+        configForm.value.sequential
+      currentNode.value.childProcessSetting.multiInstanceSetting.approveRatio =
+        configForm.value.approveRatio
+      currentNode.value.childProcessSetting.multiInstanceSetting.sourceType =
+        configForm.value.multiInstanceSourceType
+      currentNode.value.childProcessSetting.multiInstanceSetting.source =
+        configForm.value.multiInstanceSource
+    }
+  }
+
+  currentNode.value.showText = `调用子流程:${childInfo.name}`
+  settingVisible.value = false
+  return true
+}
+// 显示子流程节点配置, 由父组件传过来
+const showChildProcessNodeConfig = (node: SimpleFlowNode) => {
+  nodeName.value = node.name
+  if (node.childProcessSetting) {
+    // 1. 是否异步
+    configForm.value.async = node.childProcessSetting.async
+    // 2. 调用流程
+    configForm.value.calledProcessDefinitionKey =
+      node.childProcessSetting?.calledProcessDefinitionKey
+    // 3. 是否跳过发起人
+    configForm.value.skipStartUserNode = node.childProcessSetting.skipStartUserNode
+    // 4. 主->子变量
+    configForm.value.inVariables = node.childProcessSetting.inVariables
+    // 5. 子->主变量
+    configForm.value.outVariables = node.childProcessSetting.outVariables
+    // 6. 发起人设置
+    configForm.value.startUserType = node.childProcessSetting.startUserSetting.type
+    configForm.value.startUserEmptyType = node.childProcessSetting.startUserSetting.emptyType ?? ChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER
+    configForm.value.startUserFormField = node.childProcessSetting.startUserSetting.formField ?? ''
+    // 7. 超时设置
+    configForm.value.timeoutEnable = node.childProcessSetting.timeoutSetting.enable ?? false
+    if (configForm.value.timeoutEnable) {
+      configForm.value.timeoutType =
+        node.childProcessSetting.timeoutSetting.type ?? DelayTypeEnum.FIXED_TIME_DURATION
+      // 固定时长
+      if (configForm.value.timeoutType === DelayTypeEnum.FIXED_TIME_DURATION) {
+        const strTimeDuration = node.childProcessSetting.timeoutSetting.timeExpression ?? ''
+        let parseTime = strTimeDuration.slice(2, strTimeDuration.length - 1)
+        let parseTimeUnit = strTimeDuration.slice(strTimeDuration.length - 1)
+        configForm.value.timeDuration = parseInt(parseTime)
+        configForm.value.timeUnit = convertTimeUnit(parseTimeUnit)
+      }
+      // 固定日期时间
+      if (configForm.value.timeoutType === DelayTypeEnum.FIXED_DATE_TIME) {
+        configForm.value.dateTime = node.childProcessSetting.timeoutSetting.timeExpression ?? ''
+      }
+    }
+    // 8. 多实例设置
+    configForm.value.multiInstanceEnable =
+      node.childProcessSetting.multiInstanceSetting.enable ?? false
+    if (configForm.value.multiInstanceEnable) {
+      configForm.value.sequential =
+        node.childProcessSetting.multiInstanceSetting.sequential ?? false
+      configForm.value.approveRatio =
+        node.childProcessSetting.multiInstanceSetting.approveRatio ?? 100
+      configForm.value.multiInstanceSourceType =
+        node.childProcessSetting.multiInstanceSetting.sourceType ??
+        ChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY
+      configForm.value.multiInstanceSource =
+        node.childProcessSetting.multiInstanceSetting.source ?? ''
+    }
+  }
+  loadFormInfo()
+}
+
+defineExpose({ openDrawer, showChildProcessNodeConfig }) // 暴露方法给父组件
+
+const addVariable = (arr?: IOParameter[]) => {
+  arr?.push({
+    source: '',
+    target: ''
+  })
+}
+const deleteVariable = (index: number, arr?: IOParameter[]) => {
+  arr?.splice(index, 1)
+}
+const handleCalledElementChange = () => {
+  configForm.value.inVariables = []
+  configForm.value.outVariables = []
+  loadFormInfo()
+}
+const loadFormInfo = async () => {
+  const childInfo = childProcessOptions.value.find(
+    (option) => option.key === configForm.value.calledProcessDefinitionKey
+  )
+  const formInfo = await getForm(childInfo.formId)
+  childFormFieldOptions.value = []
+  if (formInfo.fields) {
+    formInfo.fields.forEach((fieldStr: string) => {
+      parseFormFields(JSON.parse(fieldStr), childFormFieldOptions.value)
+    })
+  }
+}
+const getIsoTimeDuration = () => {
+  let strTimeDuration = 'PT'
+  if (configForm.value.timeUnit === TimeUnitType.MINUTE) {
+    strTimeDuration += configForm.value.timeDuration + 'M'
+  }
+  if (configForm.value.timeUnit === TimeUnitType.HOUR) {
+    strTimeDuration += configForm.value.timeDuration + 'H'
+  }
+  if (configForm.value.timeUnit === TimeUnitType.DAY) {
+    strTimeDuration += configForm.value.timeDuration + 'D'
+  }
+  return strTimeDuration
+}
+const handleMultiInstanceSourceTypeChange = () => {
+  configForm.value.multiInstanceSource = ''
+}
+
+onMounted(async () => {
+  childProcessOptions.value = await getModelList(undefined)
+})
+</script>
+
+<style lang="scss" scoped></style>

+ 75 - 282
src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue

@@ -26,121 +26,11 @@
       </div>
     </template>
     <div>
-      <div class="mb-3 font-size-16px" v-if="currentNode.defaultFlow"
+      <div class="mb-3 font-size-16px" v-if="currentNode.conditionSetting?.defaultFlow"
         >未满足其它条件时,将进入此分支(该分支不可编辑和删除)</div
       >
       <div v-else>
-        <el-form ref="formRef" :model="currentNode" :rules="formRules" label-position="top">
-          <el-form-item label="配置方式" prop="conditionType">
-            <el-radio-group v-model="currentNode.conditionType" @change="changeConditionType">
-              <el-radio
-                v-for="(dict, index) in conditionConfigTypes"
-                :key="index"
-                :value="dict.value"
-                :label="dict.value"
-              >
-                {{ dict.label }}
-              </el-radio>
-            </el-radio-group>
-          </el-form-item>
-
-          <el-form-item
-            v-if="currentNode.conditionType === 1"
-            label="条件表达式"
-            prop="conditionExpression"
-          >
-            <el-input
-              type="textarea"
-              v-model="currentNode.conditionExpression"
-              clearable
-              style="width: 100%"
-            />
-          </el-form-item>
-          <el-form-item v-if="currentNode.conditionType === 2" label="条件规则">
-            <div class="condition-group-tool">
-              <div class="flex items-center">
-                <div class="mr-4">条件组关系</div>
-                <el-switch
-                  v-model="conditionGroups.and"
-                  inline-prompt
-                  active-text="且"
-                  inactive-text="或"
-                />
-              </div>
-            </div>
-            <el-space direction="vertical" :spacer="conditionGroups.and ? '且' : '或'">
-              <el-card
-                class="condition-group"
-                style="width: 530px"
-                v-for="(condition, cIdx) in conditionGroups.conditions"
-                :key="cIdx"
-              >
-                <div class="condition-group-delete" v-if="conditionGroups.conditions.length > 1">
-                  <Icon
-                    color="#0089ff"
-                    icon="ep:circle-close-filled"
-                    :size="18"
-                    @click="deleteConditionGroup(cIdx)"
-                  />
-                </div>
-                <template #header>
-                  <div class="flex items-center justify-between">
-                    <div>条件组</div>
-                    <div class="flex">
-                      <div class="mr-4">规则关系</div>
-                      <el-switch
-                        v-model="condition.and"
-                        inline-prompt
-                        active-text="且"
-                        inactive-text="或"
-                      />
-                    </div>
-                  </div>
-                </template>
-
-                <div class="flex pt-2" v-for="(rule, rIdx) in condition.rules" :key="rIdx">
-                  <div class="mr-2">
-                    <el-select style="width: 160px" v-model="rule.leftSide">
-                      <el-option
-                        v-for="(item, index) in fieldOptions"
-                        :key="index"
-                        :label="item.title"
-                        :value="item.field"
-                        :disabled="!item.required"
-                      />
-                    </el-select>
-                  </div>
-                  <div class="mr-2">
-                    <el-select v-model="rule.opCode" style="width: 100px">
-                      <el-option
-                        v-for="item in COMPARISON_OPERATORS"
-                        :key="item.value"
-                        :label="item.label"
-                        :value="item.value"
-                      />
-                    </el-select>
-                  </div>
-                  <div class="mr-2">
-                    <el-input v-model="rule.rightSide" style="width: 160px" />
-                  </div>
-                  <div class="mr-1 flex items-center" v-if="condition.rules.length > 1">
-                    <Icon
-                      icon="ep:delete"
-                      :size="18"
-                      @click="deleteConditionRule(condition, rIdx)"
-                    />
-                  </div>
-                  <div class="flex items-center">
-                    <Icon icon="ep:plus" :size="18" @click="addConditionRule(condition, rIdx)" />
-                  </div>
-                </div>
-              </el-card>
-            </el-space>
-            <div title="添加条件组" class="mt-4 cursor-pointer">
-              <Icon color="#0089ff" icon="ep:plus" :size="24" @click="addConditionGroup" />
-            </div>
-          </el-form-item>
-        </el-form>
+        <Condition ref="conditionRef" v-model="condition" />
       </div>
     </div>
     <template #footer>
@@ -153,35 +43,15 @@
   </el-drawer>
 </template>
 <script setup lang="ts">
-import {
-  SimpleFlowNode,
-  CONDITION_CONFIG_TYPES,
-  ConditionType,
-  COMPARISON_OPERATORS,
-  ConditionGroup,
-  Condition,
-  ConditionRule,
-  ProcessVariableEnum
-} from '../consts'
+import { SimpleFlowNode, ConditionType } from '../consts'
 import { getDefaultConditionNodeName } from '../utils'
-import { useFormFields } from '../node'
-import { BpmModelFormType } from '@/utils/constants'
-const message = useMessage() // 消息弹窗
+import { useFormFieldsAndStartUser, getConditionShowText } from '../node'
+import Condition from './components/Condition.vue'
+import { cloneDeep } from 'lodash-es'
+
 defineOptions({
   name: 'ConditionNodeConfig'
 })
-const formType = inject<Ref<number>>('formType') // 表单类型
-const conditionConfigTypes = computed(() => {
-  return CONDITION_CONFIG_TYPES.filter((item) => {
-    // 业务表单暂时去掉条件规则选项
-    if (formType?.value === BpmModelFormType.CUSTOM && item.value === ConditionType.RULE) {
-      return false
-    } else {
-      return true
-    }
-  })
-})
-
 const props = defineProps({
   conditionNode: {
     type: Object as () => SimpleFlowNode,
@@ -193,10 +63,50 @@ const props = defineProps({
   }
 })
 const settingVisible = ref(false)
+const currentNode = ref<SimpleFlowNode>(props.conditionNode)
+const condition = ref<any>({
+  conditionType: ConditionType.RULE, // 设置默认值
+  conditionExpression: '',
+  conditionGroups: {
+    and: true,
+    conditions: [
+      {
+        and: true,
+        rules: [
+          {
+            opCode: '==',
+            leftSide: '',
+            rightSide: ''
+          }
+        ]
+      }
+    ]
+  }
+})
 const open = () => {
-  if (currentNode.value.conditionType === ConditionType.RULE) {
-    if (currentNode.value.conditionGroups) {
-      conditionGroups.value = currentNode.value.conditionGroups
+  // 如果有已存在的配置则使用,否则使用默认值
+  if (currentNode.value.conditionSetting) {
+    condition.value = cloneDeep(currentNode.value.conditionSetting)
+  } else {
+    // 重置为默认值
+    condition.value = {
+      conditionType: ConditionType.RULE,
+      conditionExpression: '',
+      conditionGroups: {
+        and: true,
+        conditions: [
+          {
+            and: true,
+            rules: [
+              {
+                opCode: '==',
+                leftSide: '',
+                rightSide: ''
+              }
+            ]
+          }
+        ]
+      }
     }
   }
   settingVisible.value = true
@@ -219,11 +129,9 @@ const blurEvent = () => {
   showInput.value = false
   currentNode.value.name =
     currentNode.value.name ||
-    getDefaultConditionNodeName(props.nodeIndex, currentNode.value?.defaultFlow)
+    getDefaultConditionNodeName(props.nodeIndex, currentNode.value?.conditionSetting?.defaultFlow)
 }
 
-const currentNode = ref<SimpleFlowNode>(props.conditionNode)
-
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
 // 关闭
@@ -239,157 +147,42 @@ const handleClose = async (done: (cancel?: boolean) => void) => {
     done()
   }
 }
-// 表单校验规则
-const formRules = reactive({
-  conditionType: [{ required: true, message: '配置方式不能为空', trigger: 'blur' }],
-  conditionExpression: [{ required: true, message: '条件表达式不能为空', trigger: 'blur' }]
-})
-const formRef = ref() // 表单 Ref
 
-// 保存配置
+/** 保存配置 */
+const fieldOptions = useFormFieldsAndStartUser() // 流程表单字段和发起人字段
+const conditionRef = ref()
 const saveConfig = async () => {
-  if (!currentNode.value.defaultFlow) {
+  if (!currentNode.value.conditionSetting?.defaultFlow) {
     // 校验表单
-    if (!formRef) return false
-    const valid = await formRef.value.validate()
+    const valid = await conditionRef.value.validate()
     if (!valid) return false
-    const showText = getShowText()
+    const showText = getConditionShowText(
+      condition.value?.conditionType,
+      condition.value?.conditionExpression,
+      condition.value.conditionGroups,
+      fieldOptions
+    )
     if (!showText) {
       return false
     }
     currentNode.value.showText = showText
-    if (currentNode.value.conditionType === ConditionType.EXPRESSION) {
-      currentNode.value.conditionGroups = undefined
-    }
-    if (currentNode.value.conditionType === ConditionType.RULE) {
-      currentNode.value.conditionExpression = undefined
-      currentNode.value.conditionGroups = conditionGroups.value
-    }
+    // 使用 cloneDeep 进行深拷贝
+    currentNode.value.conditionSetting = cloneDeep({
+      ...currentNode.value.conditionSetting,
+      conditionType: condition.value?.conditionType,
+      conditionExpression:
+        condition.value?.conditionType === ConditionType.EXPRESSION
+          ? condition.value?.conditionExpression
+          : undefined,
+      conditionGroups:
+        condition.value?.conditionType === ConditionType.RULE
+          ? condition.value?.conditionGroups
+          : undefined
+    })
   }
   settingVisible.value = false
   return true
 }
-const getShowText = (): string => {
-  let showText = ''
-  if (currentNode.value.conditionType === ConditionType.EXPRESSION) {
-    if (currentNode.value.conditionExpression) {
-      showText = `表达式:${currentNode.value.conditionExpression}`
-    }
-  }
-  if (currentNode.value.conditionType === ConditionType.RULE) {
-    // 条件组是否为与关系
-    const groupAnd = conditionGroups.value.and
-    let warningMesg: undefined | string = undefined
-    const conditionGroup = conditionGroups.value.conditions.map((item) => {
-      return (
-        '(' +
-        item.rules
-          .map((rule) => {
-            if (rule.leftSide && rule.rightSide) {
-              return (
-                getFieldTitle(rule.leftSide) + ' ' + getOpName(rule.opCode) + ' ' + rule.rightSide
-              )
-            } else {
-              // 有一条规则不完善。提示错误
-              warningMesg = '请完善条件规则'
-              return ''
-            }
-          })
-          .join(item.and ? ' 且 ' : ' 或 ') +
-        ' ) '
-      )
-    })
-    if (warningMesg) {
-      message.warning(warningMesg)
-      showText = ''
-    } else {
-      showText = conditionGroup.join(groupAnd ? ' 且 ' : ' 或 ')
-    }
-  }
-  return showText
-}
-
-// 改变条件配置方式
-const changeConditionType = () => {}
-
-const conditionGroups = ref<ConditionGroup>({
-  and: true,
-  conditions: [
-    {
-      and: true,
-      rules: [
-        {
-          type: 1,
-          opName: '等于',
-          opCode: '==',
-          leftSide: '',
-          rightSide: ''
-        }
-      ]
-    }
-  ]
-})
-// 添加条件组
-const addConditionGroup = () => {
-  const condition = {
-    and: true,
-    rules: [
-      {
-        type: 1,
-        opName: '等于',
-        opCode: '==',
-        leftSide: '',
-        rightSide: ''
-      }
-    ]
-  }
-  conditionGroups.value.conditions.push(condition)
-}
-// 删除条件组
-const deleteConditionGroup = (idx: number) => {
-  conditionGroups.value.conditions.splice(idx, 1)
-}
-
-// 添加条件规则
-const addConditionRule = (condition: Condition, idx: number) => {
-  const rule: ConditionRule = {
-    type: 1,
-    opName: '等于',
-    opCode: '==',
-    leftSide: '',
-    rightSide: ''
-  }
-  condition.rules.splice(idx + 1, 0, rule)
-}
-
-const deleteConditionRule = (condition: Condition, idx: number) => {
-  condition.rules.splice(idx, 1)
-}
-const fieldsInfo = useFormFields()
-
-/** 条件规则可选择的表单字段 */
-const fieldOptions = computed(() => {
-  const fieldsCopy = fieldsInfo.slice()
-  // 固定添加发起人 ID 字段
-  fieldsCopy.unshift({
-    field: ProcessVariableEnum.START_USER_ID,
-    title: '发起人',
-    required: true
-  })
-  return fieldsCopy
-})
-
-/** 获取字段名称 */
-const getFieldTitle = (field: string) => {
-  const item = fieldOptions.value.find((item) => item.field === field)
-  return item?.title
-}
-
-/** 获取操作符名称 */
-const getOpName = (opCode: string): string => {
-  const opName = COMPARISON_OPERATORS.find((item: any) => item.value === opCode)
-  return opName?.label
-}
 </script>
 
 <style lang="scss" scoped>

+ 23 - 5
src/components/SimpleProcessDesignerV2/src/nodes-config/CopyTaskNodeConfig.vue

@@ -134,7 +134,7 @@
                   :key="idx"
                   :label="item.title"
                   :value="item.field"
-                  :disabled ="!item.required"
+                  :disabled="!item.required"
                 />
               </el-select>
             </el-form-item>
@@ -149,7 +149,7 @@
                   :key="idx"
                   :label="item.title"
                   :value="item.field"
-                  :disabled ="!item.required"
+                  :disabled="!item.required"
                 />
               </el-select>
             </el-form-item>
@@ -195,9 +195,15 @@
           <div class="field-permit-title">
             <div class="setting-title-label first-title"> 字段名称 </div>
             <div class="other-titles">
-              <span class="setting-title-label">只读</span>
-              <span class="setting-title-label">可编辑</span>
-              <span class="setting-title-label">隐藏</span>
+              <span class="setting-title-label cursor-pointer" @click="updatePermission('READ')">
+                只读
+              </span>
+              <span class="setting-title-label cursor-pointer" @click="updatePermission('WRITE')">
+                可编辑
+              </span>
+              <span class="setting-title-label cursor-pointer" @click="updatePermission('NONE')">
+                隐藏
+              </span>
             </div>
           </div>
           <div
@@ -368,6 +374,18 @@ const showCopyTaskNodeConfig = (node: SimpleFlowNode) => {
   getNodeConfigFormFields(node.fieldsPermission)
 }
 
+/** 批量更新权限 */
+const updatePermission = (type: string) => {
+  fieldsPermissionConfig.value.forEach((field) => {
+    field.permission =
+      type === 'READ'
+        ? FieldPermissionType.READ
+        : type === 'WRITE'
+          ? FieldPermissionType.WRITE
+          : FieldPermissionType.NONE
+  })
+}
+
 defineExpose({ openDrawer, showCopyTaskNodeConfig }) // 暴露方法给父组件
 </script>
 

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

@@ -124,6 +124,7 @@ const saveConfig = async () => {
   if (!valid) return false
   const showText = getShowText()
   if (!showText) return false
+  currentNode.value.name = nodeName.value!
   currentNode.value.showText = showText
   if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
     currentNode.value.delaySetting = {

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

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

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

@@ -36,7 +36,8 @@
             placement="top"
             :content="getUserNicknames(startUserIds)"
           >
-            {{ getUserNicknames(startUserIds.slice(0,2)) }} 等 {{ startUserIds.length }} 人可发起流程
+            {{ getUserNicknames(startUserIds.slice(0, 2)) }} 等
+            {{ startUserIds.length }} 人可发起流程
           </el-tooltip>
         </el-text>
       </el-tab-pane>
@@ -46,9 +47,15 @@
           <div class="field-permit-title">
             <div class="setting-title-label first-title"> 字段名称 </div>
             <div class="other-titles">
-              <span class="setting-title-label">只读</span>
-              <span class="setting-title-label">可编辑</span>
-              <span class="setting-title-label">隐藏</span>
+              <span class="setting-title-label cursor-pointer" @click="updatePermission('READ')">
+                只读
+              </span>
+              <span class="setting-title-label cursor-pointer" @click="updatePermission('WRITE')">
+                可编辑
+              </span>
+              <span class="setting-title-label cursor-pointer" @click="updatePermission('NONE')">
+                隐藏
+              </span>
             </div>
           </div>
           <div
@@ -157,6 +164,17 @@ const showStartUserNodeConfig = (node: SimpleFlowNode) => {
   getNodeConfigFormFields(node.fieldsPermission)
 }
 
+/** 批量更新权限 */
+const updatePermission = (type: string) => {
+  fieldsPermissionConfig.value.forEach((field) => {
+    field.permission =
+      type === 'READ'
+        ? FieldPermissionType.READ
+        : type === 'WRITE'
+          ? FieldPermissionType.WRITE
+          : FieldPermissionType.NONE
+  })
+}
 defineExpose({ openDrawer, showStartUserNodeConfig }) // 暴露方法给父组件
 </script>
 

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

@@ -0,0 +1,524 @@
+<template>
+  <el-drawer
+    :append-to-body="true"
+    v-model="settingVisible"
+    :show-close="false"
+    :size="630"
+    :before-close="saveConfig"
+  >
+    <template #header>
+      <div class="config-header">
+        <input
+          v-if="showInput"
+          type="text"
+          class="config-editable-input"
+          @blur="blurEvent()"
+          v-mountedFocus
+          v-model="nodeName"
+          :placeholder="nodeName"
+        />
+        <div v-else class="node-name">
+          {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
+        </div>
+        <div class="divide-line"></div>
+      </div>
+    </template>
+    <div>
+      <el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
+        <el-form-item label="触发器类型" prop="type">
+          <el-select v-model="configForm.type" @change="changeTriggerType">
+            <el-option
+              v-for="(item, index) in TRIGGER_TYPES"
+              :key="index"
+              :value="item.value"
+              :label="item.label"
+            />
+          </el-select>
+        </el-form-item>
+        <!-- HTTP 请求触发器 -->
+        <div
+          v-if="
+            [TriggerTypeEnum.HTTP_REQUEST, TriggerTypeEnum.HTTP_CALLBACK].includes(
+              configForm.type
+            ) && configForm.httpRequestSetting
+          "
+        >
+          <HttpRequestSetting
+            v-model:setting="configForm.httpRequestSetting"
+            :responseEnable="configForm.type === TriggerTypeEnum.HTTP_REQUEST"
+            :formItemPrefix="'httpRequestSetting'"
+          />
+        </div>
+
+        <!-- 表单数据修改触发器 -->
+        <div v-if="configForm.type === TriggerTypeEnum.FORM_UPDATE">
+          <div v-for="(formSetting, index) in configForm.formSettings" :key="index">
+            <el-card class="w-580px mt-4">
+              <template #header>
+                <div class="flex items-center justify-between">
+                  <div>修改表单设置 {{ index + 1 }}</div>
+                  <el-button
+                    type="primary"
+                    plain
+                    circle
+                    v-if="configForm.formSettings!.length > 1"
+                    @click="deleteFormSetting(index)"
+                  >
+                    <Icon icon="ep:close" />
+                  </el-button>
+                </div>
+              </template>
+
+              <!-- 条件设置 -->
+              <ConditionDialog
+                :ref="`condition-${index}`"
+                @update-condition="(val) => handleConditionUpdate(index, val)"
+              />
+              <div class="cursor-pointer" v-if="formSetting.conditionType">
+                <el-tag
+                  type="success"
+                  effect="light"
+                  closable
+                  @close="deleteFormSettingCondition(formSetting)"
+                  @click="openFormSettingCondition(index, formSetting)"
+                >
+                  {{ showConditionText(formSetting) }}
+                </el-tag>
+              </div>
+              <el-button
+                v-else
+                type="primary"
+                text
+                @click="addFormSettingCondition(index, formSetting)"
+              >
+                <Icon icon="ep:link" class="mr-5px" />添加条件
+              </el-button>
+              <el-divider content-position="left">修改表单字段设置</el-divider>
+              <!-- 表单字段修改设置 -->
+              <div
+                class="flex items-center"
+                v-for="key in Object.keys(formSetting.updateFormFields || {})"
+                :key="key"
+              >
+                <div class="mr-2 flex items-center">
+                  <el-form-item>
+                    <el-select
+                      class="w-160px!"
+                      :model-value="key"
+                      @update:model-value="(newKey) => updateFormFieldKey(formSetting, key, newKey)"
+                      placeholder="请选择表单字段"
+                      :disabled="key !== ''"
+                    >
+                      <el-option
+                        v-for="(field, fIdx) in optionalUpdateFormFields"
+                        :key="fIdx"
+                        :label="field.title"
+                        :value="field.field"
+                        :disabled="field.disabled"
+                      />
+                    </el-select>
+                  </el-form-item>
+                </div>
+                <div class="mx-2"><el-form-item>的值设置为</el-form-item></div>
+                <div class="mr-2">
+                  <el-form-item
+                    :prop="`formSettings.${index}.updateFormFields.${key}`"
+                    :rules="{
+                      required: true,
+                      message: '值不能为空',
+                      trigger: 'blur'
+                    }"
+                  >
+                    <el-input
+                      class="w-160px"
+                      v-model="formSetting.updateFormFields![key]"
+                      placeholder="请输入"
+                      :disabled="!key"
+                    />
+                  </el-form-item>
+                </div>
+                <div class="mr-1 pt-1 cursor-pointer">
+                  <el-form-item>
+                    <Icon
+                      icon="ep:delete"
+                      :size="18"
+                      @click="deleteFormFieldSetting(formSetting, key)"
+                    />
+                  </el-form-item>
+                </div>
+              </div>
+
+              <!-- 添加表单字段按钮 -->
+              <el-button type="primary" text @click="addFormFieldSetting(formSetting)">
+                <Icon icon="ep:memo" class="mr-5px" />添加修改字段
+              </el-button>
+            </el-card>
+          </div>
+
+          <!-- 添加新的设置 -->
+          <el-button class="mt-6" type="primary" text @click="addFormSetting">
+            <Icon icon="ep:setting" class="mr-5px" />添加设置
+          </el-button>
+        </div>
+
+        <!-- 表单数据删除触发器 -->
+        <div v-if="configForm.type === TriggerTypeEnum.FORM_DELETE">
+          <div v-for="(formSetting, index) in configForm.formSettings" :key="index">
+            <el-card class="w-580px mt-4">
+              <template #header>
+                <div class="flex items-center justify-between">
+                  <div>删除表单设置 {{ index + 1 }}</div>
+                  <el-button
+                    type="primary"
+                    plain
+                    circle
+                    v-if="configForm.formSettings!.length > 1"
+                    @click="deleteFormSetting(index)"
+                  >
+                    <Icon icon="ep:close" />
+                  </el-button>
+                </div>
+              </template>
+
+              <!-- 条件设置 -->
+              <ConditionDialog
+                :ref="`condition-${index}`"
+                @update-condition="(val) => handleConditionUpdate(index, val)"
+              />
+              <div class="cursor-pointer" v-if="formSetting.conditionType">
+                <el-tag
+                  type="warning"
+                  effect="light"
+                  closable
+                  @close="deleteFormSettingCondition(formSetting)"
+                  @click="openFormSettingCondition(index, formSetting)"
+                >
+                  {{ showConditionText(formSetting) }}
+                </el-tag>
+              </div>
+              <el-button
+                v-else
+                type="primary"
+                text
+                @click="addFormSettingCondition(index, formSetting)"
+              >
+                <Icon icon="ep:link" class="mr-5px" />添加条件
+              </el-button>
+
+              <el-divider content-position="left">删除表单字段设置</el-divider>
+              <!-- 表单字段删除设置 -->
+              <div class="flex flex-wrap gap-2">
+                <el-select
+                  v-model="formSetting.deleteFields"
+                  multiple
+                  placeholder="请选择要删除的字段"
+                  class="w-full"
+                >
+                  <el-option
+                    v-for="field in formFields"
+                    :key="field.field"
+                    :label="field.title"
+                    :value="field.field"
+                  />
+                </el-select>
+              </div>
+            </el-card>
+          </div>
+
+          <!-- 添加新的设置 -->
+          <el-button class="mt-6" type="primary" text @click="addFormSetting">
+            <Icon icon="ep:setting" class="mr-5px" />添加设置
+          </el-button>
+        </div>
+      </el-form>
+    </div>
+    <template #footer>
+      <el-divider />
+      <div>
+        <el-button type="primary" @click="saveConfig">确 定</el-button>
+        <el-button @click="closeDrawer">取 消</el-button>
+      </div>
+    </template>
+  </el-drawer>
+</template>
+<script setup lang="ts">
+import {
+  SimpleFlowNode,
+  NodeType,
+  TriggerSetting,
+  TRIGGER_TYPES,
+  TriggerTypeEnum,
+  FormTriggerSetting,
+  DEFAULT_CONDITION_GROUP_VALUE
+} from '../consts'
+import { useWatchNode, useDrawer, useNodeName, useFormFields, getConditionShowText } from '../node'
+import HttpRequestSetting from './components/HttpRequestSetting.vue'
+import ConditionDialog from './components/ConditionDialog.vue'
+const { proxy } = getCurrentInstance() as any
+
+defineOptions({
+  name: 'TriggerNodeConfig'
+})
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  }
+})
+const message = useMessage() // 消息弹窗
+// 抽屉配置
+const { settingVisible, closeDrawer, openDrawer } = useDrawer()
+// 当前节点
+const currentNode = useWatchNode(props)
+// 节点名称
+const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.TRIGGER_NODE)
+// 触发器表单配置
+const formRef = ref() // 表单 Ref
+// 表单校验规则
+const formRules = reactive({
+  type: [{ required: true, message: '触发器类型不能为空', trigger: 'change' }],
+  'httpRequestSetting.url': [{ required: true, message: '请求地址不能为空', trigger: 'blur' }]
+})
+// 触发器配置表单数据
+const configForm = ref<TriggerSetting>({
+  type: TriggerTypeEnum.HTTP_REQUEST,
+  httpRequestSetting: {
+    url: '',
+    header: [],
+    body: [],
+    response: []
+  },
+  formSettings: [
+    {
+      conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+      updateFormFields: {},
+      deleteFields: []
+    }
+  ]
+})
+// 流程表单字段
+const formFields = useFormFields()
+
+// 可选的修改的表单字段
+const optionalUpdateFormFields = computed(() => {
+  return formFields.map((field) => ({
+    title: field.title,
+    field: field.field,
+    disabled: false
+  }))
+})
+
+let originalSetting: TriggerSetting | undefined
+
+/** 触发器类型改变了 */
+const changeTriggerType = () => {
+  if (configForm.value.type === TriggerTypeEnum.HTTP_REQUEST) {
+    configForm.value.httpRequestSetting =
+      originalSetting?.type === TriggerTypeEnum.HTTP_REQUEST && originalSetting.httpRequestSetting
+        ? originalSetting.httpRequestSetting
+        : {
+            url: '',
+            header: [],
+            body: [],
+            response: []
+          }
+    configForm.value.formSettings = undefined
+    return
+  }
+
+  if (configForm.value.type === TriggerTypeEnum.HTTP_CALLBACK) {
+    configForm.value.httpRequestSetting =
+      originalSetting?.type === TriggerTypeEnum.HTTP_CALLBACK && originalSetting.httpRequestSetting
+        ? originalSetting.httpRequestSetting
+        : {
+            url: '',
+            header: [],
+            body: [],
+            response: []
+          }
+    configForm.value.formSettings = undefined
+    return
+  }
+
+  if (configForm.value.type === TriggerTypeEnum.FORM_UPDATE) {
+    configForm.value.formSettings =
+      originalSetting?.type === TriggerTypeEnum.FORM_UPDATE && originalSetting.formSettings
+        ? originalSetting.formSettings
+        : [
+            {
+              conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+              updateFormFields: {},
+              deleteFields: []
+            }
+          ]
+    configForm.value.httpRequestSetting = undefined
+    return
+  }
+
+  if (configForm.value.type === TriggerTypeEnum.FORM_DELETE) {
+    configForm.value.formSettings =
+      originalSetting?.type === TriggerTypeEnum.FORM_DELETE && originalSetting.formSettings
+        ? originalSetting.formSettings
+        : [
+            {
+              conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+              updateFormFields: undefined,
+              deleteFields: []
+            }
+          ]
+    configForm.value.httpRequestSetting = undefined
+    return
+  }
+}
+
+/** 添加新的修改表单设置 */
+const addFormSetting = () => {
+  configForm.value.formSettings!.push({
+    conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+    updateFormFields: {},
+    deleteFields: []
+  })
+}
+
+/** 删除修改表单设置 */
+const deleteFormSetting = (index: number) => {
+  configForm.value.formSettings!.splice(index, 1)
+}
+
+/** 添加条件配置 */
+const addFormSettingCondition = (index: number, formSetting: FormTriggerSetting) => {
+  const conditionDialog = proxy.$refs[`condition-${index}`][0]
+  conditionDialog.open(formSetting)
+}
+/** 删除条件配置 */
+const deleteFormSettingCondition = (formSetting: FormTriggerSetting) => {
+  formSetting.conditionType = undefined
+}
+/** 打开条件配置弹窗 */
+const openFormSettingCondition = (index: number, formSetting: FormTriggerSetting) => {
+  const conditionDialog = proxy.$refs[`condition-${index}`][0]
+  conditionDialog.open(formSetting)
+}
+/** 处理条件配置保存 */
+const handleConditionUpdate = (index: number, condition: any) => {
+  configForm.value.formSettings![index].conditionType = condition.conditionType
+  configForm.value.formSettings![index].conditionExpression = condition.conditionExpression
+  configForm.value.formSettings![index].conditionGroups = condition.conditionGroups
+}
+/** 条件配置展示 */
+const showConditionText = (formSetting: FormTriggerSetting) => {
+  return getConditionShowText(
+    formSetting.conditionType,
+    formSetting.conditionExpression,
+    formSetting.conditionGroups,
+    formFields
+  )
+}
+
+/** 添加修改字段设置项 */
+const addFormFieldSetting = (formSetting: FormTriggerSetting) => {
+  if (!formSetting) return
+  if (!formSetting.updateFormFields) {
+    formSetting.updateFormFields = {}
+  }
+  formSetting.updateFormFields[''] = undefined
+}
+/** 更新字段 KEY */
+const updateFormFieldKey = (formSetting: FormTriggerSetting, oldKey: string, newKey: string) => {
+  if (!formSetting?.updateFormFields) return
+  const value = formSetting.updateFormFields[oldKey]
+  delete formSetting.updateFormFields[oldKey]
+  formSetting.updateFormFields[newKey] = value
+}
+
+/** 删除修改字段设置项 */
+const deleteFormFieldSetting = (formSetting: FormTriggerSetting, key: string) => {
+  if (!formSetting?.updateFormFields) return
+  delete formSetting.updateFormFields[key]
+}
+
+/** 保存配置 */
+const saveConfig = async () => {
+  if (!formRef) return false
+  const valid = await formRef.value.validate()
+  if (!valid) return false
+  const showText = getShowText()
+  if (!showText) return false
+  currentNode.value.name = nodeName.value!
+  currentNode.value.showText = showText
+  if (configForm.value.type === TriggerTypeEnum.HTTP_REQUEST) {
+    configForm.value.formSettings = undefined
+  } else if (configForm.value.type === TriggerTypeEnum.FORM_UPDATE) {
+    configForm.value.httpRequestSetting = undefined
+    // 清理删除字段相关的数据
+    configForm.value.formSettings?.forEach((setting) => {
+      setting.deleteFields = undefined
+    })
+  } else if (configForm.value.type === TriggerTypeEnum.FORM_DELETE) {
+    configForm.value.httpRequestSetting = undefined
+    // 清理修改字段相关的数据
+    configForm.value.formSettings?.forEach((setting) => {
+      setting.updateFormFields = undefined
+    })
+  }
+  currentNode.value.triggerSetting = configForm.value
+  settingVisible.value = false
+  return true
+}
+
+/** 获取节点展示内容 */
+const getShowText = (): string => {
+  let showText = ''
+  if (
+    configForm.value.type === TriggerTypeEnum.HTTP_REQUEST ||
+    configForm.value.type === TriggerTypeEnum.HTTP_CALLBACK
+  ) {
+    showText = `${configForm.value.httpRequestSetting?.url}`
+  } else if (configForm.value.type === TriggerTypeEnum.FORM_UPDATE) {
+    for (const [index, setting] of configForm.value.formSettings!.entries()) {
+      if (!setting.updateFormFields || Object.keys(setting.updateFormFields).length === 0) {
+        message.warning(`请添加表单设置${index + 1}的修改字段`)
+        return ''
+      }
+    }
+    showText = '修改表单数据'
+  } else if (configForm.value.type === TriggerTypeEnum.FORM_DELETE) {
+    for (const [index, setting] of configForm.value.formSettings!.entries()) {
+      if (!setting.deleteFields || setting.deleteFields.length === 0) {
+        message.warning(`请选择表单设置${index + 1}要删除的字段`)
+        return ''
+      }
+    }
+    showText = '删除表单数据'
+  }
+  return showText
+}
+
+/** 显示触发器节点配置, 由父组件传过来 */
+const showTriggerNodeConfig = (node: SimpleFlowNode) => {
+  nodeName.value = node.name
+  originalSetting = node.triggerSetting ? JSON.parse(JSON.stringify(node.triggerSetting)) : {}
+  if (node.triggerSetting) {
+    configForm.value = {
+      type: node.triggerSetting.type,
+      httpRequestSetting: node.triggerSetting.httpRequestSetting || {
+        url: '',
+        header: [],
+        body: [],
+        response: []
+      },
+      formSettings: node.triggerSetting.formSettings || [
+        {
+          conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+          updateFormFields: {},
+          deleteFields: []
+        }
+      ]
+    }
+  }
+}
+
+defineExpose({ openDrawer, showTriggerNodeConfig }) // 暴露方法给父组件
+</script>
+
+<style lang="scss" scoped></style>

+ 274 - 129
src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue

@@ -3,7 +3,7 @@
     :append-to-body="true"
     v-model="settingVisible"
     :show-close="false"
-    :size="550"
+    :size="580"
     :before-close="saveConfig"
     class="justify-start"
   >
@@ -19,12 +19,13 @@
           :placeholder="nodeName"
         />
         <div v-else class="node-name">
-          {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
+          {{ nodeName }}
+          <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
         </div>
         <div class="divide-line"></div>
       </div>
     </template>
-    <div class="flex flex-items-center mb-3">
+    <div v-if="currentNode.type === NodeType.USER_TASK_NODE" class="flex flex-items-center mb-3">
       <span class="font-size-16px mr-3">审批类型 :</span>
       <el-radio-group v-model="approveType">
         <el-radio
@@ -38,22 +39,21 @@
       </el-radio-group>
     </div>
     <el-tabs type="border-card" v-model="activeTabName" v-if="approveType === ApproveType.USER">
-      <el-tab-pane label="审批人" name="user">
+      <el-tab-pane :label="`${nodeTypeName}人`" name="user">
         <div>
           <el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
-            <el-form-item label="审批人设置" prop="candidateStrategy">
+            <el-form-item :label="`${nodeTypeName}人设置`" prop="candidateStrategy">
               <el-radio-group
                 v-model="configForm.candidateStrategy"
                 @change="changeCandidateStrategy"
               >
-                <el-radio
-                  v-for="(dict, index) in CANDIDATE_STRATEGY"
-                  :key="index"
-                  :value="dict.value"
-                  :label="dict.value"
-                >
-                  {{ dict.label }}
-                </el-radio>
+                <el-row>
+                  <el-col v-for="(dict, index) in CANDIDATE_STRATEGY" :key="index" :span="8">
+                    <el-radio :value="dict.value" :label="dict.value">
+                      {{ dict.label }}
+                    </el-radio>
+                  </el-col>
+                </el-row>
               </el-radio-group>
             </el-form-item>
             <el-form-item
@@ -61,7 +61,13 @@
               label="指定角色"
               prop="roleIds"
             >
-              <el-select v-model="configForm.roleIds" clearable multiple style="width: 100%">
+              <el-select
+                filterable
+                v-model="configForm.roleIds"
+                clearable
+                multiple
+                style="width: 100%"
+              >
                 <el-option
                   v-for="item in roleOptions"
                   :key="item.id"
@@ -99,7 +105,13 @@
               prop="postIds"
               span="24"
             >
-              <el-select v-model="configForm.postIds" clearable multiple style="width: 100%">
+              <el-select
+                filterable
+                v-model="configForm.postIds"
+                clearable
+                multiple
+                style="width: 100%"
+              >
                 <el-option
                   v-for="item in postOptions"
                   :key="item.id"
@@ -114,7 +126,13 @@
               prop="userIds"
               span="24"
             >
-              <el-select v-model="configForm.userIds" clearable multiple style="width: 100%">
+              <el-select
+                filterable
+                v-model="configForm.userIds"
+                clearable
+                multiple
+                style="width: 100%"
+              >
                 <el-option
                   v-for="item in userOptions"
                   :key="item.id"
@@ -128,7 +146,13 @@
               label="指定用户组"
               prop="userGroups"
             >
-              <el-select v-model="configForm.userGroups" clearable multiple style="width: 100%">
+              <el-select
+                filterable
+                v-model="configForm.userGroups"
+                clearable
+                multiple
+                style="width: 100%"
+              >
                 <el-option
                   v-for="item in userGroupOptions"
                   :key="item.id"
@@ -142,13 +166,13 @@
               label="表单内用户字段"
               prop="formUser"
             >
-              <el-select v-model="configForm.formUser" clearable style="width: 100%">
+              <el-select filterable 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"
+                  :disabled="!item.required"
                 />
               </el-select>
             </el-form-item>
@@ -157,13 +181,13 @@
               label="表单内部门字段"
               prop="formDept"
             >
-              <el-select v-model="configForm.formDept" clearable style="width: 100%">
+              <el-select filterable 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"
+                  :disabled="!item.required"
                 />
               </el-select>
             </el-form-item>
@@ -179,7 +203,7 @@
               prop="deptLevel"
               span="24"
             >
-              <el-select v-model="configForm.deptLevel" clearable>
+              <el-select filterable v-model="configForm.deptLevel" clearable>
                 <el-option
                   v-for="(item, index) in MULTI_LEVEL_DEPT"
                   :key="index"
@@ -201,7 +225,7 @@
                 style="width: 100%"
               />
             </el-form-item>
-            <el-form-item label="多人审批方式" prop="approveMethod">
+            <el-form-item :label="`多人${nodeTypeName}方式`" prop="approveMethod">
               <el-radio-group v-model="configForm.approveMethod" @change="approveMethodChanged">
                 <div class="flex-col">
                   <div
@@ -230,92 +254,102 @@
               </el-radio-group>
             </el-form-item>
 
-            <el-divider content-position="left">审批人拒绝时</el-divider>
-            <el-form-item prop="rejectHandlerType">
-              <el-radio-group v-model="configForm.rejectHandlerType">
-                <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 v-if="currentNode.type === NodeType.USER_TASK_NODE">
+              <el-divider content-position="left">审批人拒绝时</el-divider>
+              <el-form-item prop="rejectHandlerType">
+                <el-radio-group v-model="configForm.rejectHandlerType">
+                  <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>
-                </div>
-              </el-radio-group>
-            </el-form-item>
-            <el-form-item
-              v-if="configForm.rejectHandlerType == RejectHandlerType.RETURN_USER_TASK"
-              label="驳回节点"
-              prop="returnNodeId"
-            >
-              <el-select v-model="configForm.returnNodeId" clearable style="width: 100%">
-                <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 label="启用开关" prop="timeoutHandlerEnable">
-              <el-switch
-                v-model="configForm.timeoutHandlerEnable"
-                active-text="开启"
-                inactive-text="关闭"
-                @change="timeoutHandlerChange"
-              />
-            </el-form-item>
-            <el-form-item
-              label="执行动作"
-              prop="timeoutHandlerType"
-              v-if="configForm.timeoutHandlerEnable"
-            >
-              <el-radio-group
-                v-model="configForm.timeoutHandlerType"
-                @change="timeoutHandlerTypeChanged"
+                </el-radio-group>
+              </el-form-item>
+              <el-form-item
+                v-if="configForm.rejectHandlerType == RejectHandlerType.RETURN_USER_TASK"
+                label="驳回节点"
+                prop="returnNodeId"
               >
-                <el-radio-button
-                  v-for="item in TIMEOUT_HANDLER_TYPES"
-                  :key="item.value"
-                  :value="item.value"
-                  :label="item.label"
+                <el-select
+                  filterable
+                  v-model="configForm.returnNodeId"
+                  clearable
+                  style="width: 100%"
+                >
+                  <el-option
+                    v-for="item in returnTaskList"
+                    :key="item.id"
+                    :label="item.name"
+                    :value="item.id"
+                  />
+                </el-select>
+              </el-form-item>
+            </div>
+
+            <div v-if="currentNode.type === NodeType.USER_TASK_NODE">
+              <el-divider content-position="left">审批人超时未处理时</el-divider>
+              <el-form-item label="启用开关" prop="timeoutHandlerEnable">
+                <el-switch
+                  v-model="configForm.timeoutHandlerEnable"
+                  active-text="开启"
+                  inactive-text="关闭"
+                  @change="timeoutHandlerChange"
                 />
-              </el-radio-group>
-            </el-form-item>
-            <el-form-item label="超时时间设置" v-if="configForm.timeoutHandlerEnable">
-              <span class="mr-2">当超过</span>
-              <el-form-item prop="timeDuration">
-                <el-input-number
+              </el-form-item>
+              <el-form-item
+                label="执行动作"
+                prop="timeoutHandlerType"
+                v-if="configForm.timeoutHandlerEnable"
+              >
+                <el-radio-group
+                  v-model="configForm.timeoutHandlerType"
+                  @change="timeoutHandlerTypeChanged"
+                >
+                  <el-radio-button
+                    v-for="item in TIMEOUT_HANDLER_TYPES"
+                    :key="item.value"
+                    :value="item.value"
+                    :label="item.label"
+                  />
+                </el-radio-group>
+              </el-form-item>
+              <el-form-item label="超时时间设置" v-if="configForm.timeoutHandlerEnable">
+                <span class="mr-2">当超过</span>
+                <el-form-item prop="timeDuration">
+                  <el-input-number
+                    class="mr-2"
+                    :style="{ width: '100px' }"
+                    v-model="configForm.timeDuration"
+                    :min="1"
+                    controls-position="right"
+                  />
+                </el-form-item>
+                <el-select
+                  filterable
+                  v-model="timeUnit"
                   class="mr-2"
                   :style="{ width: '100px' }"
-                  v-model="configForm.timeDuration"
-                  :min="1"
-                  controls-position="right"
-                />
+                  @change="timeUnitChange"
+                >
+                  <el-option
+                    v-for="item in TIME_UNIT_TYPES"
+                    :key="item.value"
+                    :label="item.label"
+                    :value="item.value"
+                  />
+                </el-select>
+                未处理
               </el-form-item>
-              <el-select
-                v-model="timeUnit"
-                class="mr-2"
-                :style="{ width: '100px' }"
-                @change="timeUnitChange"
+              <el-form-item
+                label="最大提醒次数"
+                prop="maxRemindCount"
+                v-if="configForm.timeoutHandlerEnable && configForm.timeoutHandlerType === 1"
               >
-                <el-option
-                  v-for="item in TIME_UNIT_TYPES"
-                  :key="item.value"
-                  :label="item.label"
-                  :value="item.value"
-                />
-              </el-select>
-              未处理
-            </el-form-item>
-            <el-form-item
-              label="最大提醒次数"
-              prop="maxRemindCount"
-              v-if="configForm.timeoutHandlerEnable && configForm.timeoutHandlerType === 1"
-            >
-              <el-input-number v-model="configForm.maxRemindCount" :min="1" :max="10" />
-            </el-form-item>
+                <el-input-number v-model="configForm.maxRemindCount" :min="1" :max="10" />
+              </el-form-item>
+            </div>
 
-            <el-divider content-position="left">审批人为空时</el-divider>
+            <el-divider content-position="left">{{ nodeTypeName }}人为空时</el-divider>
             <el-form-item prop="assignEmptyHandlerType">
               <el-radio-group v-model="configForm.assignEmptyHandlerType">
                 <div class="flex-col">
@@ -332,6 +366,7 @@
               span="24"
             >
               <el-select
+                filterable
                 v-model="configForm.assignEmptyHandlerUserIds"
                 clearable
                 multiple
@@ -346,20 +381,44 @@
               </el-select>
             </el-form-item>
 
-            <el-divider content-position="left">审批人与提交人为同一人时</el-divider>
-            <el-form-item prop="assignStartUserHandlerType">
-              <el-radio-group v-model="configForm.assignStartUserHandlerType">
-                <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 v-if="currentNode.type === NodeType.USER_TASK_NODE">
+              <el-divider content-position="left">审批人与提交人为同一人时</el-divider>
+              <el-form-item prop="assignStartUserHandlerType">
+                <el-radio-group v-model="configForm.assignStartUserHandlerType">
+                  <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>
-                </div>
-              </el-radio-group>
-            </el-form-item>
+                </el-radio-group>
+              </el-form-item>
+            </div>
+
+            <div v-if="currentNode.type === NodeType.USER_TASK_NODE">
+              <el-divider content-position="left">是否需要签名</el-divider>
+              <el-form-item prop="signEnable">
+                <el-switch v-model="configForm.signEnable" active-text="是" inactive-text="否" />
+              </el-form-item>
+            </div>
+
+            <div v-if="currentNode.type === NodeType.USER_TASK_NODE">
+              <el-divider content-position="left">审批意见</el-divider>
+              <el-form-item prop="reasonRequire">
+                <el-switch
+                  v-model="configForm.reasonRequire"
+                  active-text="必填"
+                  inactive-text="非必填"
+                />
+              </el-form-item>
+            </div>
           </el-form>
         </div>
       </el-tab-pane>
-      <el-tab-pane label="操作按钮设置" name="buttons">
+      <el-tab-pane
+        label="操作按钮设置"
+        v-if="currentNode.type === NodeType.USER_TASK_NODE"
+        name="buttons"
+      >
         <div class="button-setting-pane">
           <div class="button-setting-desc">操作按钮</div>
           <div class="button-setting-title">
@@ -395,9 +454,15 @@
           <div class="field-permit-title">
             <div class="setting-title-label first-title"> 字段名称 </div>
             <div class="other-titles">
-              <span class="setting-title-label">只读</span>
-              <span class="setting-title-label">可编辑</span>
-              <span class="setting-title-label">隐藏</span>
+              <span class="setting-title-label cursor-pointer" @click="updatePermission('READ')">
+                只读
+              </span>
+              <span class="setting-title-label cursor-pointer" @click="updatePermission('WRITE')">
+                可编辑
+              </span>
+              <span class="setting-title-label cursor-pointer" @click="updatePermission('NONE')">
+                隐藏
+              </span>
             </div>
           </div>
           <div
@@ -435,6 +500,13 @@
           </div>
         </div>
       </el-tab-pane>
+      <el-tab-pane label="监听器" name="listener">
+        <UserTaskListener
+          ref="userTaskListenerRef"
+          v-model="configForm"
+          :form-field-options="formFieldOptions"
+        />
+      </el-tab-pane>
     </el-tabs>
     <template #footer>
       <el-divider />
@@ -470,7 +542,8 @@ import {
   ASSIGN_EMPTY_HANDLER_TYPES,
   AssignEmptyHandlerType,
   FieldPermissionType,
-  ProcessVariableEnum
+  ProcessVariableEnum,
+  TRANSACTOR_DEFAULT_BUTTON_SETTING
 } from '../consts'
 
 import {
@@ -484,6 +557,7 @@ import {
 import { defaultProps } from '@/utils/tree'
 import { cloneDeep } from 'lodash-es'
 import { convertTimeUnit, getApproveTypeText } from '../utils'
+import UserTaskListener from './components/UserTaskListener.vue'
 defineOptions({
   name: 'UserTaskNodeConfig'
 })
@@ -572,7 +646,7 @@ const {
   handleCandidateParam,
   parseCandidateParam,
   getShowText
-} = useNodeForm(NodeType.USER_TASK_NODE)
+} = useNodeForm(currentNode.value.type)
 const configForm = tempConfigForm as Ref<UserTaskFormType>
 
 // 改变审批人设置策略
@@ -609,9 +683,16 @@ const {
   cTimeoutMaxRemindCount
 } = useTimeoutHandler()
 
-// 保存配置
+const userTaskListenerRef = ref()
+
+/** 节点类型名称 */
+const nodeTypeName = computed(() => {
+  return currentNode.value.type === NodeType.TRANSACTOR_NODE ? '办理' : '审批'
+})
+
+/** 保存配置 */
 const saveConfig = async () => {
-  activeTabName.value = 'user'
+  // activeTabName.value = 'user'
   // 设置审批节点名称
   currentNode.value.name = nodeName.value!
   // 设置审批类型
@@ -624,7 +705,8 @@ const saveConfig = async () => {
   }
 
   if (!formRef) return false
-  const valid = await formRef.value.validate()
+  if (!userTaskListenerRef) return false
+  const valid = (await formRef.value.validate()) && (await userTaskListenerRef.value.validate())
   if (!valid) return false
   const showText = getShowText()
   if (!showText) return false
@@ -663,13 +745,38 @@ const saveConfig = async () => {
   currentNode.value.fieldsPermission = fieldsPermissionConfig.value
   // 设置按钮权限
   currentNode.value.buttonsSetting = buttonsSetting.value
+  // 创建任务监听器
+  currentNode.value.taskCreateListener = {
+    enable: configForm.value.taskCreateListenerEnable ?? false,
+    path: configForm.value.taskCreateListenerPath,
+    header: configForm.value.taskCreateListener?.header,
+    body: configForm.value.taskCreateListener?.body
+  }
+  // 指派任务监听器
+  currentNode.value.taskAssignListener = {
+    enable: configForm.value.taskAssignListenerEnable ?? false,
+    path: configForm.value.taskAssignListenerPath,
+    header: configForm.value.taskAssignListener?.header,
+    body: configForm.value.taskAssignListener?.body
+  }
+  // 完成任务监听器
+  currentNode.value.taskCompleteListener = {
+    enable: configForm.value.taskCompleteListenerEnable ?? false,
+    path: configForm.value.taskCompleteListenerPath,
+    header: configForm.value.taskCompleteListener?.header,
+    body: configForm.value.taskCompleteListener?.body
+  }
+  // 签名
+  currentNode.value.signEnable = configForm.value.signEnable
+  // 审批意见
+  currentNode.value.reasonRequire = configForm.value.reasonRequire
 
   currentNode.value.showText = showText
   settingVisible.value = false
   return true
 }
 
-// 显示审批节点配置, 由父组件传过来
+/** 显示审批节点配置, 由父组件传过来 */
 const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
   nodeName.value = node.name
   // 1 审批类型
@@ -689,13 +796,13 @@ const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
     configForm.value.approveRatio = node.approveRatio!
   }
   // 2.3 设置审批拒绝处理
-  configForm.value.rejectHandlerType = node.rejectHandler!.type
+  configForm.value.rejectHandlerType = node.rejectHandler?.type
   configForm.value.returnNodeId = node.rejectHandler?.returnNodeId
   const matchNodeList = []
   emits('find:returnTaskNodes', matchNodeList)
   returnTaskList.value = matchNodeList
   // 2.4 设置审批超时处理
-  configForm.value.timeoutHandlerEnable = node.timeoutHandler!.enable
+  configForm.value.timeoutHandlerEnable = node.timeoutHandler?.enable
   if (node.timeoutHandler?.enable && node.timeoutHandler?.timeDuration) {
     const strTimeDuration = node.timeoutHandler.timeDuration
     let parseTime = strTimeDuration.slice(2, strTimeDuration.length - 1)
@@ -711,16 +818,44 @@ const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
   // 2.6 设置用户任务的审批人与发起人相同时
   configForm.value.assignStartUserHandlerType = node.assignStartUserHandlerType
   // 3. 操作按钮设置
-  buttonsSetting.value = cloneDeep(node.buttonsSetting) || DEFAULT_BUTTON_SETTING
+  buttonsSetting.value =
+    cloneDeep(node.buttonsSetting) ||
+    (node.type === NodeType.TRANSACTOR_NODE
+      ? TRANSACTOR_DEFAULT_BUTTON_SETTING
+      : DEFAULT_BUTTON_SETTING)
   // 4. 表单字段权限配置
   getNodeConfigFormFields(node.fieldsPermission)
+  // 5. 监听器
+  // 5.1 创建任务
+  configForm.value.taskCreateListenerEnable = node.taskCreateListener?.enable
+  configForm.value.taskCreateListenerPath = node.taskCreateListener?.path
+  configForm.value.taskCreateListener = {
+    header: node.taskCreateListener?.header ?? [],
+    body: node.taskCreateListener?.body ?? []
+  }
+  // 5.2 指派任务
+  configForm.value.taskAssignListenerEnable = node.taskAssignListener?.enable
+  configForm.value.taskAssignListenerPath = node.taskAssignListener?.path
+  configForm.value.taskAssignListener = {
+    header: node.taskAssignListener?.header ?? [],
+    body: node.taskAssignListener?.body ?? []
+  }
+  // 5.3 完成任务
+  configForm.value.taskCompleteListenerEnable = node.taskCompleteListener?.enable
+  configForm.value.taskCompleteListenerPath = node.taskCompleteListener?.path
+  configForm.value.taskCompleteListener = {
+    header: node.taskCompleteListener?.header ?? [],
+    body: node.taskCompleteListener?.body ?? []
+  }
+  // 6. 签名
+  configForm.value.signEnable = node?.signEnable ?? false
+  // 7. 审批意见
+  configForm.value.reasonRequire = node?.reasonRequire ?? false
 }
 
 defineExpose({ openDrawer, showUserTaskNodeConfig }) // 暴露方法给父组件
 
-/**
- * @description 操作按钮设置
- */
+/** 操作按钮设置 */
 function useButtonsSetting() {
   const buttonsSetting = ref<ButtonSetting[]>()
   // 操作按钮显示名称可编辑
@@ -741,9 +876,7 @@ function useButtonsSetting() {
   }
 }
 
-/**
- * @description 审批人超时未处理配置
- */
+/** 审批人超时未处理配置 */
 function useTimeoutHandler() {
   // 时间单位
   const timeUnit = ref(TimeUnitType.HOUR)
@@ -826,6 +959,18 @@ function useTimeoutHandler() {
     cTimeoutMaxRemindCount
   }
 }
+
+/** 批量更新权限 */
+const updatePermission = (type: string) => {
+  fieldsPermissionConfig.value.forEach((field) => {
+    field.permission =
+      type === 'READ'
+        ? FieldPermissionType.READ
+        : type === 'WRITE'
+          ? FieldPermissionType.WRITE
+          : FieldPermissionType.NONE
+  })
+}
 </script>
 
 <style lang="scss" scoped>

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

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

+ 308 - 0
src/components/SimpleProcessDesignerV2/src/nodes-config/components/ConditionDialog.vue

@@ -0,0 +1,308 @@
+<!-- TODO @jason:有可能,它里面套 Condition 么?  -->
+<!-- TODO 怕影响其它节点功能,后面看看如何如何复用 Condtion --> 
+<template>
+  <Dialog v-model="dialogVisible" title="条件配置" width="600px" :fullscreen="false">
+    <div class="h-410px">
+      <el-scrollbar wrap-class="h-full">
+        <el-form ref="formRef" :model="condition" :rules="formRules" label-position="top">
+          <el-form-item label="配置方式" prop="conditionType">
+            <el-radio-group v-model="condition.conditionType" @change="changeConditionType">
+              <el-radio
+                v-for="(dict, indexConditionType) in conditionConfigTypes"
+                :key="indexConditionType"
+                :value="dict.value"
+                :label="dict.value"
+              >
+                {{ dict.label }}
+              </el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item
+            v-if="condition.conditionType === ConditionType.RULE && condition.conditionGroups"
+            label="条件规则"
+          >
+            <div class="condition-group-tool">
+              <div class="flex items-center">
+                <div class="mr-4">条件组关系</div>
+                <el-switch
+                  v-model="condition.conditionGroups.and"
+                  inline-prompt
+                  active-text="且"
+                  inactive-text="或"
+                />
+              </div>
+            </div>
+            <el-space direction="vertical" :spacer="condition.conditionGroups.and ? '且' : '或'">
+              <el-card
+                class="condition-group"
+                style="width: 530px"
+                v-for="(equation, cIdx) in condition.conditionGroups.conditions"
+                :key="cIdx"
+              >
+                <div
+                  class="condition-group-delete"
+                  v-if="condition.conditionGroups.conditions.length > 1"
+                >
+                  <Icon
+                    color="#0089ff"
+                    icon="ep:circle-close-filled"
+                    :size="18"
+                    @click="deleteConditionGroup(condition.conditionGroups.conditions, cIdx)"
+                  />
+                </div>
+                <template #header>
+                  <div class="flex items-center justify-between">
+                    <div>条件组</div>
+                    <div class="flex">
+                      <div class="mr-4">规则关系</div>
+                      <el-switch
+                        v-model="equation.and"
+                        inline-prompt
+                        active-text="且"
+                        inactive-text="或"
+                      />
+                    </div>
+                  </div>
+                </template>
+
+                <div class="flex pt-2" v-for="(rule, rIdx) in equation.rules" :key="rIdx">
+                  <div class="mr-2">
+                    <el-form-item
+                      :prop="`conditionGroups.conditions.${cIdx}.rules.${rIdx}.leftSide`"
+                      :rules="{
+                        required: true,
+                        message: '左值不能为空',
+                        trigger: 'change'
+                      }"
+                    >
+                      <el-select style="width: 160px" v-model="rule.leftSide">
+                        <el-option
+                          v-for="(field, fIdx) in fieldOptions"
+                          :key="fIdx"
+                          :label="field.title"
+                          :value="field.field"
+                          :disabled="!field.required"
+                        />
+                      </el-select>
+                    </el-form-item>
+                  </div>
+                  <div class="mr-2">
+                    <el-select v-model="rule.opCode" style="width: 100px">
+                      <el-option
+                        v-for="operator in COMPARISON_OPERATORS"
+                        :key="operator.value"
+                        :label="operator.label"
+                        :value="operator.value"
+                      />
+                    </el-select>
+                  </div>
+                  <div class="mr-2">
+                    <el-form-item
+                      :prop="`conditionGroups.conditions.${cIdx}.rules.${rIdx}.rightSide`"
+                      :rules="{
+                        required: true,
+                        message: '右值不能为空',
+                        trigger: 'blur'
+                      }"
+                    >
+                      <el-input v-model="rule.rightSide" style="width: 160px" />
+                    </el-form-item>
+                  </div>
+                  <div
+                    class="cursor-pointer mr-1 flex items-center"
+                    v-if="equation.rules.length > 1"
+                  >
+                    <Icon
+                      icon="ep:delete"
+                      :size="18"
+                      @click="deleteConditionRule(equation, rIdx)"
+                    />
+                  </div>
+                  <div class="cursor-pointer flex items-center">
+                    <Icon icon="ep:plus" :size="18" @click="addConditionRule(equation, rIdx)" />
+                  </div>
+                </div>
+              </el-card>
+            </el-space>
+            <div title="添加条件组" class="mt-4 cursor-pointer">
+              <Icon
+                color="#0089ff"
+                icon="ep:plus"
+                :size="24"
+                @click="addConditionGroup(condition.conditionGroups?.conditions)"
+              />
+            </div>
+          </el-form-item>
+          <el-form-item
+            v-if="condition.conditionType === ConditionType.EXPRESSION"
+            label="条件表达式"
+            prop="conditionExpression"
+          >
+            <el-input
+              type="textarea"
+              v-model="condition.conditionExpression"
+              clearable
+              style="width: 100%"
+            />
+          </el-form-item>
+        </el-form>
+      </el-scrollbar>
+    </div>
+    <template #footer>
+      <el-button type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import {
+  COMPARISON_OPERATORS,
+  CONDITION_CONFIG_TYPES,
+  ConditionType,
+  ConditionGroup,
+  DEFAULT_CONDITION_GROUP_VALUE
+} from '../../consts'
+import { BpmModelFormType } from '@/utils/constants'
+import { useFormFieldsAndStartUser } from '../../node'
+defineOptions({
+  name: 'ConditionDialog'
+})
+
+const condition = ref<{
+  conditionType: ConditionType
+  conditionExpression?: string
+  conditionGroups?: ConditionGroup
+}>({
+  conditionType: ConditionType.RULE,
+  conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
+})
+
+const emit = defineEmits<{
+  updateCondition: [condition: object]
+}>()
+const message = useMessage() // 消息弹窗
+const dialogVisible = ref(false) // 弹窗的是否展示
+
+const formType = inject<Ref<number>>('formType') // 表单类型
+const conditionConfigTypes = computed(() => {
+  return CONDITION_CONFIG_TYPES.filter((item) => {
+    // 业务表单暂时去掉条件规则选项
+    if (formType?.value === BpmModelFormType.CUSTOM && item.value === ConditionType.RULE) {
+      return false
+    } else {
+      return true
+    }
+  })
+})
+
+/** 条件规则可选择的表单字段 */
+const fieldOptions = useFormFieldsAndStartUser()
+
+// 表单校验规则
+const formRules = reactive({
+  conditionType: [{ required: true, message: '配置方式不能为空', trigger: 'blur' }],
+  conditionExpression: [{ required: true, message: '条件表达式不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 切换条件配置方式 */
+const changeConditionType = () => {
+  if (condition.value.conditionType === ConditionType.RULE) {
+    if (!condition.value.conditionGroups) {
+      condition.value.conditionGroups = DEFAULT_CONDITION_GROUP_VALUE
+    }
+  }
+}
+const deleteConditionGroup = (conditions, index) => {
+  conditions.splice(index, 1)
+}
+
+const deleteConditionRule = (condition, index) => {
+  condition.rules.splice(index, 1)
+}
+
+const addConditionRule = (condition, index) => {
+  const rule = {
+    opCode: '==',
+    leftSide: '',
+    rightSide: ''
+  }
+  condition.rules.splice(index + 1, 0, rule)
+}
+
+const addConditionGroup = (conditions) => {
+  const condition = {
+    and: true,
+    rules: [
+      {
+        opCode: '==',
+        leftSide: '',
+        rightSide: ''
+      }
+    ]
+  }
+  conditions.push(condition)
+}
+
+/** 保存条件设置 */
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) {
+    message.warning('请完善条件规则')
+    return
+  }
+  dialogVisible.value = false
+  // 设置完的条件传递给父组件
+  emit('updateCondition', condition.value)
+}
+
+const open = (conditionObj: any | undefined) => {
+  if (conditionObj) {
+    condition.value.conditionType = conditionObj.conditionType
+    condition.value.conditionExpression = conditionObj.conditionExpression
+    condition.value.conditionGroups = conditionObj.conditionGroups
+  }
+  dialogVisible.value = true
+}
+
+defineExpose({ open })
+</script>
+
+<style lang="scss" scoped>
+.condition-group-tool {
+  display: flex;
+  justify-content: space-between;
+  width: 500px;
+  margin-bottom: 20px;
+}
+
+.condition-group {
+  position: relative;
+
+  &:hover {
+    border-color: #0089ff;
+
+    .condition-group-delete {
+      opacity: 1;
+    }
+  }
+
+  .condition-group-delete {
+    position: absolute;
+    top: 0;
+    left: 0;
+    display: flex;
+    cursor: pointer;
+    opacity: 0;
+  }
+}
+
+::v-deep(.el-card__header) {
+  padding: 8px var(--el-card-padding);
+  border-bottom: 1px solid var(--el-card-border-color);
+  box-sizing: border-box;
+}
+</style>

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

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

+ 127 - 0
src/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestSetting.vue

@@ -0,0 +1,127 @@
+<template>
+  <el-form-item>
+    <el-alert
+      title="仅支持 POST 请求,以请求体方式接收参数"
+      type="warning"
+      show-icon
+      :closable="false"
+    />
+  </el-form-item>
+  <!-- 请求地址-->
+  <el-form-item
+    label-position="top"
+    label="请求地址"
+    :prop="`${formItemPrefix}.url`"
+    :rules="{
+      required: true,
+      message: '请求地址不能为空',
+      trigger: 'blur'
+    }"
+  >
+    <el-input v-model="setting.url" />
+  </el-form-item>
+  <!-- 请求头,请求体设置-->
+  <HttpRequestParamSetting :header="setting.header" :body="setting.body" :bind="formItemPrefix" />
+  <!-- 返回值设置-->
+  <div v-if="responseEnable">
+    <el-form-item label="返回值" label-position="top">
+      <el-alert
+        title="通过请求返回值, 可以修改流程表单的值"
+        type="warning"
+        show-icon
+        :closable="false"
+      />
+    </el-form-item>
+    <el-form-item>
+      <div class="flex pt-2" v-for="(item, index) in setting.response" :key="index">
+        <div class="mr-2">
+          <el-form-item
+            :prop="`${formItemPrefix}.response.${index}.key`"
+            :rules="{
+              required: true,
+              message: '表单字段不能为空',
+              trigger: 'blur'
+            }"
+          >
+            <el-select class="w-160px!" v-model="item.key" placeholder="请选择表单字段">
+              <el-option
+                v-for="(field, fIdx) in formFields"
+                :key="fIdx"
+                :label="field.title"
+                :value="field.field"
+                :disabled="!field.required"
+              />
+            </el-select>
+          </el-form-item>
+        </div>
+        <div class="mr-2">
+          <el-form-item
+            :prop="`${formItemPrefix}.response.${index}.value`"
+            :rules="{
+              required: true,
+              message: '请求返回字段不能为空',
+              trigger: 'blur'
+            }"
+          >
+            <el-input class="w-160px" v-model="item.value" placeholder="请求返回字段" />
+          </el-form-item>
+        </div>
+        <div class="mr-1 pt-1 cursor-pointer">
+          <Icon
+            icon="ep:delete"
+            :size="18"
+            @click="deleteHttpResponseSetting(setting.response!, index)"
+          />
+        </div>
+      </div>
+      <el-button type="primary" text @click="addHttpResponseSetting(setting.response!)">
+        <Icon icon="ep:plus" class="mr-5px" />添加一行
+      </el-button>
+    </el-form-item>
+  </div>
+</template>
+<script setup lang="ts">
+import HttpRequestParamSetting from './HttpRequestParamSetting.vue'
+import { useFormFields } from '../../node'
+
+const props = defineProps({
+  setting: {
+    type: Object,
+    required: true
+  },
+  responseEnable: {
+    type: Boolean,
+    required: true
+  },
+  formItemPrefix: {
+    type: String,
+    required: true
+  }
+})
+const { setting } = toRefs(props)
+const emits = defineEmits(['update:setting'])
+watch(
+  () => setting,
+  (val) => {
+    emits('update:setting', val)
+  }
+)
+
+/** 流程表单字段 */
+const formFields = useFormFields()
+
+/** 添加 HTTP 请求返回值设置项 */
+const addHttpResponseSetting = (responseSetting: Record<string, string>[]) => {
+  responseSetting.push({
+    key: '',
+    value: ''
+  })
+}
+
+/** 删除 HTTP 请求返回值设置项 */
+const deleteHttpResponseSetting = (responseSetting: Record<string, string>[], index: number) => {
+  responseSetting.splice(index, 1)
+}
+</script>
+
+<style lang="scss" scoped></style>

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

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

+ 106 - 0
src/components/SimpleProcessDesignerV2/src/nodes/ChildProcessNode.vue

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 10 - 3
src/components/SimpleProcessDesignerV2/src/nodes/UserTaskNode.vue

@@ -9,7 +9,14 @@
         ]"
       >
         <div class="node-title-container">
-          <div class="node-title-icon user-task"><span class="iconfont icon-approve"></span></div>
+          <div
+            :class="`node-title-icon ${currentNode.type === NodeType.TRANSACTOR_NODE ? 'transactor-task' : 'user-task'}`"
+          >
+            <span
+              :class="`iconfont ${currentNode.type === NodeType.TRANSACTOR_NODE ? 'icon-transactor' : 'icon-approve'}`"
+            >
+            </span>
+          </div>
           <input
             v-if="!readonly && showInput"
             type="text"
@@ -28,7 +35,7 @@
             {{ currentNode.showText }}
           </div>
           <div class="node-text" v-else>
-            {{ NODE_DEFAULT_TEXT.get(NodeType.USER_TASK_NODE) }}
+            {{ NODE_DEFAULT_TEXT.get(currentNode.type) }}
           </div>
           <Icon icon="ep:arrow-right-bold" v-if="!readonly" />
         </div>
@@ -131,7 +138,7 @@ const emits = defineEmits<{
 
 // 是否只读
 const readonly = inject<Boolean>('readonly')
-const tasks = inject<Ref<any[]>>('tasks')
+const tasks = inject<Ref<any[]>>('tasks', ref([]))
 // 监控节点变化
 const currentNode = useWatchNode(props)
 // 节点名称编辑

BIN
src/components/SimpleProcessDesignerV2/theme/iconfont.ttf


BIN
src/components/SimpleProcessDesignerV2/theme/iconfont.woff


BIN
src/components/SimpleProcessDesignerV2/theme/iconfont.woff2


+ 88 - 17
src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss

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

+ 1 - 0
src/components/Table/src/TableSelectForm.vue

@@ -1,4 +1,5 @@
 <!-- 列表选择通用组件,参考 ProductList 组件使用 -->
+<!-- TODO 芋艿:可能会移除 -->
 <template>
   <Dialog v-model="dialogVisible" :appendToBody="true" :scroll="true" :title="title" width="60%">
     <el-table

+ 17 - 27
src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue

@@ -188,12 +188,8 @@
       :scroll="true"
       max-height="600px"
     >
-      <!-- append-to-body -->
-      <div v-highlight>
-        <code class="hljs">
-          <!-- 高亮代码块 -->
-          {{ previewResult }}
-        </code>
+      <div>
+        <pre><code v-dompurify-html="highlightedCode(previewResult)" class="hljs"></code></pre>
       </div>
     </Dialog>
   </div>
@@ -237,6 +233,8 @@ import { XmlNode, XmlNodeType, parseXmlString } from 'steady-xml'
 // const eventName = reactive({
 //   name: ''
 // })
+import hljs from 'highlight.js' // 导入代码高亮文件
+import 'highlight.js/styles/github.css' // 导入代码高亮样式
 
 defineOptions({ name: 'MyProcessDesigner' })
 
@@ -308,27 +306,17 @@ const props = defineProps({
   }
 })
 
-// 监听value变化,重新加载流程图
-watch(
-  () => props.value,
-  (newValue) => {
-    if (newValue && bpmnModeler) {
-      createNewDiagram(newValue)
-    }
-  },
-  { immediate: true }
-)
-
-// 监听processId和processName变化
-watch(
-  [() => props.processId, () => props.processName],
-  ([newId, newName]) => {
-    if (newId && newName && !props.value) {
-      createNewDiagram(null)
-    }
-  },
-  { immediate: true }
-)
+/**
+ * 代码高亮
+ */
+const highlightedCode = (code: string) => {
+  // 高亮
+  if (previewType.value === 'json') {
+    code = JSON.stringify(code, null, 2)
+  }
+  const result = hljs.highlight(code, { language: previewType.value, ignoreIllegals: true })
+  return result.value || '&nbsp;'
+}
 
 provide('configGlobal', props)
 let bpmnModeler: any = null
@@ -480,6 +468,7 @@ const initModelListeners = () => {
       emit('commandStack-changed', event)
       emit('input', xml)
       emit('change', xml)
+      emit('save', xml)
     } catch (e: any) {
       console.error(`[Process Designer Warn]: ${e.message || e}`)
     }
@@ -568,6 +557,7 @@ const importLocalFile = () => {
   reader.onload = function () {
     let xmlStr = this.result
     createNewDiagram(xmlStr)
+    emit('save', xmlStr)
   }
 }
 /* ------------------------------------------------ refs methods ------------------------------------------------------ */

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

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

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

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

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

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

+ 83 - 18
src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue

@@ -5,6 +5,7 @@
      4. 操作按钮
      5. 字段权限
      6. 审批类型
+     7. 是否需要签名
 -->
 <template>
   <div>
@@ -122,13 +123,19 @@
     </div>
 
     <el-divider content-position="left">字段权限</el-divider>
-    <div class="field-setting-pane" v-if="formType === 10">
+    <div class="field-setting-pane" v-if="formType === BpmModelFormType.NORMAL">
       <div class="field-permit-title">
         <div class="setting-title-label first-title"> 字段名称 </div>
         <div class="other-titles">
-          <span class="setting-title-label">只读</span>
-          <span class="setting-title-label">可编辑</span>
-          <span class="setting-title-label">隐藏</span>
+          <span class="setting-title-label cursor-pointer" @click="updatePermission('READ')"
+            >只读</span
+          >
+          <span class="setting-title-label cursor-pointer" @click="updatePermission('WRITE')"
+            >可编辑</span
+          >
+          <span class="setting-title-label cursor-pointer" @click="updatePermission('NONE')"
+            >隐藏</span
+          >
         </div>
       </div>
       <div class="field-setting-item" v-for="(item, index) in fieldsPermissionEl" :key="index">
@@ -139,28 +146,54 @@
               :value="FieldPermissionType.READ"
               size="large"
               :label="FieldPermissionType.READ"
-              ><span></span
-            ></el-radio>
+              @change="updateElementExtensions"
+            >
+              <span></span>
+            </el-radio>
           </div>
           <div class="item-radio-wrap">
             <el-radio
               :value="FieldPermissionType.WRITE"
               size="large"
               :label="FieldPermissionType.WRITE"
-              ><span></span
-            ></el-radio>
+              @change="updateElementExtensions"
+            >
+              <span></span>
+            </el-radio>
           </div>
           <div class="item-radio-wrap">
             <el-radio
               :value="FieldPermissionType.NONE"
               size="large"
               :label="FieldPermissionType.NONE"
-              ><span></span
-            ></el-radio>
+              @change="updateElementExtensions"
+            >
+              <span></span>
+            </el-radio>
           </div>
         </el-radio-group>
       </div>
     </div>
+
+    <el-divider content-position="left">是否需要签名</el-divider>
+    <el-form-item prop="signEnable">
+      <el-switch
+        v-model="signEnable.value"
+        active-text="是"
+        inactive-text="否"
+        @change="updateElementExtensions"
+      />
+    </el-form-item>
+
+    <el-divider content-position="left">审批意见</el-divider>
+    <el-form-item prop="reasonRequire">
+      <el-switch
+        v-model="reasonRequire.value"
+        active-text="必填"
+        inactive-text="非必填"
+        @change="updateElementExtensions"
+      />
+    </el-form-item>
   </div>
 </template>
 
@@ -180,6 +213,7 @@ import {
 } from '@/components/SimpleProcessDesignerV2/src/consts'
 import * as UserApi from '@/api/system/user'
 import { useFormFieldsPermission } from '@/components/SimpleProcessDesignerV2/src/node'
+import { BpmModelFormType } from '@/utils/constants'
 
 defineOptions({ name: 'ElementCustomConfig4UserTask' })
 const props = defineProps({
@@ -218,6 +252,12 @@ const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFie
 // 审批类型
 const approveType = ref({ value: ApproveType.USER })
 
+// 是否需要签名
+const signEnable = ref({ value: false })
+
+// 审批意见
+const reasonRequire = ref({ value: false })
+
 const elExtensionElements = ref()
 const otherExtensions = ref()
 const bpmnElement = ref()
@@ -231,7 +271,6 @@ const resetCustomConfigList = () => {
     bpmnElement.value.id,
     bpmnInstances().modeler
   )
-
   // 获取元素扩展属性 或者 创建扩展属性
   elExtensionElements.value =
     bpmnElement.value.businessObject?.extensionElements ??
@@ -294,14 +333,13 @@ const resetCustomConfigList = () => {
   }
 
   // 字段权限
-  if (formType.value === 10) {
+  if (formType.value === BpmModelFormType.NORMAL) {
     const fieldsPermissionList = elExtensionElements.value.values?.filter(
       (ex) => ex.$type === `${prefix}:FieldsPermission`
     )
     fieldsPermissionEl.value = []
     getNodeConfigFormFields()
-    // 由于默认添加了发起人元素,这里需要删掉
-    fieldsPermissionConfig.value = fieldsPermissionConfig.value.slice(1)
+    fieldsPermissionConfig.value = fieldsPermissionConfig.value
     fieldsPermissionConfig.value.forEach((element) => {
       element.permission =
         fieldsPermissionList?.find((obj) => obj.field === element.field)?.permission ?? '1'
@@ -311,6 +349,16 @@ const resetCustomConfigList = () => {
     })
   }
 
+  // 是否需要签名
+  signEnable.value =
+    elExtensionElements.value.values?.filter((ex) => ex.$type === `${prefix}:SignEnable`)?.[0] ||
+    bpmnInstances().moddle.create(`${prefix}:SignEnable`, { value: false })
+
+  // 审批意见
+  reasonRequire.value =
+    elExtensionElements.value.values?.filter((ex) => ex.$type === `${prefix}:ReasonRequire`)?.[0] ||
+    bpmnInstances().moddle.create(`${prefix}:ReasonRequire`, { value: false })
+
   // 保留剩余扩展元素,便于后面更新该元素对应属性
   otherExtensions.value =
     elExtensionElements.value.values?.filter(
@@ -322,7 +370,9 @@ const resetCustomConfigList = () => {
         ex.$type !== `${prefix}:AssignEmptyUserIds` &&
         ex.$type !== `${prefix}:ButtonsSetting` &&
         ex.$type !== `${prefix}:FieldsPermission` &&
-        ex.$type !== `${prefix}:ApproveType`
+        ex.$type !== `${prefix}:ApproveType` &&
+        ex.$type !== `${prefix}:SignEnable` &&
+        ex.$type !== `${prefix}:ReasonRequire`
     ) ?? []
 
   // 更新元素扩展属性,避免后续报错
@@ -373,7 +423,9 @@ const updateElementExtensions = () => {
       assignEmptyUserIdsEl.value,
       approveType.value,
       ...buttonsSettingEl.value,
-      ...fieldsPermissionEl.value
+      ...fieldsPermissionEl.value,
+      signEnable.value,
+      reasonRequire.value
     ]
   })
   bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
@@ -456,6 +508,19 @@ function useButtonsSetting() {
   }
 }
 
+/** 批量更新权限 */
+// TODO @lesan:这个页面,有一些 idea 红色报错,咱要不要 fix 下!
+const updatePermission = (type: string) => {
+  fieldsPermissionEl.value.forEach((field) => {
+    field.permission =
+      type === 'READ'
+        ? FieldPermissionType.READ
+        : type === 'WRITE'
+          ? FieldPermissionType.WRITE
+          : FieldPermissionType.NONE
+  })
+}
+
 const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
 onMounted(async () => {
   // 获得用户列表
@@ -466,9 +531,9 @@ onMounted(async () => {
 <style lang="scss" scoped>
 .button-setting-pane {
   display: flex;
-  flex-direction: column;
-  font-size: 14px;
   margin-top: 8px;
+  font-size: 14px;
+  flex-direction: column;
 
   .button-setting-desc {
     padding-right: 8px;

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

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

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

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

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

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

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