فهرست منبع

merge yudao 的周期性更新

zhangcl 5 ماه پیش
والد
کامیت
8d21971a68
100فایلهای تغییر یافته به همراه6518 افزوده شده و 1033 حذف شده
  1. BIN
      .image/common/ai-feature.png
  2. 1 1
      .vscode/settings.json
  3. 2 1
      build/vite/optimize.ts
  4. 2 0
      package.json
  5. 249 99
      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. 6 1
      src/api/bpm/processInstance/index.ts
  17. 169 0
      src/api/iot/device/device/index.ts
  18. 43 0
      src/api/iot/device/group/index.ts
  19. 0 74
      src/api/iot/device/index.ts
  20. 51 0
      src/api/iot/plugin/index.ts
  21. 43 0
      src/api/iot/product/category/index.ts
  22. 21 1
      src/api/iot/product/product/index.ts
  23. 127 0
      src/api/iot/rule/databridge/index.ts
  24. 41 0
      src/api/iot/statistics/index.ts
  25. 88 0
      src/api/iot/thingmodel/index.ts
  26. 0 55
      src/api/iot/thinkmodelfunction/index.ts
  27. 1 1
      src/api/mall/product/spu.ts
  28. BIN
      src/assets/imgs/iot/device.png
  29. 1 0
      src/assets/svgs/bpm/child-process.svg
  30. 1 0
      src/assets/svgs/bpm/transactor.svg
  31. 1 0
      src/assets/svgs/iot/card-fill.svg
  32. 1 0
      src/assets/svgs/iot/cube.svg
  33. 42 5
      src/components/SimpleProcessDesignerV2/src/NodeHandler.vue
  34. 14 3
      src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue
  35. 83 16
      src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue
  36. 0 1
      src/components/SimpleProcessDesignerV2/src/SimpleProcessViewer.vue
  37. 145 7
      src/components/SimpleProcessDesignerV2/src/consts.ts
  38. 71 3
      src/components/SimpleProcessDesignerV2/src/node.ts
  39. 610 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/ChildProcessNodeConfig.vue
  40. 69 75
      src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue
  41. 23 5
      src/components/SimpleProcessDesignerV2/src/nodes-config/CopyTaskNodeConfig.vue
  42. 22 4
      src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue
  43. 356 166
      src/components/SimpleProcessDesignerV2/src/nodes-config/TriggerNodeConfig.vue
  44. 198 123
      src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue
  45. 15 3
      src/components/SimpleProcessDesignerV2/src/nodes-config/components/Condition.vue
  46. 308 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/components/ConditionDialog.vue
  47. 7 3
      src/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestParamSetting.vue
  48. 127 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestSetting.vue
  49. 106 0
      src/components/SimpleProcessDesignerV2/src/nodes/ChildProcessNode.vue
  50. 9 2
      src/components/SimpleProcessDesignerV2/src/nodes/UserTaskNode.vue
  51. BIN
      src/components/SimpleProcessDesignerV2/theme/iconfont.ttf
  52. BIN
      src/components/SimpleProcessDesignerV2/theme/iconfont.woff
  53. BIN
      src/components/SimpleProcessDesignerV2/theme/iconfont.woff2
  54. 34 2
      src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss
  55. 1 1
      src/components/Table/src/Table.vue
  56. 16 6
      src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue
  57. 52 18
      src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue
  58. 76 31
      src/router/modules/remaining.ts
  59. 10 4
      src/utils/dict.ts
  60. 1 1
      src/utils/formCreate.ts
  61. 80 8
      src/utils/index.ts
  62. 3 0
      src/utils/routerHelper.ts
  63. 13 10
      src/views/ai/chat/index/components/conversation/ConversationUpdateForm.vue
  64. 104 0
      src/views/ai/chat/index/components/message/MessageKnowledge.vue
  65. 2 0
      src/views/ai/chat/index/components/message/MessageList.vue
  66. 3 3
      src/views/ai/chat/index/components/role/RoleList.vue
  67. 2 0
      src/views/ai/chat/manager/index.vue
  68. 48 40
      src/views/ai/image/index/components/common/index.vue
  69. 54 11
      src/views/ai/image/index/components/dall3/index.vue
  70. 28 4
      src/views/ai/image/index/components/midjourney/index.vue
  71. 31 7
      src/views/ai/image/index/components/stableDiffusion/index.vue
  72. 33 19
      src/views/ai/image/index/index.vue
  73. 2 0
      src/views/ai/image/manager/index.vue
  74. 146 0
      src/views/ai/knowledge/document/form/ProcessStep.vue
  75. 238 0
      src/views/ai/knowledge/document/form/SplitStep.vue
  76. 273 0
      src/views/ai/knowledge/document/form/UploadStep.vue
  77. 193 0
      src/views/ai/knowledge/document/form/index.vue
  78. 236 0
      src/views/ai/knowledge/document/index.vue
  79. 162 0
      src/views/ai/knowledge/knowledge/KnowledgeForm.vue
  80. 221 0
      src/views/ai/knowledge/knowledge/index.vue
  81. 163 0
      src/views/ai/knowledge/knowledge/retrieval/index.vue
  82. 101 0
      src/views/ai/knowledge/segment/KnowledgeSegmentForm.vue
  83. 242 0
      src/views/ai/knowledge/segment/index.vue
  84. 2 2
      src/views/ai/mindmap/index/components/Left.vue
  85. 2 0
      src/views/ai/mindmap/manager/index.vue
  86. 2 0
      src/views/ai/model/apiKey/index.vue
  87. 37 9
      src/views/ai/model/chatRole/ChatRoleForm.vue
  88. 14 0
      src/views/ai/model/chatRole/index.vue
  89. 51 12
      src/views/ai/model/model/ModelForm.vue
  90. 27 20
      src/views/ai/model/model/index.vue
  91. 112 0
      src/views/ai/model/tool/ToolForm.vue
  92. 178 0
      src/views/ai/model/tool/index.vue
  93. 2 0
      src/views/ai/music/manager/index.vue
  94. 9 25
      src/views/ai/utils/constants.ts
  95. 8 37
      src/views/ai/write/manager/index.vue
  96. 9 9
      src/views/bpm/model/CategoryDraggableModel.vue
  97. 73 42
      src/views/bpm/model/definition/index.vue
  98. 77 0
      src/views/bpm/model/form/ExtraSettings.vue
  99. 6 5
      src/views/bpm/model/form/FormDesign.vue
  100. 1 1
      src/views/bpm/model/form/ProcessDesign.vue

BIN
.image/common/ai-feature.png


+ 1 - 1
.vscode/settings.json

@@ -87,7 +87,7 @@
     "source.fixAll.stylelint": "explicit"
   },
   "[vue]": {
-    "editor.defaultFormatter": "esbenp.prettier-vscode"
+    "editor.defaultFormatter": "octref.vetur"
   },
   "i18n-ally.localesPaths": ["src/locales"],
   "i18n-ally.keystyle": "nested",

+ 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']

+ 2 - 0
package.json

@@ -67,6 +67,7 @@
     "sortablejs": "^1.15.3",
     "steady-xml": "^0.1.0",
     "url": "^0.11.3",
+    "v3-jsoneditor": "^0.0.6",
     "video.js": "^7.21.5",
     "vue": "3.5.12",
     "vue-dompurify-html": "^4.1.4",
@@ -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",

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 249 - 99
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'
+  })
+}

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

@@ -90,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 })
 }
 
 // 获取表单字段权限

+ 169 - 0
src/api/iot/device/device/index.ts

@@ -0,0 +1,169 @@
+import request from '@/config/axios'
+
+// IoT 设备 VO
+export interface DeviceVO {
+  id: number // 设备 ID,主键,自增
+  deviceKey: string // 设备唯一标识符
+  deviceName: string // 设备名称
+  productId: number // 产品编号
+  productKey: string // 产品标识
+  deviceType: number // 设备类型
+  nickname: string // 设备备注名称
+  gatewayId: number // 网关设备 ID
+  state: number // 设备状态
+  onlineTime: Date // 最后上线时间
+  offlineTime: Date // 最后离线时间
+  activeTime: Date // 设备激活时间
+  createTime: Date // 创建时间
+  ip: string // 设备的 IP 地址
+  firmwareVersion: string // 设备的固件版本
+  deviceSecret: string // 设备密钥,用于设备认证,需安全存储
+  mqttClientId: string // MQTT 客户端 ID
+  mqttUsername: string // MQTT 用户名
+  mqttPassword: string // MQTT 密码
+  authType: string // 认证类型
+  latitude: number // 设备位置的纬度
+  longitude: number // 设备位置的经度
+  areaId: number // 地区编码
+  address: string // 设备详细地址
+  serialNumber: string // 设备序列号
+  config: string // 设备配置
+  groupIds?: number[] // 添加分组 ID
+}
+
+// IoT 设备数据 VO
+export interface DeviceDataVO {
+  deviceId: number // 设备编号
+  thinkModelFunctionId: number // 物模型编号
+  productKey: string // 产品标识
+  deviceName: string // 设备名称
+  identifier: string // 属性标识符
+  name: string // 属性名称
+  dataType: string // 数据类型
+  updateTime: Date // 更新时间
+  value: string // 最新值
+}
+
+// IoT 设备数据 VO
+export interface DeviceHistoryDataVO {
+  time: number // 时间
+  data: string // 数据
+}
+
+// IoT 设备状态枚举
+export enum DeviceStateEnum {
+  INACTIVE = 0, // 未激活
+  ONLINE = 1, // 在线
+  OFFLINE = 2 // 离线
+}
+
+// IoT 设备上行 Request VO
+export interface IotDeviceUpstreamReqVO {
+  id: number // 设备编号
+  type: string // 消息类型
+  identifier: string // 标识符
+  data: any // 请求参数
+}
+
+// IoT 设备下行 Request VO
+export interface IotDeviceDownstreamReqVO {
+  id: number // 设备编号
+  type: string // 消息类型
+  identifier: string // 标识符
+  data: any // 请求参数
+}
+
+// MQTT 连接参数 VO
+export interface MqttConnectionParamsVO {
+  mqttClientId: string // MQTT 客户端 ID
+  mqttUsername: string // MQTT 用户名
+  mqttPassword: string // MQTT 密码
+}
+
+// 设备 API
+export const DeviceApi = {
+  // 查询设备分页
+  getDevicePage: async (params: any) => {
+    return await request.get({ url: `/iot/device/page`, params })
+  },
+
+  // 查询设备详情
+  getDevice: async (id: number) => {
+    return await request.get({ url: `/iot/device/get?id=` + id })
+  },
+
+  // 新增设备
+  createDevice: async (data: DeviceVO) => {
+    return await request.post({ url: `/iot/device/create`, data })
+  },
+
+  // 修改设备
+  updateDevice: async (data: DeviceVO) => {
+    return await request.put({ url: `/iot/device/update`, data })
+  },
+
+  // 修改设备分组
+  updateDeviceGroup: async (data: { ids: number[]; groupIds: number[] }) => {
+    return await request.put({ url: `/iot/device/update-group`, data })
+  },
+
+  // 删除单个设备
+  deleteDevice: async (id: number) => {
+    return await request.delete({ url: `/iot/device/delete?id=` + id })
+  },
+
+  // 删除多个设备
+  deleteDeviceList: async (ids: number[]) => {
+    return await request.delete({ url: `/iot/device/delete-list`, params: { ids: ids.join(',') } })
+  },
+
+  // 导出设备
+  exportDeviceExcel: async (params: any) => {
+    return await request.download({ url: `/iot/device/export-excel`, params })
+  },
+
+  // 获取设备数量
+  getDeviceCount: async (productId: number) => {
+    return await request.get({ url: `/iot/device/count?productId=` + productId })
+  },
+
+  // 获取设备的精简信息列表
+  getSimpleDeviceList: async (deviceType?: number) => {
+    return await request.get({ url: `/iot/device/simple-list?`, params: { deviceType } })
+  },
+
+  // 获取导入模板
+  importDeviceTemplate: async () => {
+    return await request.download({ url: `/iot/device/get-import-template` })
+  },
+
+  // 设备上行
+  upstreamDevice: async (data: IotDeviceUpstreamReqVO) => {
+    return await request.post({ url: `/iot/device/upstream`, data })
+  },
+
+  // 设备下行
+  downstreamDevice: async (data: IotDeviceDownstreamReqVO) => {
+    return await request.post({ url: `/iot/device/downstream`, data })
+  },
+
+  // 获取设备属性最新数据
+  getLatestDeviceProperties: async (params: any) => {
+    return await request.get({ url: `/iot/device/property/latest`, params })
+  },
+
+  // 获取设备属性历史数据
+  getHistoryDevicePropertyPage: async (params: any) => {
+    return await request.get({ url: `/iot/device/property/history-page`, params })
+  },
+
+  // 查询设备日志分页
+  getDeviceLogPage: async (params: any) => {
+    return await request.get({ url: `/iot/device/log/page`, params })
+  },
+
+  // 获取设备MQTT连接参数
+  getMqttConnectionParams: async (deviceId: number) => {
+    return await request.get({ url: `/iot/device/mqtt-connection-params`, params: { deviceId } })
+  }
+}

+ 43 - 0
src/api/iot/device/group/index.ts

@@ -0,0 +1,43 @@
+import request from '@/config/axios'
+
+// IoT 设备分组 VO
+export interface DeviceGroupVO {
+  id: number // 分组 ID
+  name: string // 分组名字
+  status: number // 分组状态
+  description: string // 分组描述
+  deviceCount?: number // 设备数量
+}
+
+// IoT 设备分组 API
+export const DeviceGroupApi = {
+  // 查询设备分组分页
+  getDeviceGroupPage: async (params: any) => {
+    return await request.get({ url: `/iot/device-group/page`, params })
+  },
+
+  // 查询设备分组详情
+  getDeviceGroup: async (id: number) => {
+    return await request.get({ url: `/iot/device-group/get?id=` + id })
+  },
+
+  // 新增设备分组
+  createDeviceGroup: async (data: DeviceGroupVO) => {
+    return await request.post({ url: `/iot/device-group/create`, data })
+  },
+
+  // 修改设备分组
+  updateDeviceGroup: async (data: DeviceGroupVO) => {
+    return await request.put({ url: `/iot/device-group/update`, data })
+  },
+
+  // 删除设备分组
+  deleteDeviceGroup: async (id: number) => {
+    return await request.delete({ url: `/iot/device-group/delete?id=` + id })
+  },
+
+  // 获取设备分组的精简信息列表
+  getSimpleDeviceGroupList: async () => {
+    return await request.get({ url: `/iot/device-group/simple-list` })
+  }
+}

+ 0 - 74
src/api/iot/device/index.ts

@@ -1,74 +0,0 @@
-import request from '@/config/axios'
-
-// IoT 设备 VO
-export interface DeviceVO {
-  id: number // 设备 ID,主键,自增
-  deviceKey: string // 设备唯一标识符
-  deviceName: string // 设备名称
-  productId: number // 产品编号
-  productKey: string // 产品标识
-  deviceType: number // 设备类型
-  nickname: string // 设备备注名称
-  gatewayId: number // 网关设备 ID
-  status: number // 设备状态
-  statusLastUpdateTime: Date // 设备状态最后更新时间
-  lastOnlineTime: Date // 最后上线时间
-  lastOfflineTime: Date // 最后离线时间
-  activeTime: Date // 设备激活时间
-  createTime: Date // 创建时间
-  ip: string // 设备的 IP 地址
-  firmwareVersion: string // 设备的固件版本
-  deviceSecret: string // 设备密钥,用于设备认证,需安全存储
-  mqttClientId: string // MQTT 客户端 ID
-  mqttUsername: string // MQTT 用户名
-  mqttPassword: string // MQTT 密码
-  authType: string // 认证类型
-  latitude: number // 设备位置的纬度
-  longitude: number // 设备位置的经度
-  areaId: number // 地区编码
-  address: string // 设备详细地址
-  serialNumber: string // 设备序列号
-}
-
-export interface DeviceUpdateStatusVO {
-  id: number // 设备 ID,主键,自增
-  status: number // 设备状态
-}
-
-// 设备 API
-export const DeviceApi = {
-  // 查询设备分页
-  getDevicePage: async (params: any) => {
-    return await request.get({ url: `/iot/device/page`, params })
-  },
-
-  // 查询设备详情
-  getDevice: async (id: number) => {
-    return await request.get({ url: `/iot/device/get?id=` + id })
-  },
-
-  // 新增设备
-  createDevice: async (data: DeviceVO) => {
-    return await request.post({ url: `/iot/device/create`, data })
-  },
-
-  // 修改设备
-  updateDevice: async (data: DeviceVO) => {
-    return await request.put({ url: `/iot/device/update`, data })
-  },
-
-  // 修改设备状态
-  updateDeviceStatus: async (data: DeviceUpdateStatusVO) => {
-    return await request.put({ url: `/iot/device/update-status`, data })
-  },
-
-  // 删除设备
-  deleteDevice: async (id: number) => {
-    return await request.delete({ url: `/iot/device/delete?id=` + id })
-  },
-
-  // 获取设备数量
-  getDeviceCount: async (productId: number) => {
-    return await request.get({ url: `/iot/device/count?productId=` + productId })
-  }
-}

+ 51 - 0
src/api/iot/plugin/index.ts

@@ -0,0 +1,51 @@
+import request from '@/config/axios'
+
+// IoT 插件配置 VO
+export interface PluginConfigVO {
+  id: number // 主键ID
+  pluginKey: string // 插件标识
+  name: string // 插件名称
+  description: string // 描述
+  deployType: number // 部署方式
+  fileName: string // 插件包文件名
+  version: string // 插件版本
+  type: number // 插件类型
+  protocol: string // 设备插件协议类型
+  status: number // 状态
+  configSchema: string // 插件配置项描述信息
+  config: string // 插件配置信息
+  script: string // 插件脚本
+}
+
+// IoT 插件配置 API
+export const PluginConfigApi = {
+  // 查询插件配置分页
+  getPluginConfigPage: async (params: any) => {
+    return await request.get({ url: `/iot/plugin-config/page`, params })
+  },
+
+  // 查询插件配置详情
+  getPluginConfig: async (id: number) => {
+    return await request.get({ url: `/iot/plugin-config/get?id=` + id })
+  },
+
+  // 新增插件配置
+  createPluginConfig: async (data: PluginConfigVO) => {
+    return await request.post({ url: `/iot/plugin-config/create`, data })
+  },
+
+  // 修改插件配置
+  updatePluginConfig: async (data: PluginConfigVO) => {
+    return await request.put({ url: `/iot/plugin-config/update`, data })
+  },
+
+  // 删除插件配置
+  deletePluginConfig: async (id: number) => {
+    return await request.delete({ url: `/iot/plugin-config/delete?id=` + id })
+  },
+
+  // 修改插件状态
+  updatePluginStatus: async (data: any) => {
+    return await request.put({ url: `/iot/plugin-config/update-status`, data })
+  }
+}

+ 43 - 0
src/api/iot/product/category/index.ts

@@ -0,0 +1,43 @@
+import request from '@/config/axios'
+
+// IoT 产品分类 VO
+export interface ProductCategoryVO {
+  id: number // 分类 ID
+  name: string // 分类名字
+  sort: number // 分类排序
+  status: number // 分类状态
+  description: string // 分类描述
+}
+
+// IoT 产品分类 API
+export const ProductCategoryApi = {
+  // 查询产品分类分页
+  getProductCategoryPage: async (params: any) => {
+    return await request.get({ url: `/iot/product-category/page`, params })
+  },
+
+  // 查询产品分类详情
+  getProductCategory: async (id: number) => {
+    return await request.get({ url: `/iot/product-category/get?id=` + id })
+  },
+
+  // 新增产品分类
+  createProductCategory: async (data: ProductCategoryVO) => {
+    return await request.post({ url: `/iot/product-category/create`, data })
+  },
+
+  // 修改产品分类
+  updateProductCategory: async (data: ProductCategoryVO) => {
+    return await request.put({ url: `/iot/product-category/update`, data })
+  },
+
+  // 删除产品分类
+  deleteProductCategory: async (id: number) => {
+    return await request.delete({ url: `/iot/product-category/delete?id=` + id })
+  },
+
+  /** 获取产品分类精简列表 */
+  getSimpleProductCategoryList: () => {
+    return request.get({ url: '/iot/product-category/simple-list' })
+  }
+}

+ 21 - 1
src/api/iot/product/index.ts → src/api/iot/product/product/index.ts

@@ -7,6 +7,9 @@ export interface ProductVO {
   productKey: string // 产品标识
   protocolId: number // 协议编号
   categoryId: number // 产品所属品类标识符
+  categoryName?: string // 产品所属品类名称
+  icon: string // 产品图标
+  picUrl: string // 产品图片
   description: string // 产品描述
   validateType: number // 数据校验级别
   status: number // 产品状态
@@ -18,6 +21,23 @@ export interface ProductVO {
   createTime: Date // 创建时间
 }
 
+// IOT 数据校验级别枚举类
+export enum ValidateTypeEnum {
+  WEAK = 0, // 弱校验
+  NONE = 1 // 免校验
+}
+// IOT 产品设备类型枚举类 0: 直连设备, 1: 网关子设备, 2: 网关设备
+export enum DeviceTypeEnum {
+  DEVICE = 0, // 直连设备
+  GATEWAY_SUB = 1, // 网关子设备
+  GATEWAY = 2 // 网关设备
+}
+// IOT 数据格式枚举类
+export enum DataFormatEnum {
+  JSON = 0, // 标准数据格式(JSON)
+  CUSTOMIZE = 1 // 透传/自定义
+}
+
 // IoT 产品 API
 export const ProductApi = {
   // 查询产品分页
@@ -57,6 +77,6 @@ export const ProductApi = {
 
   // 查询产品(精简)列表
   getSimpleProductList() {
-    return request.get({ url: '/iot/product/list-all-simple' })
+    return request.get({ url: '/iot/product/simple-list' })
   }
 }

+ 127 - 0
src/api/iot/rule/databridge/index.ts

@@ -0,0 +1,127 @@
+import request from '@/config/axios'
+
+// IoT 数据桥梁 VO
+export interface DataBridgeVO {
+  id?: number // 桥梁编号
+  name?: string // 桥梁名称
+  description?: string // 桥梁描述
+  status?: number // 桥梁状态
+  direction?: number // 桥梁方向
+  type?: number // 桥梁类型
+  config?:
+    | HttpConfig
+    | MqttConfig
+    | RocketMQConfig
+    | KafkaMQConfig
+    | RabbitMQConfig
+    | RedisStreamMQConfig // 桥梁配置
+}
+
+interface Config {
+  type: string
+}
+
+/** HTTP 配置 */
+export interface HttpConfig extends Config {
+  url: string
+  method: string
+  headers: Record<string, string>
+  query: Record<string, string>
+  body: string
+}
+
+/** MQTT 配置 */
+export interface MqttConfig extends Config {
+  url: string
+  username: string
+  password: string
+  clientId: string
+  topic: string
+}
+
+/** RocketMQ 配置 */
+export interface RocketMQConfig extends Config {
+  nameServer: string
+  accessKey: string
+  secretKey: string
+  group: string
+  topic: string
+  tags: string
+}
+
+/** Kafka 配置 */
+export interface KafkaMQConfig extends Config {
+  bootstrapServers: string
+  username: string
+  password: string
+  ssl: boolean
+  topic: string
+}
+
+/** RabbitMQ 配置 */
+export interface RabbitMQConfig extends Config {
+  host: string
+  port: number
+  virtualHost: string
+  username: string
+  password: string
+  exchange: string
+  routingKey: string
+  queue: string
+}
+
+/** Redis Stream MQ 配置 */
+export interface RedisStreamMQConfig extends Config {
+  host: string
+  port: number
+  password: string
+  database: number
+  topic: string
+}
+
+/** 数据桥梁类型 */
+// TODO @puhui999:枚举用 number 可以么?
+export const IoTDataBridgeConfigType = {
+  HTTP: '1',
+  TCP: '2',
+  WEBSOCKET: '3',
+  MQTT: '10',
+  DATABASE: '20',
+  REDIS_STREAM: '21',
+  ROCKETMQ: '30',
+  RABBITMQ: '31',
+  KAFKA: '32'
+} as const
+
+// 数据桥梁 API
+export const DataBridgeApi = {
+  // 查询数据桥梁分页
+  getDataBridgePage: async (params: any) => {
+    return await request.get({ url: `/iot/data-bridge/page`, params })
+  },
+
+  // 查询数据桥梁详情
+  getDataBridge: async (id: number) => {
+    return await request.get({ url: `/iot/data-bridge/get?id=` + id })
+  },
+
+  // 新增数据桥梁
+  createDataBridge: async (data: DataBridgeVO) => {
+    return await request.post({ url: `/iot/data-bridge/create`, data })
+  },
+
+  // 修改数据桥梁
+  updateDataBridge: async (data: DataBridgeVO) => {
+    return await request.put({ url: `/iot/data-bridge/update`, data })
+  },
+
+  // 删除数据桥梁
+  deleteDataBridge: async (id: number) => {
+    return await request.delete({ url: `/iot/data-bridge/delete?id=` + id })
+  },
+
+  // 导出数据桥梁 Excel
+  exportDataBridge: async (params) => {
+    return await request.download({ url: `/iot/data-bridge/export-excel`, params })
+  }
+}

+ 41 - 0
src/api/iot/statistics/index.ts

@@ -0,0 +1,41 @@
+import request from '@/config/axios'
+
+/** IoT 统计数据类型 */
+export interface IotStatisticsSummaryRespVO {
+  productCategoryCount: number
+  productCount: number
+  deviceCount: number
+  deviceMessageCount: number
+  productCategoryTodayCount: number
+  productTodayCount: number
+  deviceTodayCount: number
+  deviceMessageTodayCount: number
+  deviceOnlineCount: number
+  deviceOfflineCount: number
+  deviceInactiveCount: number
+  productCategoryDeviceCounts: Record<string, number>
+}
+
+/** IoT 消息统计数据类型 */
+export interface IotStatisticsDeviceMessageSummaryRespVO {
+  upstreamCounts: Record<number, number>
+  downstreamCounts: Record<number, number>
+}
+
+// IoT 数据统计 API
+export const ProductCategoryApi = {
+  // 查询基础的数据统计
+  getIotStatisticsSummary: async () => {
+    return await request.get<IotStatisticsSummaryRespVO>({
+      url: `/iot/statistics/get-summary`
+    })
+  },
+
+  // 查询设备上下行消息的数据统计
+  getIotStatisticsDeviceMessageSummary: async (params: { startTime: number; endTime: number }) => {
+    return await request.get<IotStatisticsDeviceMessageSummaryRespVO>({
+      url: `/iot/statistics/get-log-summary`,
+      params
+    })
+  }
+}

+ 88 - 0
src/api/iot/thingmodel/index.ts

@@ -0,0 +1,88 @@
+import request from '@/config/axios'
+
+/**
+ * IoT 产品物模型
+ */
+export interface ThingModelData {
+  id?: number // 物模型功能编号
+  identifier?: string // 功能标识
+  name?: string // 功能名称
+  description?: string // 功能描述
+  productId?: number // 产品编号
+  productKey?: string // 产品标识
+  dataType: string // 数据类型,与 dataSpecs 的 dataType 保持一致
+  type: number // 功能类型
+  property: ThingModelProperty // 属性
+  event?: ThingModelEvent // 事件
+  service?: ThingModelService // 服务
+}
+
+/**
+ * IoT 模拟设备
+ */
+// TODO @super:和 ThingModelSimulatorData 会不会好点
+export interface SimulatorData extends ThingModelData {
+  simulateValue?: string | number // 用于存储模拟值 TODO @super:字段使用 value 会不会好点
+}
+
+/**
+ * ThingModelProperty 类型
+ */
+export interface ThingModelProperty {
+  [key: string]: any
+}
+
+/**
+ * ThingModelEvent 类型
+ */
+export interface ThingModelEvent {
+  [key: string]: any
+}
+
+/**
+ * ThingModelService 类型
+ */
+export interface ThingModelService {
+  [key: string]: any
+}
+
+// IoT 产品物模型 API
+export const ThingModelApi = {
+  // 查询产品物模型分页
+  getThingModelPage: async (params: any) => {
+    return await request.get({ url: `/iot/thing-model/page`, params })
+  },
+
+  // 获得产品物模型列表
+  getThingModelList: async (params: any) => {
+    return await request.get({ url: `/iot/thing-model/list`, params })
+  },
+
+  // 获得产品物模型
+  getThingModelListByProductId: async (params: any) => {
+    return await request.get({
+      url: `/iot/thing-model/list-by-product-id`,
+      params
+    })
+  },
+
+  // 查询产品物模型详情
+  getThingModel: async (id: number) => {
+    return await request.get({ url: `/iot/thing-model/get?id=` + id })
+  },
+
+  // 新增产品物模型
+  createThingModel: async (data: ThingModelData) => {
+    return await request.post({ url: `/iot/thing-model/create`, data })
+  },
+
+  // 修改产品物模型
+  updateThingModel: async (data: ThingModelData) => {
+    return await request.put({ url: `/iot/thing-model/update`, data })
+  },
+
+  // 删除产品物模型
+  deleteThingModel: async (id: number) => {
+    return await request.delete({ url: `/iot/thing-model/delete?id=` + id })
+  }
+}

+ 0 - 55
src/api/iot/thinkmodelfunction/index.ts

@@ -1,55 +0,0 @@
-import request from '@/config/axios'
-
-// IoT 产品物模型 VO
-export interface ThinkModelFunctionVO {
-  id: number // 物模型功能编号
-  identifier: string // 功能标识
-  name: string // 功能名称
-  description: string // 功能描述
-  productId: number // 产品编号
-  productKey: string // 产品标识
-  type: number // 功能类型
-  property: string // 属性
-  event: string // 事件
-  service: string // 服务
-}
-
-// IoT 产品物模型 API
-export const ThinkModelFunctionApi = {
-  // 查询产品物模型分页
-  getThinkModelFunctionPage: async (params: any) => {
-    return await request.get({ url: `/iot/think-model-function/page`, params })
-  },
-  // 获得产品物模型
-  getThinkModelFunctionListByProductId: async (params: any) => {
-    return await request.get({
-      url: `/iot/think-model-function/list-by-product-id`,
-      params
-    })
-  },
-
-  // 查询产品物模型详情
-  getThinkModelFunction: async (id: number) => {
-    return await request.get({ url: `/iot/think-model-function/get?id=` + id })
-  },
-
-  // 新增产品物模型
-  createThinkModelFunction: async (data: ThinkModelFunctionVO) => {
-    return await request.post({ url: `/iot/think-model-function/create`, data })
-  },
-
-  // 修改产品物模型
-  updateThinkModelFunction: async (data: ThinkModelFunctionVO) => {
-    return await request.put({ url: `/iot/think-model-function/update`, data })
-  },
-
-  // 删除产品物模型
-  deleteThinkModelFunction: async (id: number) => {
-    return await request.delete({ url: `/iot/think-model-function/delete?id=` + id })
-  },
-
-  // 导出产品物模型 Excel
-  exportThinkModelFunction: async (params) => {
-    return await request.download({ url: `/iot/think-model-function/export-excel`, params })
-  }
-}

+ 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 })
 }
 

BIN
src/assets/imgs/iot/device.png


+ 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>

+ 1 - 0
src/assets/svgs/iot/card-fill.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" class="design-iconfont" viewBox="0 0 12 12"><path fill="url(#a)" fill-rule="evenodd" d="M1 0a1 1 0 0 0-1 1v3.538a1 1 0 0 0 1 1h3.538a1 1 0 0 0 1-1V1a1 1 0 0 0-1-1H1Zm0 6.462a1 1 0 0 0-1 1V11a1 1 0 0 0 1 1h3.538a1 1 0 0 0 1-1V7.462a1 1 0 0 0-1-1H1ZM6.462 1a1 1 0 0 1 1-1H11a1 1 0 0 1 1 1v3.538a1 1 0 0 1-1 1H7.462a1 1 0 0 1-1-1V1Zm1 5.462a1 1 0 0 0-1 1V11a1 1 0 0 0 1 1H11a1 1 0 0 0 1-1V7.462a1 1 0 0 0-1-1H7.462Z" clip-rule="evenodd"/><defs><linearGradient id="a" x1="0" x2="12" y1="0" y2="12" gradientUnits="userSpaceOnUse"><stop stop-color="#1B3149"/><stop offset="1" stop-color="#717D8A"/></linearGradient></defs></svg>

+ 1 - 0
src/assets/svgs/iot/cube.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 12 12"><g clip-path="url(#a)"><path fill="url(#b)" fill-rule="evenodd" d="M6.958.42C6.444.216 5.61.216 5.098.42L1.15 1.975c-.77.304-.77.797 0 1.1l3.947 1.558c.514.202 1.347.202 1.86 0l3.948-1.557c.77-.304.77-.797 0-1.1L6.958.418ZM4.715 11.788a.857.857 0 0 0 .3.056c.383 0 .671-.295.671-.7V6.404c0-.49-.364-1.007-.817-1.177L1.09 3.805a.808.808 0 0 0-.284-.056c-.353 0-.581.275-.581.7V9.19c0 .508.33 1.014.763 1.177l3.726 1.422Zm2.229-.024h-.02l.073.003c.074.004.154.009.227-.019L11 10.367c.45-.168.83-.686.83-1.177V4.45c0-.413-.29-.7-.673-.7a.965.965 0 0 0-.317.055l-3.72 1.422c-.44.165-.75.67-.75 1.177v4.74c0 .42.218.621.575.621Z" clip-rule="evenodd"/></g><defs><linearGradient id="b" x1=".226" x2="11.803" y1=".267" y2="11.871" gradientUnits="userSpaceOnUse"><stop stop-color="#1B3149"/><stop offset="1" stop-color="#717D8A"/></linearGradient><clipPath id="a"><path fill="#fff" d="M0 0h12v12H0z"/></clipPath></defs></svg>

+ 42 - 5
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>
@@ -57,7 +63,13 @@
             </div>
             <div class="handler-item-text">触发器</div>
           </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>
         </template>
@@ -78,7 +90,7 @@ import {
   SimpleFlowNode,
   DEFAULT_CONDITION_GROUP_VALUE
 } from './consts'
-import {generateUUID} from '@/utils'
+import { generateUUID } from '@/utils'
 
 defineOptions({
   name: 'NodeHandler'
@@ -114,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: {
@@ -277,6 +289,31 @@ const addNode = (type: number) => {
     }
     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>
 

+ 14 - 3
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"
@@ -50,12 +54,18 @@
     :flow-node="currentNode"
     @update:flow-node="handleModelValueUpdate"
   />
-   <!-- 触发器节点 -->
-   <TriggerNode
+  <!-- 触发器节点 -->
+  <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"
@@ -81,6 +91,7 @@ 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({

+ 83 - 16
src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue

@@ -1,6 +1,6 @@
 <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">
@@ -23,10 +23,19 @@
           <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>
@@ -76,11 +85,51 @@ const emits = defineEmits<{
 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
@@ -88,7 +137,6 @@ const zoomIn = () => {
   scaleValue.value += 10
 }
 
-// 缩小
 const zoomOut = () => {
   if (scaleValue.value == MIN_SCALE_VALUE) {
     return
@@ -100,20 +148,15 @@ const processReZoom = () => {
   scaleValue.value = 100
 }
 
+const resetPosition = () => {
+  currentX.value = initialX.value
+  currentY.value = initialY.value
+}
+
+/** 校验节点设置 */
 const errorDialogVisible = ref(false)
 let errorNodes: SimpleFlowNode[] = []
 
-const saveSimpleFlowModel = async () => {
-  errorNodes = []
-  validateNode(processNodeTree.value, errorNodes)
-  if (errorNodes.length > 0) {
-    errorDialogVisible.value = true
-    return
-  }
-  emits('save', processNodeTree.value)
-}
-
-// 校验节点设置。 暂时以 showText 为空 未节点错误配置
 const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
   if (node) {
     const { type, showText, conditionNodes } = node
@@ -193,6 +236,30 @@ const importLocalFile = () => {
     }
   }
 }
+
+// 在组件初始化时记录初始位置
+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

+ 145 - 7
src/components/SimpleProcessDesignerV2/src/consts.ts

@@ -23,6 +23,11 @@ export enum NodeType {
    */
   COPY_TASK_NODE = 12,
 
+  /**
+   * 办理人节点
+   */
+  TRANSACTOR_NODE = 13,
+
   /**
    * 延迟器节点
    */
@@ -33,6 +38,11 @@ export enum NodeType {
    */
   TRIGGER_NODE = 15,
 
+  /**
+   * 子流程节点
+   */
+  CHILD_PROCESS_NODE = 20,
+
   /**
    * 条件节点
    */
@@ -123,6 +133,8 @@ export interface SimpleFlowNode {
   reasonRequire?: boolean
   // 触发器设置
   triggerSetting?: TriggerSetting
+  // 子流程
+  childProcessSetting?: ChildProcessSetting
 }
 // 候选人策略枚举 ( 用于审批节点。抄送节点 )
 export enum CandidateStrategy {
@@ -150,6 +162,10 @@ export enum CandidateStrategy {
    * 指定用户
    */
   USER = 30,
+  /**
+   * 审批人自选
+   */
+  APPROVE_USER_SELECT = 34,
   /**
    * 发起人自选
    */
@@ -506,6 +522,8 @@ 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, '审批人')
@@ -515,15 +533,19 @@ 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 },
@@ -627,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 },
@@ -717,7 +749,7 @@ export type RouterSetting = {
 export type TriggerSetting = {
   type: TriggerTypeEnum
   httpRequestSetting?: HttpRequestTriggerSetting
-  normalFormSetting?: NormalFormTriggerSetting
+  formSettings?: FormTriggerSetting[]
 }
 
 /**
@@ -729,9 +761,17 @@ export enum TriggerTypeEnum {
    */
   HTTP_REQUEST = 1,
   /**
-   * 更新流程表单触发器
+   * 接收 HTTP 回调请求触发器
    */
-  UPDATE_NORMAL_FORM = 2 // TODO @jason:FORM_UPDATE?
+  HTTP_CALLBACK = 2,
+  /**
+   * 表单数据更新触发器
+   */
+  FORM_UPDATE = 10,
+  /**
+   * 表单数据删除触发器
+   */
+  FORM_DELETE = 11
 }
 
 /**
@@ -751,12 +791,110 @@ export type HttpRequestTriggerSetting = {
 /**
  * 流程表单触发器配置结构定义
  */
-export type NormalFormTriggerSetting = {
-  // 更新表单字段
+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: '修改表单数据', value: TriggerTypeEnum.UPDATE_NORMAL_FORM }
+  { 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 }
 ]

+ 71 - 3
src/components/SimpleProcessDesignerV2/src/node.ts

@@ -15,7 +15,10 @@ import {
   AssignEmptyHandlerType,
   FieldPermissionType,
   HttpRequestParam,
-  ProcessVariableEnum
+  ProcessVariableEnum,
+  ConditionType,
+  ConditionGroup,
+  COMPARISON_OPERATORS
 } from './consts'
 import { parseFormFields } from '@/components/FormCreate/src/utils'
 
@@ -201,7 +204,7 @@ export function useNodeForm(nodeType: NodeType) {
   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,
@@ -307,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 = `发起人自选`
@@ -543,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>

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

@@ -43,15 +43,12 @@
   </el-drawer>
 </template>
 <script setup lang="ts">
-import {
-  SimpleFlowNode,
-  ConditionType,
-  COMPARISON_OPERATORS,
-} from '../consts'
+import { SimpleFlowNode, ConditionType } from '../consts'
 import { getDefaultConditionNodeName } from '../utils'
-import { useFormFieldsAndStartUser } from '../node'
+import { useFormFieldsAndStartUser, getConditionShowText } from '../node'
 import Condition from './components/Condition.vue'
-const message = useMessage() // 消息弹窗
+import { cloneDeep } from 'lodash-es'
+
 defineOptions({
   name: 'ConditionNodeConfig'
 })
@@ -67,9 +64,51 @@ const props = defineProps({
 })
 const settingVisible = ref(false)
 const currentNode = ref<SimpleFlowNode>(props.conditionNode)
-const condition = ref<any>()
+const condition = ref<any>({
+  conditionType: ConditionType.RULE, // 设置默认值
+  conditionExpression: '',
+  conditionGroups: {
+    and: true,
+    conditions: [
+      {
+        and: true,
+        rules: [
+          {
+            opCode: '==',
+            leftSide: '',
+            rightSide: ''
+          }
+        ]
+      }
+    ]
+  }
+})
 const open = () => {
-  condition.value = currentNode.value.conditionSetting
+  // 如果有已存在的配置则使用,否则使用默认值
+  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
 }
 
@@ -93,8 +132,6 @@ const blurEvent = () => {
     getDefaultConditionNodeName(props.nodeIndex, currentNode.value?.conditionSetting?.defaultFlow)
 }
 
-
-
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
 // 关闭
@@ -111,84 +148,41 @@ const handleClose = async (done: (cancel?: boolean) => void) => {
   }
 }
 
+/** 保存配置 */
+const fieldOptions = useFormFieldsAndStartUser() // 流程表单字段和发起人字段
 const conditionRef = ref()
-// 保存配置
 const saveConfig = async () => {
   if (!currentNode.value.conditionSetting?.defaultFlow) {
     // 校验表单
     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
-    currentNode.value.conditionSetting!.conditionType = condition.value?.conditionType
-    if (currentNode.value.conditionSetting?.conditionType === ConditionType.EXPRESSION) {
-      currentNode.value.conditionSetting.conditionGroups = undefined
-      currentNode.value.conditionSetting.conditionExpression = condition.value?.conditionExpression
-    }
-    if (currentNode.value.conditionSetting!.conditionType === ConditionType.RULE) {
-      currentNode.value.conditionSetting!.conditionExpression = undefined
-      currentNode.value.conditionSetting!.conditionGroups = condition.value?.conditionGroups
-    }
+    // 使用 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 (condition.value?.conditionType === ConditionType.EXPRESSION) {
-    if (condition.value.conditionExpression) {
-      showText = `表达式:${condition.value.conditionExpression}`
-    }
-  }
-  if (condition.value?.conditionType === ConditionType.RULE) {
-    // 条件组是否为与关系
-    const groupAnd = condition.value.conditionGroups?.and
-    let warningMesg: undefined | string = undefined
-    const conditionGroup = condition.value.conditionGroups?.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 fieldOptions = useFormFieldsAndStartUser()
-
-/** 获取字段名称 */
-const getFieldTitle = (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
-}
 </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>
 

+ 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>
 

+ 356 - 166
src/components/SimpleProcessDesignerV2/src/nodes-config/TriggerNodeConfig.vue

@@ -3,7 +3,7 @@
     :append-to-body="true"
     v-model="settingVisible"
     :show-close="false"
-    :size="550"
+    :size="630"
     :before-close="saveConfig"
   >
     <template #header>
@@ -26,7 +26,7 @@
     <div>
       <el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
         <el-form-item label="触发器类型" prop="type">
-          <el-select v-model="configForm.type">
+          <el-select v-model="configForm.type" @change="changeTriggerType">
             <el-option
               v-for="(item, index) in TRIGGER_TYPES"
               :key="index"
@@ -37,146 +37,197 @@
         </el-form-item>
         <!-- HTTP 请求触发器 -->
         <div
-          v-if="configForm.type === TriggerTypeEnum.HTTP_REQUEST && configForm.httpRequestSetting"
+          v-if="
+            [TriggerTypeEnum.HTTP_REQUEST, TriggerTypeEnum.HTTP_CALLBACK].includes(
+              configForm.type
+            ) && configForm.httpRequestSetting
+          "
         >
-          <el-form-item>
-            <el-alert
-              title="仅支持 POST 请求,以请求体方式接收参数"
-              type="warning"
-              show-icon
-              :closable="false"
-            />
-          </el-form-item>
-          <!-- 请求地址-->
-          <el-form-item label="请求地址" prop="httpRequestSetting.url">
-            <el-input v-model="configForm.httpRequestSetting.url" />
-          </el-form-item>
-          <!-- 请求头,请求体设置-->
-          <HttpRequestParamSetting
-            :header="configForm.httpRequestSetting.header"
-            :body="configForm.httpRequestSetting.body"
-            :bind="'httpRequestSetting'"
+          <HttpRequestSetting
+            v-model:setting="configForm.httpRequestSetting"
+            :responseEnable="configForm.type === TriggerTypeEnum.HTTP_REQUEST"
+            :formItemPrefix="'httpRequestSetting'"
           />
-          <!-- 返回值设置-->
-          <el-form-item label="返回值">
-            <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 configForm.httpRequestSetting.response"
-              :key="index"
-            >
-              <div class="mr-2">
-                <el-form-item
-                  :prop="`httpRequestSetting.response.${index}.key`"
-                  :rules="{
-                    required: true,
-                    message: '表单字段不能为空',
-                    trigger: 'blur'
-                  }"
+        </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)"
                 >
-                  <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"
+                  {{ 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-select>
-                </el-form-item>
+                  </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>
-              <div class="mr-2">
-                <el-form-item
-                  :prop="`httpRequestSetting.response.${index}.value`"
-                  :rules="{
-                    required: true,
-                    message: '请求返回字段不能为空',
-                    trigger: 'blur'
-                  }"
+
+              <!-- 添加表单字段按钮 -->
+              <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)"
                 >
-                  <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(configForm.httpRequestSetting.response!, index)"
-                />
+                  {{ showConditionText(formSetting) }}
+                </el-tag>
               </div>
-            </div>
-            <el-button
-              type="primary"
-              text
-              @click="addHttpResponseSetting(configForm.httpRequestSetting.response!)"
-            >
-              <Icon icon="ep:plus" class="mr-5px" />添加一行
-            </el-button>
-          </el-form-item>
-        </div>
-        <div
-          v-if="
-            configForm.type === TriggerTypeEnum.UPDATE_NORMAL_FORM && configForm.normalFormSetting
-          "
-        >
-          <el-divider content-position="left">修改表单设置</el-divider>
-          <div
-            class="flex items-center"
-            v-for="key in Object.keys(configForm.normalFormSetting.updateFormFields!)"
-            :key="key"
-          >
-            <div class="mr-2 flex items-center">
-              <el-form-item>
+              <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
-                  class="w-160px!"
-                  :model-value="key"
-                  @update:model-value="(newKey) => updateFormFieldKey(key, newKey)"
-                  placeholder="请选择表单字段"
-                  :disabled="key !== ''"
+                  v-model="formSetting.deleteFields"
+                  multiple
+                  placeholder="请选择要删除的字段"
+                  class="w-full"
                 >
                   <el-option
-                    v-for="(field, fIdx) in optionalUpdateFormFields"
-                    :key="fIdx"
+                    v-for="field in formFields"
+                    :key="field.field"
                     :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="`normalFormSetting.updateFormFields.${key}`"
-                :rules="{
-                  required: true,
-                  message: '值不能为空',
-                  trigger: 'blur'
-                }"
-              >
-                <el-input
-                  class="w-160px"
-                  v-model="configForm.normalFormSetting.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(key)" />
-              </el-form-item>
-            </div>
+              </div>
+            </el-card>
           </div>
-          <el-button type="primary" text @click="addFormFieldSetting()">
-            <Icon icon="ep:plus" class="mr-5px" />添加修改字段
+
+          <!-- 添加新的设置 -->
+          <el-button class="mt-6" type="primary" text @click="addFormSetting">
+            <Icon icon="ep:setting" class="mr-5px" />添加设置
           </el-button>
         </div>
       </el-form>
@@ -191,9 +242,19 @@
   </el-drawer>
 </template>
 <script setup lang="ts">
-import { SimpleFlowNode, NodeType, TriggerSetting, TRIGGER_TYPES, TriggerTypeEnum } from '../consts'
-import { useWatchNode, useDrawer, useNodeName, useFormFields } from '../node'
-import HttpRequestParamSetting from './components/HttpRequestParamSetting.vue'
+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'
@@ -227,52 +288,153 @@ const configForm = ref<TriggerSetting>({
     body: [],
     response: []
   },
-  normalFormSetting: { updateFormFields: {} }
+  formSettings: [
+    {
+      conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+      updateFormFields: {},
+      deleteFields: []
+    }
+  ]
 })
 // 流程表单字段
 const formFields = useFormFields()
 
 // 可选的修改的表单字段
 const optionalUpdateFormFields = computed(() => {
-  const usedFields = Object.keys(configForm.value.normalFormSetting?.updateFormFields || {})
   return formFields.map((field) => ({
     title: field.title,
     field: field.field,
-    disabled: usedFields.includes(field.field)
+    disabled: false
   }))
 })
 
-const updateFormFieldKey = (oldKey: string, newKey: string) => {
-  if (!configForm.value.normalFormSetting?.updateFormFields) return
-  const value = configForm.value.normalFormSetting.updateFormFields[oldKey]
-  delete configForm.value.normalFormSetting.updateFormFields[oldKey]
-  configForm.value.normalFormSetting.updateFormFields[newKey] = value
+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
+  }
 }
 
-/** 添加 HTTP 请求返回值设置项*/
-const addHttpResponseSetting = (responseSetting: Record<string, string>[]) => {
-  responseSetting.push({
-    key: '',
-    value: ''
+/** 添加新的修改表单设置 */
+const addFormSetting = () => {
+  configForm.value.formSettings!.push({
+    conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+    updateFormFields: {},
+    deleteFields: []
   })
 }
 
-/** 删除 HTTP 请求返回值设置项 */
-const deleteHttpResponseSetting = (responseSetting: Record<string, string>[], index: number) => {
-  responseSetting.splice(index, 1)
+/** 删除修改表单设置 */
+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 = () => {
-  if (configForm.value.normalFormSetting!.updateFormFields === undefined) {
-    configForm.value.normalFormSetting!.updateFormFields = {}
+/** 添加修改字段设置项 */
+const addFormFieldSetting = (formSetting: FormTriggerSetting) => {
+  if (!formSetting) return
+  if (!formSetting.updateFormFields) {
+    formSetting.updateFormFields = {}
   }
-  configForm.value.normalFormSetting!.updateFormFields[''] = undefined
+  formSetting.updateFormFields[''] = undefined
 }
-/** 删除修改表单设置项 */
-const deleteFormFieldSetting = (key: string) => {
-  if (!configForm.value.normalFormSetting?.updateFormFields) return
-  delete configForm.value.normalFormSetting.updateFormFields[key]
+/** 更新字段 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]
 }
 
 /** 保存配置 */
@@ -285,10 +447,19 @@ const saveConfig = async () => {
   currentNode.value.name = nodeName.value!
   currentNode.value.showText = showText
   if (configForm.value.type === TriggerTypeEnum.HTTP_REQUEST) {
-    configForm.value.normalFormSetting = undefined
-  }
-  if (configForm.value.type === TriggerTypeEnum.UPDATE_NORMAL_FORM) {
+    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
@@ -298,15 +469,27 @@ const saveConfig = async () => {
 /** 获取节点展示内容 */
 const getShowText = (): string => {
   let showText = ''
-  if (configForm.value.type === TriggerTypeEnum.HTTP_REQUEST) {
+  if (
+    configForm.value.type === TriggerTypeEnum.HTTP_REQUEST ||
+    configForm.value.type === TriggerTypeEnum.HTTP_CALLBACK
+  ) {
     showText = `${configForm.value.httpRequestSetting?.url}`
-  } else if (configForm.value.type === TriggerTypeEnum.UPDATE_NORMAL_FORM) {
-    const updatefields = Object.keys(configForm.value.normalFormSetting?.updateFormFields || {})
-    if (updatefields.length === 0) {
-      message.warning('请设置修改表单字段')
-    } else {
-      showText = '修改表单数据'
+  } 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
 }
@@ -314,6 +497,7 @@ const getShowText = (): string => {
 /** 显示触发器节点配置, 由父组件传过来 */
 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,
@@ -323,7 +507,13 @@ const showTriggerNodeConfig = (node: SimpleFlowNode) => {
         body: [],
         response: []
       },
-      normalFormSetting: node.triggerSetting.normalFormSetting || { updateFormFields: {} }
+      formSettings: node.triggerSetting.formSettings || [
+        {
+          conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+          updateFormFields: {},
+          deleteFields: []
+        }
+      ]
     }
   }
 }

+ 198 - 123
src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue

@@ -25,7 +25,7 @@
         <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
@@ -39,10 +39,10 @@
       </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"
@@ -61,7 +61,13 @@
               label="指定角色"
               prop="roleIds"
             >
-              <el-select filterable 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 filterable 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 filterable 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 filterable 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"
@@ -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,93 +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 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>
-
-            <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
-                filterable
-                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">
@@ -348,30 +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>
 
-            <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 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>
 
-            <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 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">
@@ -407,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
@@ -448,7 +501,11 @@
         </div>
       </el-tab-pane>
       <el-tab-pane label="监听器" name="listener">
-        <UserTaskListener ref="userTaskListenerRef" v-model="configForm" :form-field-options="formFieldOptions" />
+        <UserTaskListener
+          ref="userTaskListenerRef"
+          v-model="configForm"
+          :form-field-options="formFieldOptions"
+        />
       </el-tab-pane>
     </el-tabs>
     <template #footer>
@@ -485,7 +542,8 @@ import {
   ASSIGN_EMPTY_HANDLER_TYPES,
   AssignEmptyHandlerType,
   FieldPermissionType,
-  ProcessVariableEnum
+  ProcessVariableEnum,
+  TRANSACTOR_DEFAULT_BUTTON_SETTING
 } from '../consts'
 
 import {
@@ -588,7 +646,7 @@ const {
   handleCandidateParam,
   parseCandidateParam,
   getShowText
-} = useNodeForm(NodeType.USER_TASK_NODE)
+} = useNodeForm(currentNode.value.type)
 const configForm = tempConfigForm as Ref<UserTaskFormType>
 
 // 改变审批人设置策略
@@ -627,7 +685,12 @@ const {
 
 const userTaskListenerRef = ref()
 
-// 保存配置
+/** 节点类型名称 */
+const nodeTypeName = computed(() => {
+  return currentNode.value.type === NodeType.TRANSACTOR_NODE ? '办理' : '审批'
+})
+
+/** 保存配置 */
 const saveConfig = async () => {
   // activeTabName.value = 'user'
   // 设置审批节点名称
@@ -713,7 +776,7 @@ const saveConfig = async () => {
   return true
 }
 
-// 显示审批节点配置, 由父组件传过来
+/** 显示审批节点配置, 由父组件传过来 */
 const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
   nodeName.value = node.name
   // 1 审批类型
@@ -733,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)
@@ -755,7 +818,11 @@ 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. 监听器
@@ -773,7 +840,7 @@ const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
     header: node.taskAssignListener?.header ?? [],
     body: node.taskAssignListener?.body ?? []
   }
- // 5.3 完成任务
+  // 5.3 完成任务
   configForm.value.taskCompleteListenerEnable = node.taskCompleteListener?.enable
   configForm.value.taskCompleteListenerPath = node.taskCompleteListener?.path
   configForm.value.taskCompleteListener = {
@@ -788,9 +855,7 @@ const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
 
 defineExpose({ openDrawer, showUserTaskNodeConfig }) // 暴露方法给父组件
 
-/**
- * @description 操作按钮设置
- */
+/** 操作按钮设置 */
 function useButtonsSetting() {
   const buttonsSetting = ref<ButtonSetting[]>()
   // 操作按钮显示名称可编辑
@@ -811,9 +876,7 @@ function useButtonsSetting() {
   }
 }
 
-/**
- * @description 审批人超时未处理配置
- */
+/** 审批人超时未处理配置 */
 function useTimeoutHandler() {
   // 时间单位
   const timeUnit = ref(TimeUnitType.HOUR)
@@ -896,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>

+ 15 - 3
src/components/SimpleProcessDesignerV2/src/nodes-config/components/Condition.vue

@@ -12,7 +12,10 @@
         </el-radio>
       </el-radio-group>
     </el-form-item>
-    <el-form-item v-if="condition.conditionType === ConditionType.RULE && condition.conditionGroups" label="条件规则">
+    <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>
@@ -67,14 +70,23 @@
                   trigger: 'change'
                 }"
               >
-                <el-select style="width: 160px" v-model="rule.leftSide">
+                <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>

+ 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>

+ 7 - 3
src/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestParamSetting.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-form-item label="请求头">
+  <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
@@ -69,7 +69,7 @@
       <Icon icon="ep:plus" class="mr-5px" />添加一行
     </el-button>
   </el-form-item>
-  <el-form-item label="请求体">
+  <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
@@ -141,7 +141,11 @@
   </el-form-item>
 </template>
 <script setup lang="ts">
-import { HttpRequestParam, BPM_HTTP_REQUEST_PARAM_TYPES, BpmHttpRequestParamTypeEnum } from '../../consts'
+import {
+  HttpRequestParam,
+  BPM_HTTP_REQUEST_PARAM_TYPES,
+  BpmHttpRequestParamTypeEnum
+} from '../../consts'
 import { useFormFieldsAndStartUser } from '../../node'
 defineOptions({
   name: 'HttpRequestParamSetting'

+ 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>

+ 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>

+ 9 - 2
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>

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


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


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


+ 34 - 2
src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss

@@ -177,6 +177,18 @@
     color: #ca3a31
   }
 
+  .transactor {
+    color: #330099;
+  }
+
+  .child-process {
+    color: #996633;
+  }
+
+  .async-child-process {
+    color: #006666;
+  }
+
   .handler-item-text {
     margin-top: 4px;
     width: 80px;
@@ -290,10 +302,22 @@
           &.trigger-node {
             color: #3373d2;
           }
-          
+
           &.router-node {
             color: #ca3a31
           }
+
+          &.transactor-task {
+            color: #330099;
+          }
+
+          &.child-process {
+            color: #996633;
+          }
+
+          &.async-child-process {
+            color: #006666;
+          }
         }
 
         .node-title {
@@ -777,7 +801,7 @@
   content: "\e7eb";
 }
 
-.icon-handle:before {
+.icon-transactor:before {
   content: "\e61c";
 }
 
@@ -792,3 +816,11 @@
 .icon-parallel:before {
   content: "\e688";
 }
+
+.icon-async-child-process:before {
+  content: "\e6f2";
+}
+
+.icon-child-process:before {
+  content: "\e6c1";
+}

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

@@ -56,7 +56,7 @@ export default defineComponent({
     // 注册
     onMounted(() => {
       const tableRef = unref(elTableRef)
-      emit('register', tableRef?.$parent, elTableRef)
+      emit('register', tableRef?.$parent, elTableRef.value)
     })
 
     const pageSizeRef = ref(props.pageSize)

+ 16 - 6
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,6 +306,18 @@ const props = defineProps({
   }
 })
 
+/**
+ * 代码高亮
+ */
+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
 const defaultZoom = ref(1)

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

@@ -123,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">
@@ -140,24 +146,30 @@
               :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>
@@ -165,12 +177,22 @@
 
     <el-divider content-position="left">是否需要签名</el-divider>
     <el-form-item prop="signEnable">
-      <el-switch v-model="signEnable.value" active-text="是" inactive-text="否" />
+      <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="非必填" />
+      <el-switch
+        v-model="reasonRequire.value"
+        active-text="必填"
+        inactive-text="非必填"
+        @change="updateElementExtensions"
+      />
     </el-form-item>
   </div>
 </template>
@@ -191,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({
@@ -248,7 +271,6 @@ const resetCustomConfigList = () => {
     bpmnElement.value.id,
     bpmnInstances().modeler
   )
-
   // 获取元素扩展属性 或者 创建扩展属性
   elExtensionElements.value =
     bpmnElement.value.businessObject?.extensionElements ??
@@ -311,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'
@@ -487,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 () => {
   // 获得用户列表
@@ -497,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;

+ 76 - 31
src/router/modules/remaining.ts

@@ -314,33 +314,9 @@ const remainingRouter: AppRouteRecordRaw[] = [
           activeMenu: '/bpm/manager/form'
         }
       },
-      {
-        path: 'manager/model/edit',
-        component: () => import('@/views/bpm/model/editor/index.vue'),
-        name: 'BpmModelEditor',
-        meta: {
-          noCache: true,
-          hidden: true,
-          canTo: true,
-          title: '设计流程',
-          activeMenu: '/bpm/manager/model'
-        }
-      },
-      {
-        path: 'manager/simple/model',
-        component: () => import('@/views/bpm/simple/SimpleModelDesign.vue'),
-        name: 'SimpleModelDesign',
-        meta: {
-          noCache: true,
-          hidden: true,
-          canTo: true,
-          title: '仿钉钉设计流程',
-          activeMenu: '/bpm/manager/model'
-        }
-      },
       {
         path: 'manager/definition',
-        component: () => import('@/views/bpm/definition/index.vue'),
+        component: () => import('@/views/bpm/model/definition/index.vue'),
         name: 'BpmProcessDefinition',
         meta: {
           noCache: true,
@@ -416,7 +392,6 @@ const remainingRouter: AppRouteRecordRaw[] = [
         }
       },
       {
-        // TODO @zws:1)建议,在加一个路由。然后标题是“复制流程”,这样体验会好点;2)复制出来的数据,在名字前面,加“副本 ”,和钉钉保持一致!
         path: 'manager/model/:type/:id',
         component: () => import('@/views/bpm/model/form/index.vue'),
         name: 'BpmModelUpdate',
@@ -693,6 +668,65 @@ const remainingRouter: AppRouteRecordRaw[] = [
           icon: 'ep:home-filled',
           noCache: false
         }
+      },
+      {
+        path: 'knowledge/document',
+        component: () => import('@/views/ai/knowledge/document/index.vue'),
+        name: 'AiKnowledgeDocument',
+        meta: {
+          title: '知识库文档',
+          icon: 'ep:document',
+          noCache: false,
+          activeMenu: '/ai/knowledge'
+        }
+      },
+      {
+        path: 'knowledge/document/create',
+        component: () => import('@/views/ai/knowledge/document/form/index.vue'),
+        name: 'AiKnowledgeDocumentCreate',
+        meta: {
+          title: '创建文档',
+          icon: 'ep:plus',
+          noCache: true,
+          hidden: true,
+          activeMenu: '/ai/knowledge'
+        }
+      },
+      {
+        path: 'knowledge/document/update',
+        component: () => import('@/views/ai/knowledge/document/form/index.vue'),
+        name: 'AiKnowledgeDocumentUpdate',
+        meta: {
+          title: '修改文档',
+          icon: 'ep:edit',
+          noCache: true,
+          hidden: true,
+          activeMenu: '/ai/knowledge'
+        }
+      },
+      {
+        path: 'knowledge/retrieval',
+        component: () => import('@/views/ai/knowledge/knowledge/retrieval/index.vue'),
+        name: 'AiKnowledgeRetrieval',
+        meta: {
+          title: '文档召回测试',
+          icon: 'ep:search',
+          noCache: true,
+          hidden: true,
+          activeMenu: '/ai/knowledge'
+        }
+      },
+      {
+        path: 'knowledge/segment',
+        component: () => import('@/views/ai/knowledge/segment/index.vue'),
+        name: 'AiKnowledgeSegment',
+        meta: {
+          title: '知识库分段',
+          icon: 'ep:tickets',
+          noCache: true,
+          hidden: true,
+          activeMenu: '/ai/knowledge'
+        }
       }
     ]
   },
@@ -715,15 +749,15 @@ const remainingRouter: AppRouteRecordRaw[] = [
     },
     children: [
       {
-        path: 'product/detail/:id',
+        path: 'product/product/detail/:id',
         name: 'IoTProductDetail',
         meta: {
           title: '产品详情',
           noCache: true,
           hidden: true,
-          activeMenu: '/iot/product'
+          activeMenu: '/iot/device/product'
         },
-        component: () => import('@/views/iot/product/detail/index.vue')
+        component: () => import('@/views/iot/product/product/detail/index.vue')
       },
       {
         path: 'device/detail/:id',
@@ -732,9 +766,20 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: '设备详情',
           noCache: true,
           hidden: true,
-          activeMenu: '/iot/device'
+          activeMenu: '/iot/device/device'
+        },
+        component: () => import('@/views/iot/device/device/detail/index.vue')
+      },
+      {
+        path: 'plugin/detail/:id',
+        name: 'IoTPluginDetail',
+        meta: {
+          title: '插件详情',
+          noCache: true,
+          hidden: true,
+          activeMenu: '/iot/plugin'
         },
-        component: () => import('@/views/iot/device/detail/index.vue')
+        component: () => import('@/views/iot/plugin/detail/index.vue')
       }
     ]
   }

+ 10 - 4
src/utils/dict.ts

@@ -224,6 +224,7 @@ export enum DICT_TYPE {
 
   // ========== AI - 人工智能模块  ==========
   AI_PLATFORM = 'ai_platform', // AI 平台
+  AI_MODEL_TYPE = 'ai_model_type', // AI 模型类型
   AI_IMAGE_STATUS = 'ai_image_status', // AI 图片状态
   AI_MUSIC_STATUS = 'ai_music_status', // AI 音乐状态
   AI_GENERATE_MODE = 'ai_generate_mode', // AI 生成模式
@@ -240,9 +241,14 @@ export enum DICT_TYPE {
   IOT_PRODUCT_DEVICE_TYPE = 'iot_product_device_type', // IOT 产品设备类型
   IOT_DATA_FORMAT = 'iot_data_format', // IOT 数据格式
   IOT_PROTOCOL_TYPE = 'iot_protocol_type', // IOT 接入网关协议
-  IOT_DEVICE_STATUS = 'iot_device_status', // IOT 设备状态
-  IOT_PRODUCT_FUNCTION_TYPE = 'iot_product_function_type', // IOT 产品功能类型
+  IOT_DEVICE_STATE = 'iot_device_state', // IOT 设备状态
+  IOT_THING_MODEL_TYPE = 'iot_thing_model_type', // IOT 产品功能类型
   IOT_DATA_TYPE = 'iot_data_type', // IOT 数据类型
-  IOT_UNIT_TYPE = 'iot_unit_type', // IOT 单位类型
-  IOT_RW_TYPE = 'iot_rw_type' // IOT 读写类型
+  IOT_THING_MODEL_UNIT = 'iot_thing_model_unit', // IOT 物模型单位
+  IOT_RW_TYPE = 'iot_rw_type', // IOT 读写类型
+  IOT_PLUGIN_DEPLOY_TYPE = 'iot_plugin_deploy_type', // IOT 插件部署类型
+  IOT_PLUGIN_STATUS = 'iot_plugin_status', // IOT 插件状态
+  IOT_PLUGIN_TYPE = 'iot_plugin_type', // IOT 插件类型
+  IOT_DATA_BRIDGE_DIRECTION_ENUM = 'iot_data_bridge_direction_enum', // 桥梁方向
+  IOT_DATA_BRIDGE_TYPE_ENUM = 'iot_data_bridge_type_enum' // 桥梁类型
 }

+ 1 - 1
src/utils/formCreate.ts

@@ -11,7 +11,7 @@ export const encodeConf = (designerRef: object) => {
 // 编码表单 Fields
 export const encodeFields = (designerRef: object) => {
   // @ts-ignore
-  const rule = designerRef.value.getRule()
+  const rule = JSON.parse(designerRef.value.getJson())
   const fields: string[] = []
   rule.forEach((item) => {
     fields.push(JSON.stringify(item))

+ 80 - 8
src/utils/index.ts

@@ -116,6 +116,78 @@ export function toAnyString() {
   return str
 }
 
+/**
+ * 生成指定长度的随机字符串
+ *
+ * @param length 字符串长度
+ */
+export function generateRandomStr(length: number): string {
+  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
+  let result = ''
+  for (let i = 0; i < length; i++) {
+    result += chars.charAt(Math.floor(Math.random() * chars.length))
+  }
+  return result
+}
+
+/**
+ * 根据支持的文件类型生成 accept 属性值
+ *
+ * @param supportedFileTypes 支持的文件类型数组,如 ['PDF', 'DOC', 'DOCX']
+ * @returns 用于文件上传组件 accept 属性的字符串
+ */
+export const generateAcceptedFileTypes = (supportedFileTypes: string[]): string => {
+  const allowedExtensions = supportedFileTypes.map((ext) => ext.toLowerCase())
+  const mimeTypes: string[] = []
+
+  // 添加常见的 MIME 类型映射
+  if (allowedExtensions.includes('txt')) {
+    mimeTypes.push('text/plain')
+  }
+  if (allowedExtensions.includes('pdf')) {
+    mimeTypes.push('application/pdf')
+  }
+  if (allowedExtensions.includes('html') || allowedExtensions.includes('htm')) {
+    mimeTypes.push('text/html')
+  }
+  if (allowedExtensions.includes('csv')) {
+    mimeTypes.push('text/csv')
+  }
+  if (allowedExtensions.includes('xlsx') || allowedExtensions.includes('xls')) {
+    mimeTypes.push('application/vnd.ms-excel')
+    mimeTypes.push('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
+  }
+  if (allowedExtensions.includes('docx') || allowedExtensions.includes('doc')) {
+    mimeTypes.push('application/msword')
+    mimeTypes.push('application/vnd.openxmlformats-officedocument.wordprocessingml.document')
+  }
+  if (allowedExtensions.includes('pptx') || allowedExtensions.includes('ppt')) {
+    mimeTypes.push('application/vnd.ms-powerpoint')
+    mimeTypes.push('application/vnd.openxmlformats-officedocument.presentationml.presentation')
+  }
+  if (allowedExtensions.includes('xml')) {
+    mimeTypes.push('application/xml')
+    mimeTypes.push('text/xml')
+  }
+  if (allowedExtensions.includes('md') || allowedExtensions.includes('markdown')) {
+    mimeTypes.push('text/markdown')
+  }
+  if (allowedExtensions.includes('epub')) {
+    mimeTypes.push('application/epub+zip')
+  }
+  if (allowedExtensions.includes('eml')) {
+    mimeTypes.push('message/rfc822')
+  }
+  if (allowedExtensions.includes('msg')) {
+    mimeTypes.push('application/vnd.ms-outlook')
+  }
+
+  // 添加文件扩展名
+  const extensions = allowedExtensions.map((ext) => `.${ext}`)
+
+  return [...mimeTypes, ...extensions].join(',')
+}
+
 /**
  * 首字母大写
  */
@@ -445,7 +517,7 @@ export function jsonParse(str: string) {
   try {
     return JSON.parse(str)
   } catch (e) {
-    console.error(`str[${str}] 不是一个 JSON 字符串`)
+    console.log(`str[${str}] 不是一个 JSON 字符串`)
     return ''
   }
 }
@@ -453,14 +525,14 @@ export function jsonParse(str: string) {
 /**
  * 截取字符串
  *
- * @param name
- * @param start
- * @param end
+ * @param str 字符串
+ * @param start 开始位置
+ * @param end 结束位置
  */
 
-export const sliceName = (name: string,start: number, end : number) => {
-  if (name.length > end) {
-    return name.slice(start, end)
+export const subString = (str: string, start: number, end: number) => {
+  if (str.length > end) {
+    return str.slice(start, end)
   }
-  return name
+  return str
 }

+ 3 - 0
src/utils/routerHelper.ts

@@ -100,6 +100,9 @@ export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecord
     //处理顶级非目录路由
     if (!route.children && route.parentId == 0 && route.component) {
       data.component = Layout
+      data.meta = {
+        hidden: meta.hidden,
+      }
       data.name = toCamelCase(route.path, true) + 'Parent'
       data.redirect = ''
       meta.alwaysShow = true

+ 13 - 10
src/views/ai/chat/index/components/conversation/ConversationUpdateForm.vue

@@ -11,17 +11,17 @@
         <el-input
           type="textarea"
           v-model="formData.systemMessage"
-          rows="4"
+          :rows="4"
           placeholder="请输入角色设定"
         />
       </el-form-item>
       <el-form-item label="模型" prop="modelId">
         <el-select v-model="formData.modelId" placeholder="请选择模型">
           <el-option
-            v-for="chatModel in chatModelList"
-            :key="chatModel.id"
-            :label="chatModel.name"
-            :value="chatModel.id"
+            v-for="model in models"
+            :key="model.id"
+            :label="model.name"
+            :value="model.id"
           />
         </el-select>
       </el-form-item>
@@ -32,6 +32,7 @@
           :min="0"
           :max="2"
           :precision="2"
+          class="!w-1/1"
         />
       </el-form-item>
       <el-form-item label="回复数 Token 数" prop="maxTokens">
@@ -39,7 +40,8 @@
           v-model="formData.maxTokens"
           placeholder="请输入回复数 Token 数"
           :min="0"
-          :max="4096"
+          :max="8192"
+          class="!w-1/1"
         />
       </el-form-item>
       <el-form-item label="上下文数量" prop="maxContexts">
@@ -48,6 +50,7 @@
           placeholder="请输入上下文数量"
           :min="0"
           :max="20"
+          class="!w-1/1"
         />
       </el-form-item>
     </el-form>
@@ -58,9 +61,9 @@
   </Dialog>
 </template>
 <script setup lang="ts">
-import { CommonStatusEnum } from '@/utils/constants'
-import { ChatModelApi, ChatModelVO } from '@/api/ai/model/chatModel'
+import { ModelApi, ModelVO } from '@/api/ai/model/model'
 import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
+import { AiModelTypeEnum } from '@/views/ai/utils/constants'
 
 /** AI 聊天对话的更新表单 */
 defineOptions({ name: 'ChatConversationUpdateForm' })
@@ -85,7 +88,7 @@ const formRules = reactive({
   maxContexts: [{ required: true, message: '上下文数量不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
-const chatModelList = ref([] as ChatModelVO[]) // 聊天模型列表
+const models = ref([] as ModelVO[]) // 聊天模型列表
 
 /** 打开弹窗 */
 const open = async (id: number) => {
@@ -107,7 +110,7 @@ const open = async (id: number) => {
     }
   }
   // 获得下拉数据
-  chatModelList.value = await ChatModelApi.getChatModelSimpleList(CommonStatusEnum.ENABLE)
+  models.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.CHAT)
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 

+ 104 - 0
src/views/ai/chat/index/components/message/MessageKnowledge.vue

@@ -0,0 +1,104 @@
+<!-- 知识引用组件 -->
+<template>
+  <!-- 知识引用列表 -->
+  <div v-if="segments && segments.length > 0" class="mt-10px p-10px rounded-8px bg-[#f5f5f5]">
+    <div class="text-14px text-[#666] mb-8px flex items-center">
+      <Icon icon="ep:document" class="mr-5px" /> 知识引用
+    </div>
+    <div class="flex flex-wrap gap-8px">
+      <div
+        v-for="(doc, index) in documentList"
+        :key="index"
+        class="p-8px px-12px bg-white rounded-6px cursor-pointer transition-all hover:bg-[#e6f4ff]"
+        @click="handleClick(doc)"
+      >
+        <div class="text-14px text-[#333] mb-4px">
+          {{ doc.title }}
+          <span class="text-12px text-[#999] ml-4px">({{ doc.segments.length }} 条)</span>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <!-- 知识引用详情弹窗 -->
+  <el-popover
+    v-model:visible="dialogVisible"
+    :width="600"
+    trigger="click"
+    placement="top-start"
+    :offset="55"
+    popper-class="knowledge-popover"
+  >
+    <template #reference>
+      <div ref="documentRef"></div>
+    </template>
+    <template #default>
+      <div class="text-16px font-bold mb-12px">{{ document?.title }}</div>
+      <div class="max-h-[60vh] overflow-y-auto">
+        <div
+          v-for="(segment, index) in document?.segments"
+          :key="index"
+          class="p-12px border-b-solid border-b-[#eee] last:border-b-0"
+        >
+          <div
+            class="block mb-8px px-8px py-2px bg-[#f5f5f5] rounded-4px text-12px text-[#666] w-fit"
+          >
+            分段 {{ segment.id }}
+          </div>
+          <div class="text-14px leading-[1.6] text-[#333] mt-[10px]">
+            {{ segment.content }}
+          </div>
+        </div>
+      </div>
+    </template>
+  </el-popover>
+</template>
+
+<script setup lang="ts">
+const props = defineProps<{
+  segments: {
+    id: number
+    documentId: number
+    documentName: string
+    content: string
+  }[]
+}>()
+
+const document = ref<{
+  id: number
+  title: string
+  segments: {
+    id: number
+    content: string
+  }[]
+} | null>(null) // 知识库文档列表
+const dialogVisible = ref(false) // 知识引用详情弹窗
+const documentRef = ref<HTMLElement>() // 知识引用详情弹窗 Ref
+
+/** 按照 document 聚合 segments */
+const documentList = computed(() => {
+  if (!props.segments) return []
+
+  const docMap = new Map()
+  props.segments.forEach((segment) => {
+    if (!docMap.has(segment.documentId)) {
+      docMap.set(segment.documentId, {
+        id: segment.documentId,
+        title: segment.documentName,
+        segments: []
+      })
+    }
+    docMap.get(segment.documentId).segments.push({
+      id: segment.id,
+      content: segment.content
+    })
+  })
+  return Array.from(docMap.values())
+})
+
+/** 点击 document 处理 */
+const handleClick = (doc: any) => {
+  document.value = doc
+  dialogVisible.value = true
+}
+</script>

+ 2 - 0
src/views/ai/chat/index/components/message/MessageList.vue

@@ -12,6 +12,7 @@
           </div>
           <div class="left-text-container" ref="markdownViewRef">
             <MarkdownView class="left-text" :content="item.content" />
+            <MessageKnowledge v-if="item.segments" :segments="item.segments" />
           </div>
           <div class="left-btns">
             <el-button class="btn-cus" link @click="copyContent(item.content)">
@@ -62,6 +63,7 @@
 import { PropType } from 'vue'
 import { formatDate } from '@/utils/formatTime'
 import MarkdownView from '@/components/MarkdownView/index.vue'
+import MessageKnowledge from './MessageKnowledge.vue'
 import { useClipboard } from '@vueuse/core'
 import { ArrowDownBold, Edit, RefreshRight } from '@element-plus/icons-vue'
 import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message'

+ 3 - 3
src/views/ai/chat/index/components/role/RoleList.vue

@@ -41,9 +41,9 @@
 </template>
 
 <script setup lang="ts">
-import {ChatRoleVO} from '@/api/ai/model/chatRole'
-import {PropType, ref} from 'vue'
-import {More} from '@element-plus/icons-vue'
+import { ChatRoleVO } from '@/api/ai/model/chatRole'
+import { PropType, ref } from 'vue'
+import { More } from '@element-plus/icons-vue'
 
 const tabsRef = ref<any>() // tabs ref
 

+ 2 - 0
src/views/ai/chat/manager/index.vue

@@ -1,4 +1,6 @@
 <template>
+  <doc-alert title="AI 对话聊天" url="https://doc.iocoder.cn/ai/chat/" />
+
   <ContentWrap>
     <el-tabs>
       <el-tab-pane label="对话列表">

+ 48 - 40
src/views/ai/image/index/components/other/index.vue → src/views/ai/image/index/components/common/index.vue

@@ -2,11 +2,11 @@
 <template>
   <div class="prompt">
     <el-text tag="b">画面描述</el-text>
-    <el-text tag="p">建议使用“形容词+动词+风格”的格式,使用“,”隔开</el-text>
+    <el-text tag="p">建议使用“形容词 + 动词 + 风格”的格式,使用“,”隔开</el-text>
     <el-input
       v-model="prompt"
       maxlength="1024"
-      rows="5"
+      :rows="5"
       class="w-100% mt-15px"
       input-style="border-radius: 7px;"
       placeholder="例如:童话里的小屋应该是什么样子?"
@@ -57,8 +57,13 @@
       <el-text tag="b">模型</el-text>
     </div>
     <el-space wrap class="group-item-body">
-      <el-select v-model="model" placeholder="Select" size="large" class="!w-350px">
-        <el-option v-for="item in models" :key="item.key" :label="item.name" :value="item.key" />
+      <el-select v-model="modelId" placeholder="Select" size="large" class="!w-350px">
+        <el-option
+          v-for="item in platformModels"
+          :key="item.id"
+          :label="item.name"
+          :value="item.id"
+        />
       </el-select>
     </el-space>
   </div>
@@ -72,25 +77,34 @@
     </el-space>
   </div>
   <div class="btns">
-    <el-button type="primary" size="large" round :loading="drawIn" @click="handleGenerateImage">
+    <el-button
+      type="primary"
+      size="large"
+      round
+      :loading="drawIn"
+      :disabled="prompt.length === 0"
+      @click="handleGenerateImage"
+    >
       {{ drawIn ? '生成中' : '生成内容' }}
     </el-button>
   </div>
 </template>
 <script setup lang="ts">
 import { ImageApi, ImageDrawReqVO, ImageVO } from '@/api/ai/image'
-import {
-  AiPlatformEnum,
-  ChatGlmModels,
-  ImageHotWords,
-  ImageModelVO,
-  OtherPlatformEnum,
-  QianFanModels,
-  TongYiWanXiangModels
-} from '@/views/ai/utils/constants'
+import { AiPlatformEnum, ImageHotWords, OtherPlatformEnum } from '@/views/ai/utils/constants'
+import { ModelVO } from '@/api/ai/model/model'
 
 const message = useMessage() // 消息弹窗
 
+// 接收父组件传入的模型列表
+const props = defineProps({
+  models: {
+    type: Array<ModelVO>,
+    default: () => [] as ModelVO[]
+  }
+})
+const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
+
 // 定义属性
 const drawIn = ref<boolean>(false) // 生成中
 const selectHotWord = ref<string>('') // 选中的热词
@@ -99,10 +113,8 @@ const prompt = ref<string>('') // 提示词
 const width = ref<number>(512) // 图片宽度
 const height = ref<number>(512) // 图片高度
 const otherPlatform = ref<string>(AiPlatformEnum.TONG_YI) // 平台
-const models = ref<ImageModelVO[]>(TongYiWanXiangModels) // 模型  TongYiWanXiangModels、QianFanModels
-const model = ref<string>(models.value[0].key) // 模型
-
-const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
+const platformModels = ref<ModelVO[]>([]) // 模型列表
+const modelId = ref<number>() // 选中的模型
 
 /** 选择热词 */
 const handleHotWordClick = async (hotWord: string) => {
@@ -125,11 +137,11 @@ const handleGenerateImage = async () => {
     // 加载中
     drawIn.value = true
     // 回调
-    emits('onDrawStart', AiPlatformEnum.STABLE_DIFFUSION)
+    emits('onDrawStart', otherPlatform.value)
     // 发送请求
     const form = {
       platform: otherPlatform.value,
-      model: model.value, // 模型
+      modelId: modelId.value, // 模型
       prompt: prompt.value, // 提示词
       width: width.value, // 图片宽度
       height: height.value, // 图片高度
@@ -138,7 +150,7 @@ const handleGenerateImage = async () => {
     await ImageApi.drawImage(form)
   } finally {
     // 回调
-    emits('onDrawComplete', AiPlatformEnum.STABLE_DIFFUSION)
+    emits('onDrawComplete', otherPlatform.value)
     // 加载结束
     drawIn.value = false
   }
@@ -153,33 +165,29 @@ const settingValues = async (detail: ImageVO) => {
 
 /** 平台切换 */
 const handlerPlatformChange = async (platform: string) => {
-  // 切换平台,切换模型、风格
-  if (AiPlatformEnum.TONG_YI === platform) {
-    models.value = TongYiWanXiangModels
-  } else if (AiPlatformEnum.YI_YAN === platform) {
-    models.value = QianFanModels
-  } else if (AiPlatformEnum.ZHI_PU === platform) {
-    models.value = ChatGlmModels
-  } else {
-    models.value = []
-  }
-  // 切换平台,默认选择一个风格
-  if (models.value.length > 0) {
-    model.value = models.value[0].key
+  // 根据选择的平台筛选模型
+  platformModels.value = props.models.filter((item: ModelVO) => item.platform === platform)
+
+  // 切换平台,默认选择一个模型
+  if (platformModels.value.length > 0) {
+    modelId.value = platformModels.value[0].id // 使用 model 属性作为值
   } else {
-    model.value = ''
+    modelId.value = undefined
   }
 }
 
+/** 监听 models 变化 */
+watch(
+  () => props.models,
+  () => {
+    handlerPlatformChange(otherPlatform.value)
+  },
+  { immediate: true, deep: true }
+)
 /** 暴露组件方法 */
 defineExpose({ settingValues })
 </script>
 <style scoped lang="scss">
-// 提示词
-.prompt {
-}
-
-// 热词
 .hot-words {
   display: flex;
   flex-direction: column;

+ 54 - 11
src/views/ai/image/index/components/dall3/index.vue

@@ -2,11 +2,11 @@
 <template>
   <div class="prompt">
     <el-text tag="b">画面描述</el-text>
-    <el-text tag="p">建议使用“形容词+动词+风格”的格式,使用“,”隔开</el-text>
+    <el-text tag="p">建议使用"形容词 + 动词 + 风格"的格式,使用","隔开</el-text>
     <el-input
       v-model="prompt"
       maxlength="1024"
-      rows="5"
+      :rows="5"
       class="w-100% mt-15px"
       input-style="border-radius: 7px;"
       placeholder="例如:童话里的小屋应该是什么样子?"
@@ -82,7 +82,14 @@
     </el-space>
   </div>
   <div class="btns">
-    <el-button type="primary" size="large" round :loading="drawIn" @click="handleGenerateImage">
+    <el-button
+      type="primary"
+      size="large"
+      round
+      :loading="drawIn"
+      :disabled="prompt.length === 0"
+      @click="handleGenerateImage"
+    >
       {{ drawIn ? '生成中' : '生成内容' }}
     </el-button>
   </div>
@@ -95,11 +102,22 @@ import {
   ImageHotWords,
   Dall3SizeList,
   ImageModelVO,
-  AiPlatformEnum
+  AiPlatformEnum,
+  ImageSizeVO
 } from '@/views/ai/utils/constants'
+import { ModelVO } from '@/api/ai/model/model'
 
 const message = useMessage() // 消息弹窗
 
+// 接收父组件传入的模型列表
+const props = defineProps({
+  models: {
+    type: Array<ModelVO>,
+    default: () => [] as ModelVO[]
+  }
+})
+const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
+
 // 定义属性
 const prompt = ref<string>('') // 提示词
 const drawIn = ref<boolean>(false) // 生成中
@@ -108,8 +126,6 @@ const selectModel = ref<string>('dall-e-3') // 模型
 const selectSize = ref<string>('1024x1024') // 选中 size
 const style = ref<string>('vivid') // style 样式
 
-const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
-
 /** 选择热词 */
 const handleHotWordClick = async (hotWord: string) => {
   // 情况一:取消选中
@@ -126,6 +142,27 @@ const handleHotWordClick = async (hotWord: string) => {
 /** 选择 model 模型 */
 const handleModelClick = async (model: ImageModelVO) => {
   selectModel.value = model.key
+  // 可以在这里添加模型特定的处理逻辑
+  // 例如,如果未来需要根据不同模型设置不同参数
+  if (model.key === 'dall-e-3') {
+    // DALL-E-3 模型特定的处理
+    style.value = 'vivid' // 默认设置vivid风格
+  } else if (model.key === 'dall-e-2') {
+    // DALL-E-2 模型特定的处理
+    style.value = 'natural' // 如果有其他DALL-E-2适合的默认风格
+  }
+
+  // 更新其他相关参数
+  // 例如可以默认选择最适合当前模型的尺寸
+  const recommendedSize = Dall3SizeList.find(
+    (size) =>
+      (model.key === 'dall-e-3' && size.key === '1024x1024') ||
+      (model.key === 'dall-e-2' && size.key === '512x512')
+  )
+
+  if (recommendedSize) {
+    selectSize.value = recommendedSize.key
+  }
 }
 
 /** 选择 style 样式  */
@@ -140,6 +177,15 @@ const handleSizeClick = async (imageSize: ImageSizeVO) => {
 
 /**  图片生产  */
 const handleGenerateImage = async () => {
+  // 从 models 中查找匹配的模型
+  const matchedModel = props.models.find(
+    (item) => item.model === selectModel.value && item.platform === AiPlatformEnum.OPENAI
+  )
+  if (!matchedModel) {
+    message.error('该模型不可用,请选择其它模型')
+    return
+  }
+
   // 二次确认
   await message.confirm(`确认生成内容?`)
   try {
@@ -151,7 +197,8 @@ const handleGenerateImage = async () => {
     const form = {
       platform: AiPlatformEnum.OPENAI,
       prompt: prompt.value, // 提示词
-      model: selectModel.value, // 模型
+      modelId: matchedModel.id, // 使用匹配到的模型
+      style: style.value, // 图像生成的风格
       width: imageSize.width, // size 不能为空
       height: imageSize.height, // size 不能为空
       options: {
@@ -183,10 +230,6 @@ const settingValues = async (detail: ImageVO) => {
 defineExpose({ settingValues })
 </script>
 <style scoped lang="scss">
-// 提示词
-.prompt {
-}
-
 // 热词
 .hot-words {
   display: flex;

+ 28 - 4
src/views/ai/image/index/components/midjourney/index.vue

@@ -6,7 +6,7 @@
     <el-input
       v-model="prompt"
       maxlength="1024"
-      rows="5"
+      :rows="5"
       class="w-100% mt-15px"
       input-style="border-radius: 7px;"
       placeholder="例如:童话里的小屋应该是什么样子?"
@@ -95,7 +95,13 @@
     </el-space>
   </div>
   <div class="btns">
-    <el-button type="primary" size="large" round @click="handleGenerateImage">
+    <el-button
+      type="primary"
+      size="large"
+      round
+      :disabled="prompt.length === 0"
+      @click="handleGenerateImage"
+    >
       {{ drawIn ? '生成中' : '生成内容' }}
     </el-button>
   </div>
@@ -112,9 +118,19 @@ import {
   MidjourneyVersions,
   NijiVersionList
 } from '@/views/ai/utils/constants'
+import { ModelVO } from '@/api/ai/model/model'
 
 const message = useMessage() // 消息弹窗
 
+// 接收父组件传入的模型列表
+const props = defineProps({
+  models: {
+    type: Array<ModelVO>,
+    default: () => [] as ModelVO[]
+  }
+})
+const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
+
 // 定义属性
 const drawIn = ref<boolean>(false) // 生成中
 const selectHotWord = ref<string>('') // 选中的热词
@@ -125,7 +141,6 @@ const selectModel = ref<string>('midjourney') // 选中的模型
 const selectSize = ref<string>('1:1') // 选中 size
 const selectVersion = ref<any>('6.0') // 选中的 version
 const versionList = ref<any>(MidjourneyVersions) // version 列表
-const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
 
 /** 选择热词 */
 const handleHotWordClick = async (hotWord: string) => {
@@ -158,6 +173,15 @@ const handleModelClick = async (model: ImageModelVO) => {
 
 /** 图片生成 */
 const handleGenerateImage = async () => {
+  // 从 models 中查找匹配的模型
+  const matchedModel = props.models.find(
+    (item) => item.model === selectModel.value && item.platform === AiPlatformEnum.MIDJOURNEY
+  )
+  if (!matchedModel) {
+    message.error('该模型不可用,请选择其它模型')
+    return
+  }
+
   // 二次确认
   await message.confirm(`确认生成内容?`)
   try {
@@ -171,7 +195,7 @@ const handleGenerateImage = async () => {
     ) as ImageSizeVO
     const req = {
       prompt: prompt.value,
-      model: selectModel.value,
+      modelId: matchedModel.id,
       width: imageSize.width,
       height: imageSize.height,
       version: selectVersion.value,

+ 31 - 7
src/views/ai/image/index/components/stableDiffusion/index.vue

@@ -2,11 +2,11 @@
 <template>
   <div class="prompt">
     <el-text tag="b">画面描述</el-text>
-    <el-text tag="p">建议使用“形容词+动词+风格”的格式,使用“,”隔开</el-text>
+    <el-text tag="p">建议使用“形容词 + 动词 + 风格”的格式,使用“,”隔开</el-text>
     <el-input
       v-model="prompt"
       maxlength="1024"
-      rows="5"
+      :rows="5"
       class="w-100% mt-15px"
       input-style="border-radius: 7px;"
       placeholder="例如:童话里的小屋应该是什么样子?"
@@ -128,7 +128,14 @@
     </el-space>
   </div>
   <div class="btns">
-    <el-button type="primary" size="large" round :loading="drawIn" @click="handleGenerateImage">
+    <el-button
+      type="primary"
+      size="large"
+      round
+      :loading="drawIn"
+      :disabled="prompt.length === 0"
+      @click="handleGenerateImage"
+    >
       {{ drawIn ? '生成中' : '生成内容' }}
     </el-button>
   </div>
@@ -143,9 +150,19 @@ import {
   StableDiffusionSamplers,
   StableDiffusionStylePresets
 } from '@/views/ai/utils/constants'
+import { ModelVO } from '@/api/ai/model/model'
 
 const message = useMessage() // 消息弹窗
 
+// 接收父组件传入的模型列表
+const props = defineProps({
+  models: {
+    type: Array<ModelVO>,
+    default: () => [] as ModelVO[]
+  }
+})
+const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
+
 // 定义属性
 const drawIn = ref<boolean>(false) // 生成中
 const selectHotWord = ref<string>('') // 选中的热词
@@ -160,8 +177,6 @@ const scale = ref<number>(7.5) // 引导系数
 const clipGuidancePreset = ref<string>('NONE') // 文本提示相匹配的图像(clip_guidance_preset) 简称 CLIP
 const stylePreset = ref<string>('3d-model') // 风格
 
-const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
-
 /** 选择热词 */
 const handleHotWordClick = async (hotWord: string) => {
   // 情况一:取消选中
@@ -177,6 +192,16 @@ const handleHotWordClick = async (hotWord: string) => {
 
 /** 图片生成 */
 const handleGenerateImage = async () => {
+  // 从 models 中查找匹配的模型
+  const selectModel = 'stable-diffusion-v1-6'
+  const matchedModel = props.models.find(
+    (item) => item.model === selectModel && item.platform === AiPlatformEnum.STABLE_DIFFUSION
+  )
+  if (!matchedModel) {
+    message.error('该模型不可用,请选择其它模型')
+    return
+  }
+
   // 二次确认
   if (hasChinese(prompt.value)) {
     message.alert('暂不支持中文!')
@@ -191,8 +216,7 @@ const handleGenerateImage = async () => {
     emits('onDrawStart', AiPlatformEnum.STABLE_DIFFUSION)
     // 发送请求
     const form = {
-      platform: AiPlatformEnum.STABLE_DIFFUSION,
-      model: 'stable-diffusion-v1-6',
+      modelId: matchedModel.id,
       prompt: prompt.value, // 提示词
       width: width.value, // 图片宽度
       height: height.value, // 图片高度

+ 33 - 19
src/views/ai/image/index/index.vue

@@ -6,21 +6,28 @@
         <el-segmented v-model="selectPlatform" :options="platformOptions" />
       </div>
       <div class="modal-switch-container">
+        <Common
+          v-if="selectPlatform === 'common'"
+          ref="commonRef"
+          :models="models"
+          @on-draw-complete="handleDrawComplete"
+        />
         <Dall3
           v-if="selectPlatform === AiPlatformEnum.OPENAI"
           ref="dall3Ref"
+          :models="models"
           @on-draw-start="handleDrawStart"
           @on-draw-complete="handleDrawComplete"
         />
-        <Midjourney v-if="selectPlatform === AiPlatformEnum.MIDJOURNEY" ref="midjourneyRef" />
+        <Midjourney
+          v-if="selectPlatform === AiPlatformEnum.MIDJOURNEY"
+          ref="midjourneyRef"
+          :models="models"
+        />
         <StableDiffusion
           v-if="selectPlatform === AiPlatformEnum.STABLE_DIFFUSION"
           ref="stableDiffusionRef"
-          @on-draw-complete="handleDrawComplete"
-        />
-        <Other
-          v-if="selectPlatform === 'other'"
-          ref="otherRef"
+          :models="models"
           @on-draw-complete="handleDrawComplete"
         />
       </div>
@@ -38,17 +45,23 @@ import { ImageVO } from '@/api/ai/image'
 import Dall3 from './components/dall3/index.vue'
 import Midjourney from './components/midjourney/index.vue'
 import StableDiffusion from './components/stableDiffusion/index.vue'
-import Other from './components/other/index.vue'
+import Common from './components/common/index.vue'
+import { ModelApi, ModelVO } from '@/api/ai/model/model'
+import { AiModelTypeEnum } from '@/views/ai/utils/constants'
 
 const imageListRef = ref<any>() // image 列表 ref
 const dall3Ref = ref<any>() // dall3(openai) ref
 const midjourneyRef = ref<any>() // midjourney ref
 const stableDiffusionRef = ref<any>() // stable diffusion ref
-const otherRef = ref<any>() // stable diffusion ref
+const commonRef = ref<any>() // stable diffusion ref
 
 // 定义属性
-const selectPlatform = ref(AiPlatformEnum.MIDJOURNEY)
+const selectPlatform = ref('common') // 选中的平台
 const platformOptions = [
+  {
+    label: '通用',
+    value: 'common'
+  },
   {
     label: 'DALL3 绘画',
     value: AiPlatformEnum.OPENAI
@@ -58,15 +71,13 @@ const platformOptions = [
     value: AiPlatformEnum.MIDJOURNEY
   },
   {
-    label: 'Stable Diffusion',
+    label: 'SD 绘图',
     value: AiPlatformEnum.STABLE_DIFFUSION
-  },
-  {
-    label: '其它',
-    value: 'other'
   }
 ]
 
+const models = ref<ModelVO[]>([]) // 模型列表
+
 /** 绘画 start  */
 const handleDrawStart = async (platform: string) => {}
 
@@ -75,7 +86,7 @@ const handleDrawComplete = async (platform: string) => {
   await imageListRef.value.getImageList()
 }
 
-/**  重新生成:将画图详情填充到对应平台  */
+/** 重新生成:将画图详情填充到对应平台 */
 const handleRegeneration = async (image: ImageVO) => {
   // 切换平台
   selectPlatform.value = image.platform
@@ -90,6 +101,12 @@ const handleRegeneration = async (image: ImageVO) => {
   }
   // TODO @fan:貌似 other 重新设置不行?
 }
+
+/** 组件挂载的时候 */
+onMounted(async () => {
+  // 获取模型列表
+  models.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.IMAGE)
+})
 </script>
 
 <style scoped lang="scss">
@@ -109,10 +126,7 @@ const handleRegeneration = async (image: ImageVO) => {
     display: flex;
     flex-direction: column;
     padding: 20px;
-    width: 350px;
-
-    .segmented {
-    }
+    width: 390px;
 
     .segmented .el-segmented {
       --el-border-radius-base: 16px;

+ 2 - 0
src/views/ai/image/manager/index.vue

@@ -1,4 +1,6 @@
 <template>
+  <doc-alert title="AI 绘图创作" url="https://doc.iocoder.cn/ai/image/" />
+
   <ContentWrap>
     <!-- 搜索工作栏 -->
     <el-form

+ 146 - 0
src/views/ai/knowledge/document/form/ProcessStep.vue

@@ -0,0 +1,146 @@
+<template>
+  <div>
+    <!-- 文件处理列表 -->
+    <div class="mt-15px grid grid-cols-1 gap-2">
+      <div
+        v-for="(file, index) in modelValue.list"
+        :key="index"
+        class="flex items-center py-4px px-12px border-l-4 border-l-[#409eff] rounded-sm shadow-sm hover:bg-[#ecf5ff] transition-all duration-300"
+      >
+        <!-- 文件图标和名称 -->
+        <div class="flex items-center min-w-[200px] mr-10px">
+          <Icon icon="ep:document" class="mr-8px text-[#409eff]" />
+          <span class="text-[13px] text-[#303133] break-all">{{ file.name }}</span>
+        </div>
+
+        <!-- 处理进度 -->
+        <div class="flex-1">
+          <el-progress
+            :percentage="file.progress || 0"
+            :stroke-width="10"
+            :status="isProcessComplete(file) ? 'success' : ''"
+          />
+        </div>
+
+        <!-- 分段数量 -->
+        <div class="ml-10px text-[13px] text-[#606266]">
+          分段数量:{{ file.count ? file.count : '-' }}
+        </div>
+      </div>
+    </div>
+
+    <!-- 底部完成按钮 -->
+    <div class="flex justify-end mt-20px">
+      <el-button
+        :type="allProcessComplete ? 'success' : 'primary'"
+        :disabled="!allProcessComplete"
+        @click="handleComplete"
+      >
+        完成
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { KnowledgeSegmentApi } from '@/api/ai/knowledge/segment'
+
+const props = defineProps({
+  modelValue: {
+    type: Object,
+    required: true
+  }
+})
+
+const emit = defineEmits(['update:modelValue'])
+const parent = inject('parent') as any
+const pollingTimer = ref<number | null>(null) // 轮询定时器 ID,用于跟踪和清除轮询进程
+
+/** 判断文件处理是否完成 */
+const isProcessComplete = (file) => {
+  return file.progress === 100
+}
+
+/** 判断所有文件是否都处理完成 */
+const allProcessComplete = computed(() => {
+  return props.modelValue.list.every((file) => isProcessComplete(file))
+})
+
+/** 完成按钮点击事件处理 */
+const handleComplete = () => {
+  if (parent?.exposed?.handleBack) {
+    parent.exposed.handleBack()
+  }
+}
+
+/** 获取文件处理进度 */
+const getProcessList = async () => {
+  try {
+    // 1. 调用 API 获取处理进度
+    const documentIds = props.modelValue.list.filter((item) => item.id).map((item) => item.id)
+    if (documentIds.length === 0) {
+      return
+    }
+    const result = await KnowledgeSegmentApi.getKnowledgeSegmentProcessList(documentIds)
+
+    // 2.1更新进度
+    const updatedList = props.modelValue.list.map((file) => {
+      const processInfo = result.find((item) => item.documentId === file.id)
+      if (processInfo) {
+        // 计算进度百分比:已嵌入数量 / 总数量 * 100
+        const progress =
+          processInfo.embeddingCount && processInfo.count
+            ? Math.floor((processInfo.embeddingCount / processInfo.count) * 100)
+            : 0
+        return {
+          ...file,
+          progress: progress,
+          count: processInfo.count || 0
+        }
+      }
+      return file
+    })
+
+    // 2.2 更新数据
+    emit('update:modelValue', {
+      ...props.modelValue,
+      list: updatedList
+    })
+
+    // 3. 如果未完成,继续轮询
+    if (!updatedList.every((file) => isProcessComplete(file))) {
+      pollingTimer.value = window.setTimeout(getProcessList, 3000)
+    }
+  } catch (error) {
+    // 出错后也继续轮询
+    console.error('获取处理进度失败:', error)
+    pollingTimer.value = window.setTimeout(getProcessList, 5000)
+  }
+}
+
+/** 组件挂载时开始轮询 */
+onMounted(() => {
+  // 1. 初始化进度为 0
+  const initialList = props.modelValue.list.map((file) => ({
+    ...file,
+    progress: 0
+  }))
+
+  emit('update:modelValue', {
+    ...props.modelValue,
+    list: initialList
+  })
+
+  // 2. 开始轮询获取进度
+  getProcessList()
+})
+
+/** 组件卸载前清除轮询 */
+onBeforeUnmount(() => {
+  // 1. 清除定时器
+  if (pollingTimer.value) {
+    clearTimeout(pollingTimer.value)
+    pollingTimer.value = null
+  }
+})
+</script>

+ 238 - 0
src/views/ai/knowledge/document/form/SplitStep.vue

@@ -0,0 +1,238 @@
+<template>
+  <div>
+    <!-- 上部分段设置部分 -->
+    <div class="mb-20px">
+      <div class="mb-20px flex justify-between items-center">
+        <div class="text-16px font-bold flex items-center">
+          分段设置
+          <el-tooltip
+            content="系统会自动将文档内容分割成多个段落,您可以根据需要调整分段方式和内容。"
+            placement="top"
+          >
+            <Icon icon="ep:warning" class="ml-5px text-gray-400" />
+          </el-tooltip>
+        </div>
+        <div>
+          <el-button type="primary" plain size="small" @click="handleAutoSegment">
+            预览分段
+          </el-button>
+        </div>
+      </div>
+
+      <div class="segment-settings mb-20px">
+        <el-form label-width="120px">
+          <el-form-item label="最大 Token 数">
+            <el-input-number v-model="modelData.segmentMaxTokens" :min="1" :max="2048" />
+          </el-form-item>
+        </el-form>
+      </div>
+    </div>
+
+    <!-- 下部文件预览部分 -->
+    <div class="mb-10px">
+      <div class="text-16px font-bold mb-10px">分段预览</div>
+
+      <!-- 文件选择器 -->
+      <div class="file-selector mb-10px">
+        <el-dropdown v-if="modelData.list && modelData.list.length > 0" trigger="click">
+          <div class="flex items-center cursor-pointer">
+            <Icon icon="ep:document" class="text-danger mr-5px" />
+            <span>{{ currentFile?.name || '请选择文件' }}</span>
+            <span v-if="currentFile?.segments" class="ml-5px text-gray-500 text-12px">
+              ({{ currentFile.segments.length }}个分片)
+            </span>
+            <Icon icon="ep:arrow-down" class="ml-5px" />
+          </div>
+          <template #dropdown>
+            <el-dropdown-menu>
+              <el-dropdown-item
+                v-for="(file, index) in modelData.list"
+                :key="index"
+                @click="selectFile(index)"
+              >
+                {{ file.name }}
+                <span v-if="file.segments" class="ml-5px text-gray-500 text-12px">
+                  ({{ file.segments.length }}个分片)
+                </span>
+              </el-dropdown-item>
+            </el-dropdown-menu>
+          </template>
+        </el-dropdown>
+        <div v-else class="text-gray-400">暂无上传文件</div>
+      </div>
+
+      <!-- 文件内容预览 -->
+      <div class="file-preview bg-gray-50 p-15px rounded-md max-h-600px overflow-y-auto">
+        <div v-if="splitLoading" class="flex justify-center items-center py-20px">
+          <Icon icon="ep:loading" class="is-loading" />
+          <span class="ml-10px">正在加载分段内容...</span>
+        </div>
+        <template
+          v-else-if="currentFile && currentFile.segments && currentFile.segments.length > 0"
+        >
+          <div v-for="(segment, index) in currentFile.segments" :key="index" class="mb-10px">
+            <div class="text-gray-500 text-12px mb-5px">
+              分片-{{ index + 1 }} · {{ segment.contentLength || 0 }} 字符数 ·
+              {{ segment.tokens || 0 }} Token
+            </div>
+            <div class="bg-white p-10px rounded-md">{{ segment.content }}</div>
+          </div>
+        </template>
+        <el-empty v-else description="暂无预览内容" />
+      </div>
+    </div>
+
+    <!-- 添加底部按钮 -->
+    <div class="mt-20px flex justify-between">
+      <div>
+        <el-button v-if="!modelData.id" @click="handlePrevStep">上一步</el-button>
+      </div>
+      <div>
+        <el-button type="primary" :loading="submitLoading" @click="handleSave">
+          保存并处理
+        </el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed, getCurrentInstance, inject, onMounted, PropType, ref } from 'vue'
+import { Icon } from '@/components/Icon'
+import { KnowledgeSegmentApi } from '@/api/ai/knowledge/segment'
+import { useMessage } from '@/hooks/web/useMessage'
+import { KnowledgeDocumentApi } from '@/api/ai/knowledge/document'
+
+const props = defineProps({
+  modelValue: {
+    type: Object as PropType<any>,
+    required: true
+  }
+})
+
+const emit = defineEmits(['update:modelValue'])
+const message = useMessage() // 消息提示
+const parent = inject('parent', null) // 获取父组件实例
+
+const modelData = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+}) // 表单数据
+
+const splitLoading = ref(false) // 分段加载状态
+const currentFile = ref<any>(null) // 当前选中的文件
+const submitLoading = ref(false) // 提交按钮加载状态
+
+/** 选择文件 */
+const selectFile = async (index: number) => {
+  currentFile.value = modelData.value.list[index]
+  await splitContent(currentFile.value)
+}
+
+/** 获取文件分段内容 */
+const splitContent = async (file: any) => {
+  if (!file || !file.url) {
+    message.warning('文件 URL 不存在')
+    return
+  }
+
+  splitLoading.value = true
+  try {
+    // 调用后端分段接口,获取文档的分段内容、字符数和 Token 数
+    file.segments = await KnowledgeSegmentApi.splitContent(
+      file.url,
+      modelData.value.segmentMaxTokens
+    )
+  } catch (error) {
+    console.error('获取分段内容失败:', file, error)
+  } finally {
+    splitLoading.value = false
+  }
+}
+
+/** 处理预览分段 */
+const handleAutoSegment = async () => {
+  // 如果没有选中文件,默认选中第一个
+  if (!currentFile.value && modelData.value.list && modelData.value.list.length > 0) {
+    currentFile.value = modelData.value.list[0]
+  }
+  // 如果没有选中文件,提示请先选择文件
+  if (!currentFile.value) {
+    message.warning('请先选择文件')
+    return
+  }
+
+  // 获取分段内容
+  await splitContent(currentFile.value)
+}
+
+/** 上一步按钮处理 */
+const handlePrevStep = () => {
+  const parentEl = parent || getCurrentInstance()?.parent
+  if (parentEl && typeof parentEl.exposed?.goToPrevStep === 'function') {
+    parentEl.exposed.goToPrevStep()
+  }
+}
+
+/** 保存操作 */
+const handleSave = async () => {
+  // 保存前验证
+  if (!currentFile?.value?.segments || currentFile.value.segments.length === 0) {
+    message.warning('请先预览分段内容')
+    return
+  }
+
+  // 设置按钮加载状态
+  submitLoading.value = true
+  try {
+    if (modelData.value.id) {
+      // 修改场景
+      await KnowledgeDocumentApi.updateKnowledgeDocument({
+        id: modelData.value.id,
+        segmentMaxTokens: modelData.value.segmentMaxTokens
+      })
+    } else {
+      // 新增场景
+      const data = await KnowledgeDocumentApi.createKnowledgeDocumentList({
+        knowledgeId: modelData.value.knowledgeId,
+        segmentMaxTokens: modelData.value.segmentMaxTokens,
+        list: modelData.value.list.map((item: any) => ({
+          name: item.name,
+          url: item.url
+        }))
+      })
+      modelData.value.list.forEach((document: any, index: number) => {
+        document.id = data[index]
+      })
+    }
+
+    // 进入下一步
+    const parentEl = parent || getCurrentInstance()?.parent
+    if (parentEl && typeof parentEl.exposed?.goToNextStep === 'function') {
+      parentEl.exposed.goToNextStep()
+    }
+  } catch (error: any) {
+    console.error('保存失败:', modelData.value, error)
+  } finally {
+    // 关闭按钮加载状态
+    submitLoading.value = false
+  }
+}
+
+/** 初始化 */
+onMounted(async () => {
+  // 确保 segmentMaxTokens 存在
+  if (!modelData.value.segmentMaxTokens) {
+    modelData.value.segmentMaxTokens = 500
+  }
+  // 如果没有选中文件,默认选中第一个
+  if (!currentFile.value && modelData.value.list && modelData.value.list.length > 0) {
+    currentFile.value = modelData.value.list[0]
+  }
+
+  // 如果有选中的文件,获取分段内容
+  if (currentFile.value) {
+    await splitContent(currentFile.value)
+  }
+})
+</script>

+ 273 - 0
src/views/ai/knowledge/document/form/UploadStep.vue

@@ -0,0 +1,273 @@
+<template>
+  <el-form ref="formRef" :model="modelData" label-width="0" class="mt-20px">
+    <el-form-item class="mb-20px">
+      <div class="w-full">
+        <div
+          class="w-full border-2 border-dashed border-[#dcdfe6] rounded-md p-20px text-center hover:border-[#409eff]"
+        >
+          <el-upload
+            ref="uploadRef"
+            class="upload-demo"
+            drag
+            :action="uploadUrl"
+            :auto-upload="true"
+            :on-success="handleUploadSuccess"
+            :on-error="handleUploadError"
+            :on-change="handleFileChange"
+            :on-remove="handleFileRemove"
+            :before-upload="beforeUpload"
+            :http-request="httpRequest"
+            :file-list="fileList"
+            :multiple="true"
+            :show-file-list="false"
+            :accept="acceptedFileTypes"
+          >
+            <div class="flex flex-col items-center justify-center py-20px">
+              <Icon icon="ep:upload-filled" class="text-[48px] text-[#c0c4cc] mb-10px" />
+              <div class="el-upload__text text-[16px] text-[#606266]">
+                拖拽文件至此,或者
+                <em class="text-[#409eff] not-italic cursor-pointer">选择文件</em>
+              </div>
+              <div class="el-upload__tip mt-10px text-[#909399] text-[12px]">
+                已支持 {{ supportedFileTypes.join('、') }},每个文件不超过 {{ maxFileSize }} MB。
+              </div>
+            </div>
+          </el-upload>
+        </div>
+
+        <div
+          v-if="modelData.list && modelData.list.length > 0"
+          class="mt-15px grid grid-cols-1 gap-2"
+        >
+          <div
+            v-for="(file, index) in modelData.list"
+            :key="index"
+            class="flex justify-between items-center py-4px px-12px border-l-4 border-l-[#409eff] rounded-sm shadow-sm hover:bg-[#ecf5ff] transition-all duration-300"
+          >
+            <div class="flex items-center">
+              <Icon icon="ep:document" class="mr-8px text-[#409eff]" />
+              <span class="text-[13px] text-[#303133] break-all">{{ file.name }}</span>
+            </div>
+            <el-button type="danger" link @click="removeFile(index)" class="ml-2">
+              <Icon icon="ep:delete" />
+            </el-button>
+          </div>
+        </div>
+      </div>
+    </el-form-item>
+
+    <!-- 添加下一步按钮 -->
+    <el-form-item>
+      <div class="flex justify-end w-full">
+        <el-button type="primary" @click="handleNextStep" :disabled="!isAllUploaded">
+          下一步
+        </el-button>
+      </div>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script lang="ts" setup>
+import { PropType, ref, computed, inject, getCurrentInstance, onMounted } from 'vue'
+import { useMessage } from '@/hooks/web/useMessage'
+import { useUpload } from '@/components/UploadFile/src/useUpload'
+import { generateAcceptedFileTypes } from '@/utils'
+import { Icon } from '@/components/Icon'
+
+const props = defineProps({
+  modelValue: {
+    type: Object as PropType<any>,
+    required: true
+  }
+})
+
+const emit = defineEmits(['update:modelValue'])
+
+const formRef = ref() // 表单引用
+const uploadRef = ref() // 上传组件引用
+const parent = inject('parent', null) // 获取父组件实例
+const { uploadUrl, httpRequest } = useUpload() // 使用上传组件的钩子
+const message = useMessage() // 消息弹窗
+const fileList = ref([]) // 文件列表
+const uploadingCount = ref(0) // 上传中的文件数量
+
+// 支持的文件类型和大小限制
+const supportedFileTypes = [
+  'TXT',
+  'MARKDOWN',
+  'MDX',
+  'PDF',
+  'HTML',
+  'XLSX',
+  'XLS',
+  'DOC',
+  'DOCX',
+  'CSV',
+  'EML',
+  'MSG',
+  'PPTX',
+  'XML',
+  'EPUB',
+  'PPT',
+  'MD',
+  'HTM'
+]
+const allowedExtensions = supportedFileTypes.map((ext) => ext.toLowerCase()) // 小写的扩展名列表
+const maxFileSize = 15 // 最大文件大小(MB)
+
+// 构建 accept 属性值,用于限制文件选择对话框中可见的文件类型
+const acceptedFileTypes = computed(() => generateAcceptedFileTypes(supportedFileTypes))
+
+/** 表单数据 */
+const modelData = computed({
+  get: () => {
+    return props.modelValue
+  },
+  set: (val) => emit('update:modelValue', val)
+})
+
+/** 确保 list 属性存在 */
+const ensureListExists = () => {
+  if (!props.modelValue.list) {
+    emit('update:modelValue', {
+      ...props.modelValue,
+      list: []
+    })
+  }
+}
+
+/** 是否所有文件都已上传完成 */
+const isAllUploaded = computed(() => {
+  return modelData.value.list && modelData.value.list.length > 0 && uploadingCount.value === 0
+})
+
+/**
+ * 上传前检查文件类型和大小
+ *
+ * @param file 待上传的文件
+ * @returns 是否允许上传
+ */
+const beforeUpload = (file) => {
+  // 1.1 检查文件扩展名
+  const fileName = file.name.toLowerCase()
+  const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1)
+  if (!allowedExtensions.includes(fileExtension)) {
+    message.error('不支持的文件类型!')
+    return false
+  }
+  // 1.2 检查文件大小
+  if (!(file.size / 1024 / 1024 < maxFileSize)) {
+    message.error(`文件大小不能超过 ${maxFileSize} MB!`)
+    return false
+  }
+
+  // 2. 增加上传中的文件计数
+  uploadingCount.value++
+  return true
+}
+
+/**
+ * 文件上传成功处理
+ *
+ * @param response 上传响应
+ * @param file 上传的文件
+ */
+const handleUploadSuccess = (response, file) => {
+  // 添加到文件列表
+  if (response && response.data) {
+    ensureListExists()
+    emit('update:modelValue', {
+      ...props.modelValue,
+      list: [
+        ...props.modelValue.list,
+        {
+          name: file.name,
+          url: response.data
+        }
+      ]
+    })
+  } else {
+    message.error(`文件 ${file.name} 上传失败`)
+  }
+
+  // 减少上传中的文件计数
+  uploadingCount.value = Math.max(0, uploadingCount.value - 1)
+}
+
+/**
+ * 文件上传失败处理
+ *
+ * @param error 错误信息
+ * @param file 上传的文件
+ */
+const handleUploadError = (error, file) => {
+  message.error(`文件 ${file.name} 上传失败: ${error}`)
+  // 减少上传中的文件计数
+  uploadingCount.value = Math.max(0, uploadingCount.value - 1)
+}
+
+/**
+ * 文件变更处理
+ *
+ * @param file 变更的文件
+ */
+const handleFileChange = (file) => {
+  if (file.status === 'success' || file.status === 'fail') {
+    uploadingCount.value = Math.max(0, uploadingCount.value - 1)
+  }
+}
+
+/**
+ * 文件移除处理
+ *
+ * @param file 被移除的文件
+ */
+const handleFileRemove = (file) => {
+  if (file.status === 'uploading') {
+    uploadingCount.value = Math.max(0, uploadingCount.value - 1)
+  }
+}
+
+/**
+ * 从列表中移除文件
+ *
+ * @param index 要移除的文件索引
+ */
+const removeFile = (index: number) => {
+  // 从列表中移除文件
+  const newList = [...props.modelValue.list]
+  newList.splice(index, 1)
+  // 更新表单数据
+  emit('update:modelValue', {
+    ...props.modelValue,
+    list: newList
+  })
+}
+
+/** 下一步按钮处理 */
+const handleNextStep = () => {
+  // 1.1 检查是否有文件上传
+  if (!modelData.value.list || modelData.value.list.length === 0) {
+    message.warning('请上传至少一个文件')
+    return
+  }
+  // 1.2 检查是否有文件正在上传
+  if (uploadingCount.value > 0) {
+    message.warning('请等待所有文件上传完成')
+    return
+  }
+
+  // 2. 获取父组件的goToNextStep方法
+  const parentEl = parent || getCurrentInstance()?.parent
+  if (parentEl && typeof parentEl.exposed?.goToNextStep === 'function') {
+    parentEl.exposed.goToNextStep()
+  }
+}
+
+/** 初始化 */
+onMounted(() => {
+  ensureListExists()
+})
+</script>
+
+<style lang="scss" scoped></style>

+ 193 - 0
src/views/ai/knowledge/document/form/index.vue

@@ -0,0 +1,193 @@
+<template>
+  <ContentWrap>
+    <div class="mx-auto">
+      <!-- 头部导航栏 -->
+      <div
+        class="absolute top-0 left-0 right-0 h-50px bg-white border-bottom z-10 flex items-center px-20px"
+      >
+        <!-- 左侧标题 -->
+        <div class="w-200px flex items-center overflow-hidden">
+          <Icon icon="ep:arrow-left" class="cursor-pointer flex-shrink-0" @click="handleBack" />
+          <span class="ml-10px text-16px truncate">
+            {{ formData.id ? '编辑知识库文档' : '创建知识库文档' }}
+          </span>
+        </div>
+
+        <!-- 步骤条 -->
+        <div class="flex-1 flex items-center justify-center h-full">
+          <div class="w-400px flex items-center justify-between h-full">
+            <div
+              v-for="(step, index) in steps"
+              :key="index"
+              class="flex items-center mx-15px relative h-full"
+              :class="[
+                currentStep === index
+                  ? 'text-[#3473ff] border-[#3473ff] border-b-2 border-b-solid'
+                  : 'text-gray-500'
+              ]"
+            >
+              <div
+                class="w-28px h-28px rounded-full flex items-center justify-center mr-8px border-2 border-solid text-15px"
+                :class="[
+                  currentStep === index
+                    ? 'bg-[#3473ff] text-white border-[#3473ff]'
+                    : 'border-gray-300 bg-white text-gray-500'
+                ]"
+              >
+                {{ index + 1 }}
+              </div>
+              <span class="text-16px font-bold whitespace-nowrap">{{ step.title }}</span>
+            </div>
+          </div>
+        </div>
+
+        <!-- 右侧按钮 - 已移除 -->
+        <div class="w-200px flex items-center justify-end gap-2"> </div>
+      </div>
+
+      <!-- 主体内容 -->
+      <div class="mt-50px">
+        <!-- 第一步:上传文档 -->
+        <div v-if="currentStep === 0" class="mx-auto w-560px">
+          <UploadStep v-model="formData" ref="uploadDocumentRef" />
+        </div>
+
+        <!-- 第二步:文档分段 -->
+        <div v-if="currentStep === 1" class="mx-auto w-560px">
+          <SplitStep v-model="formData" ref="documentSegmentRef" />
+        </div>
+
+        <!-- 第三步:处理并完成 -->
+        <div v-if="currentStep === 2" class="mx-auto w-560px">
+          <ProcessStep v-model="formData" ref="processCompleteRef" />
+        </div>
+      </div>
+    </div>
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { useRoute, useRouter } from 'vue-router'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import UploadStep from './UploadStep.vue'
+import SplitStep from './SplitStep.vue'
+import ProcessStep from './ProcessStep.vue'
+import { KnowledgeDocumentApi } from '@/api/ai/knowledge/document'
+
+const { delView } = useTagsViewStore() // 视图操作
+const route = useRoute() // 路由
+const router = useRouter() // 路由
+
+// 组件引用
+const uploadDocumentRef = ref()
+const documentSegmentRef = ref()
+const processCompleteRef = ref()
+const currentStep = ref(0) // 步骤控制
+const steps = [{ title: '上传文档' }, { title: '文档分段' }, { title: '处理并完成' }]
+const formData = ref({
+  knowledgeId: undefined, // 知识库编号
+  id: undefined, // 编辑的文档编号(documentId)
+  segmentMaxTokens: 500, // 分段最大 token 数
+  list: [] as Array<{
+    id: number // 文档编号
+    name: string // 文档名称
+    url: string // 文档 URL
+    segments: Array<{
+      content?: string
+      contentLength?: number
+      tokens?: number
+    }>
+    count?: number // 段落数量
+    process?: number // 处理进度
+  }> // 用于存储上传的文件列表
+}) // 表单数据
+
+provide('parent', getCurrentInstance()) // 提供 parent 给子组件使用
+
+/** 初始化数据 */
+const initData = async () => {
+  // 【新增场景】从路由参数中获取知识库 ID
+  if (route.query.knowledgeId) {
+    formData.value.knowledgeId = route.query.knowledgeId as any
+  }
+
+  // 【修改场景】从路由参数中获取文档 ID
+  const documentId = route.query.id
+  if (documentId) {
+    // 获取文档信息
+    formData.value.id = documentId as any
+    const document = await KnowledgeDocumentApi.getKnowledgeDocument(documentId as any)
+    formData.value.segmentMaxTokens = document.segmentMaxTokens
+    formData.value.list = [
+      {
+        id: document.id,
+        name: document.name,
+        url: document.url,
+        segments: []
+      }
+    ]
+    // 进入下一步
+    goToNextStep()
+  }
+}
+
+/** 切换到下一步 */
+const goToNextStep = () => {
+  if (currentStep.value < steps.length - 1) {
+    currentStep.value++
+  }
+}
+
+/** 切换到上一步 */
+const goToPrevStep = () => {
+  if (currentStep.value > 0) {
+    currentStep.value--
+  }
+}
+
+/** 返回列表页 */
+const handleBack = () => {
+  // 先删除当前页签
+  delView(unref(router.currentRoute))
+  // 跳转到列表页
+  router.push({ name: 'AiKnowledgeDocument', query: { knowledgeId: formData.value.knowledgeId } })
+}
+
+/** 初始化 */
+onMounted(async () => {
+  await initData()
+})
+
+/** 添加组件卸载前的清理代码 */
+onBeforeUnmount(() => {
+  // 清理所有的引用
+  uploadDocumentRef.value = null
+  documentSegmentRef.value = null
+  processCompleteRef.value = null
+})
+
+/** 暴露方法给子组件使用 */
+defineExpose({
+  goToNextStep,
+  goToPrevStep,
+  handleBack
+})
+</script>
+
+<style lang="scss" scoped>
+.border-bottom {
+  border-bottom: 1px solid #dcdfe6;
+}
+
+.text-primary {
+  color: #3473ff;
+}
+
+.bg-primary {
+  background-color: #3473ff;
+}
+
+.border-primary {
+  border-color: #3473ff;
+}
+</style>

+ 236 - 0
src/views/ai/knowledge/document/index.vue

@@ -0,0 +1,236 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="文件名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入文件名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="是否启用" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择是否启用"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button type="primary" plain @click="handleCreate" v-hasPermi="['ai:knowledge:create']">
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="文档编号" align="center" prop="id" />
+      <el-table-column label="文件名称" align="center" prop="name" />
+      <el-table-column label="字符数" align="center" prop="contentLength" />
+      <el-table-column label="Token 数" align="center" prop="tokens" />
+      <el-table-column label="分片最大 Token 数" align="center" prop="segmentMaxTokens" />
+      <el-table-column label="召回次数" align="center" prop="retrievalCount" />
+      <el-table-column label="是否启用" align="center" prop="status">
+        <template #default="scope">
+          <el-switch
+            v-model="scope.row.status"
+            :active-value="0"
+            :inactive-value="1"
+            @change="handleStatusChange(scope.row)"
+            :disabled="!checkPermi(['ai:knowledge:update'])"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="上传时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center" min-width="120px">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="handleUpdate(scope.row.id)"
+            v-hasPermi="['ai:knowledge:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="primary"
+            @click="handleSegment(scope.row.id)"
+            v-hasPermi="['ai:knowledge:query']"
+          >
+            分段
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['ai:knowledge:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <!-- <KnowledgeDocumentForm ref="formRef" @success="getList" /> -->
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { KnowledgeDocumentApi, KnowledgeDocumentVO } from '@/api/ai/knowledge/document'
+import { useRoute, useRouter } from 'vue-router'
+import { checkPermi } from '@/utils/permission'
+import { CommonStatusEnum } from '@/utils/constants'
+// import KnowledgeDocumentForm from './KnowledgeDocumentForm.vue'
+
+/** AI 知识库文档 列表 */
+defineOptions({ name: 'KnowledgeDocument' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const route = useRoute() // 路由
+const router = useRouter() // 路由
+
+const loading = ref(true) // 列表的加载中
+const list = ref<KnowledgeDocumentVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  status: undefined,
+  knowledgeId: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await KnowledgeDocumentApi.getKnowledgeDocumentPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 跳转到创建文档页面 */
+const handleCreate = () => {
+  router.push({
+    name: 'AiKnowledgeDocumentCreate',
+    query: { knowledgeId: queryParams.knowledgeId }
+  })
+}
+
+/** 跳转到更新文档页面 */
+const handleUpdate = (id: number) => {
+  router.push({
+    name: 'AiKnowledgeDocumentUpdate',
+    query: { id, knowledgeId: queryParams.knowledgeId }
+  })
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await KnowledgeDocumentApi.deleteKnowledgeDocument(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 修改状态操作 */
+const handleStatusChange = async (row: KnowledgeDocumentVO) => {
+  try {
+    // 修改状态的二次确认
+    const text = row.status === CommonStatusEnum.ENABLE ? '启用' : '禁用'
+    await message.confirm('确认要"' + text + '""' + row.name + '"文档吗?')
+    // 发起修改状态
+    await KnowledgeDocumentApi.updateKnowledgeDocumentStatus({ id: row.id, status: row.status })
+    message.success(t('common.updateSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {
+    // 取消后,进行恢复按钮
+    row.status =
+      row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
+  }
+}
+
+/** 跳转到知识库分段页面 */
+const handleSegment = (id: number) => {
+  router.push({
+    name: 'AiKnowledgeSegment',
+    query: { documentId: id }
+  })
+}
+
+/** 初始化 **/
+onMounted(() => {
+  // 如果知识库 ID 不存在,显示错误提示并关闭页面
+  if (!route.query.knowledgeId) {
+    message.error('知识库 ID 不存在,无法查看文档列表')
+    // 关闭当前路由,返回到知识库列表页面
+    router.push({ name: 'AiKnowledge' })
+    return
+  }
+
+  // 从路由参数中获取知识库 ID
+  queryParams.knowledgeId = route.query.knowledgeId as any
+  getList()
+})
+</script>

+ 162 - 0
src/views/ai/knowledge/knowledge/KnowledgeForm.vue

@@ -0,0 +1,162 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="130px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="知识库名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入知识库名称" />
+      </el-form-item>
+      <el-form-item label="知识库描述" prop="description">
+        <el-input
+          v-model="formData.description"
+          type="textarea"
+          :rows="3"
+          placeholder="请输入知识库描述"
+        />
+      </el-form-item>
+      <el-form-item label="向量模型" prop="embeddingModelId">
+        <el-select
+          v-model="formData.embeddingModelId"
+          placeholder="请选择向量模型"
+          clearable
+          class="!w-full"
+        >
+          <el-option v-for="item in modelList" :key="item.id" :label="item.name" :value="item.id" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="检索 topK" prop="topK">
+        <el-input-number
+          v-model="formData.topK"
+          placeholder="请输入检索 topK"
+          :min="0"
+          :max="10"
+          class="!w-1/1"
+        />
+      </el-form-item>
+      <el-form-item label="检索相似度阈值" prop="similarityThreshold">
+        <el-input-number
+          v-model="formData.similarityThreshold"
+          placeholder="请输入检索相似度阈值"
+          :min="0"
+          :max="1"
+          :step="0.01"
+          :precision="2"
+          class="!w-1/1"
+        />
+      </el-form-item>
+      <el-form-item label="是否启用" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { KnowledgeApi, KnowledgeVO } from '@/api/ai/knowledge/knowledge'
+import { CommonStatusEnum } from '@/utils/constants'
+import { ModelApi, ModelVO } from '@/api/ai/model/model'
+import { AiModelTypeEnum } from '../../utils/constants'
+
+/** AI 知识库表单 */
+defineOptions({ name: 'KnowledgeForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  description: undefined,
+  embeddingModelId: undefined,
+  topK: undefined,
+  similarityThreshold: undefined,
+  status: CommonStatusEnum.ENABLE // 默认开启
+})
+const formRules = reactive({
+  name: [{ required: true, message: '请输入知识库名称', trigger: 'blur' }],
+  embeddingModelId: [{ required: true, message: '请输入向量模型', trigger: 'blur' }],
+  topK: [{ required: true, message: '请输入检索 topK', trigger: 'blur' }],
+  similarityThreshold: [{ required: true, message: '请输入检索相似度阈值', trigger: 'blur' }],
+  status: [{ required: true, message: '请选择是否启用', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+const modelList = ref<ModelVO[]>([]) // 向量模型选项
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 获取向量模型列表
+  modelList.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.EMBEDDING)
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await KnowledgeApi.getKnowledge(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as KnowledgeVO
+    if (formType.value === 'create') {
+      await KnowledgeApi.createKnowledge(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await KnowledgeApi.updateKnowledge(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    description: undefined,
+    embeddingModelId: undefined,
+    topK: undefined,
+    similarityThreshold: undefined,
+    status: CommonStatusEnum.ENABLE // 默认开启
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 221 - 0
src/views/ai/knowledge/knowledge/index.vue

@@ -0,0 +1,221 @@
+<template>
+  <doc-alert title="AI 知识库" url="https://doc.iocoder.cn/ai/knowledge/" />
+
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="95px"
+    >
+      <el-form-item label="知识库名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入知识库名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="是否启用" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择是否启用"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-220px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['ai:knowledge:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="知识库名称" align="center" prop="name" />
+      <el-table-column label="知识库描述" align="center" prop="description" />
+      <el-table-column label="向量化模型" align="center" prop="embeddingModel" />
+      <el-table-column label="是否启用" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center" min-width="120px">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['ai:knowledge:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="primary"
+            @click="handleDocument(scope.row.id)"
+            v-hasPermi="['ai:knowledge:query']"
+          >
+            文档
+          </el-button>
+          <el-button
+            link
+            type="primary"
+            @click="handleRetrieval(scope.row.id)"
+            v-hasPermi="['ai:knowledge:query']"
+          >
+            召回测试
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['ai:knowledge:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <KnowledgeForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { KnowledgeApi, KnowledgeVO } from '@/api/ai/knowledge/knowledge'
+import KnowledgeForm from './KnowledgeForm.vue'
+import { useRouter } from 'vue-router'
+
+/** AI 知识库列表 */
+defineOptions({ name: 'Knowledge' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<KnowledgeVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  status: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await KnowledgeApi.getKnowledgePage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await KnowledgeApi.deleteKnowledge(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 文档按钮操作 */
+const router = useRouter()
+const handleDocument = (id: number) => {
+  router.push({
+    name: 'AiKnowledgeDocument',
+    query: { knowledgeId: id }
+  })
+}
+
+/** 跳转到文档召回测试页面 */
+const handleRetrieval = (id: number) => {
+  router.push({
+    name: 'AiKnowledgeRetrieval',
+    query: { id }
+  })
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 163 - 0
src/views/ai/knowledge/knowledge/retrieval/index.vue

@@ -0,0 +1,163 @@
+<template>
+  <div class="flex gap-20px w-full">
+    <!-- 左侧输入区域 -->
+    <ContentWrap class="flex-1 min-w-300px">
+      <div class="mb-15px">
+        <h3 class="m-0 mb-5px">召回测试</h3>
+        <div class="text-gray-500 text-14px">根据给定的查询文本测试召回效果。</div>
+      </div>
+      <div>
+        <div class="relative mb-10px">
+          <el-input
+            v-model="queryParams.content"
+            type="textarea"
+            :rows="8"
+            placeholder="请输入文本"
+          />
+          <div class="absolute bottom-10px right-10px text-gray-400 text-12px">
+            {{ queryParams.content?.length }} / 200
+          </div>
+        </div>
+        <div class="flex items-center mb-10px">
+          <span class="w-60px text-gray-500">topK:</span>
+          <el-input-number v-model="queryParams.topK" :min="1" :max="20" />
+        </div>
+        <div class="flex items-center mb-15px">
+          <span class="w-60px text-gray-500">相似度:</span>
+          <el-input-number
+            v-model="queryParams.similarityThreshold"
+            :min="0"
+            :max="1"
+            :precision="2"
+            :step="0.01"
+          />
+        </div>
+        <div class="flex justify-end">
+          <el-button type="primary" @click="getRetrievalResult" :loading="loading">测试</el-button>
+        </div>
+      </div>
+    </ContentWrap>
+
+    <!-- 右侧召回结果区域 -->
+    <ContentWrap class="flex-1 min-w-300px">
+      <el-empty v-if="loading" description="正在检索中..." />
+      <div v-else-if="segments.length > 0" class="font-bold mb-15px">
+        {{ segments.length }} 个召回段落
+      </div>
+      <el-empty v-else description="暂无召回结果" />
+      <div>
+        <div
+          v-for="(segment, index) in segments"
+          :key="index"
+          class="mb-20px border border-solid border-gray-200 rounded p-15px"
+        >
+          <div class="flex justify-between text-12px text-gray-500 mb-5px">
+            <span>
+              分段({{ segment.id }}) · {{ segment.contentLength }} 字符数 ·
+              {{ segment.tokens }} Token
+            </span>
+            <span class="px-8px py-4px bg-blue-50 text-blue-500 rounded-full text-12px font-bold">
+              score: {{ segment.score }}
+            </span>
+          </div>
+          <div
+            class="bg-gray-50 p-10px rounded mb-10px whitespace-pre-wrap overflow-hidden transition-all duration-100 text-13px"
+            :class="{
+              'line-clamp-2 max-h-50px': !segment.expanded,
+              'max-h-500px': segment.expanded
+            }"
+          >
+            {{ segment.content }}
+          </div>
+          <div class="flex justify-between items-center">
+            <div class="flex items-center text-gray-500 text-13px">
+              <Icon icon="ep:document" class="mr-5px" />
+              <span>{{ segment.documentName || '未知文档' }}</span>
+            </div>
+            <el-button size="small" @click="toggleExpand(segment)">
+              {{ segment.expanded ? '收起' : '展开' }}
+              <Icon :icon="segment.expanded ? 'ep:arrow-up' : 'ep:arrow-down'" />
+            </el-button>
+          </div>
+        </div>
+      </div>
+    </ContentWrap>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useMessage } from '@/hooks/web/useMessage'
+import { KnowledgeSegmentApi } from '@/api/ai/knowledge/segment'
+import { KnowledgeApi } from '@/api/ai/knowledge/knowledge'
+/** 文档召回测试 */
+defineOptions({ name: 'KnowledgeDocumentRetrieval' })
+
+const message = useMessage() // 消息弹窗
+const route = useRoute() // 路由
+const router = useRouter() // 路由
+
+const loading = ref(false) // 加载状态
+const segments = ref<any[]>([]) // 召回结果
+const queryParams = reactive({
+  id: undefined,
+  content: '',
+  topK: 10,
+  similarityThreshold: 0.5
+})
+
+/** 调用文档召回测试接口 */
+const getRetrievalResult = async () => {
+  if (!queryParams.content) {
+    message.warning('请输入查询文本')
+    return
+  }
+
+  loading.value = true
+  segments.value = []
+
+  try {
+    const data = await KnowledgeSegmentApi.searchKnowledgeSegment({
+      knowledgeId: queryParams.id,
+      content: queryParams.content,
+      topK: queryParams.topK,
+      similarityThreshold: queryParams.similarityThreshold
+    })
+    segments.value = data || []
+  } catch (error) {
+    console.error(error)
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 展开/收起段落内容 */
+const toggleExpand = (segment: any) => {
+  segment.expanded = !segment.expanded
+}
+
+/** 获取知识库信息 */
+const getKnowledgeInfo = async (id: number) => {
+  try {
+    const knowledge = await KnowledgeApi.getKnowledge(id)
+    if (knowledge) {
+      queryParams.topK = knowledge.topK || queryParams.topK
+      queryParams.similarityThreshold =
+        knowledge.similarityThreshold || queryParams.similarityThreshold
+    }
+  } catch (error) {}
+}
+
+/** 初始化 **/
+onMounted(() => {
+  // 如果知识库 ID 不存在,显示错误提示并关闭页面
+  if (!route.query.id) {
+    message.error('知识库 ID 不存在,无法进行召回测试')
+    router.back()
+    return
+  }
+  queryParams.id = route.query.id as any
+
+  // 获取知识库信息并设置默认值
+  getKnowledgeInfo(queryParams.id as any)
+})
+</script>

+ 101 - 0
src/views/ai/knowledge/segment/KnowledgeSegmentForm.vue

@@ -0,0 +1,101 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="切片内容" prop="content">
+        <el-input
+          v-model="formData.content"
+          type="textarea"
+          :rows="6"
+          placeholder="请输入切片内容"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { KnowledgeSegmentApi, KnowledgeSegmentVO } from '@/api/ai/knowledge/segment'
+
+/** AI 知识库分段表单 */
+defineOptions({ name: 'KnowledgeSegmentForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  documentId: undefined,
+  content: undefined
+})
+const formRules = reactive({
+  content: [{ required: true, message: '切片内容不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number, documentId?: any) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  formData.value.documentId = documentId as any
+
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await KnowledgeSegmentApi.getKnowledgeSegment(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as KnowledgeSegmentVO
+    if (formType.value === 'create') {
+      await KnowledgeSegmentApi.createKnowledgeSegment(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await KnowledgeSegmentApi.updateKnowledgeSegment(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    documentId: undefined,
+    content: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 242 - 0
src/views/ai/knowledge/segment/index.vue

@@ -0,0 +1,242 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="文档编号" prop="documentId">
+        <el-input
+          v-model="queryParams.documentId"
+          placeholder="请输入文档编号"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="是否启用" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择是否启用"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['ai:knowledge:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="分段编号" align="center" prop="id" />
+      <el-table-column type="expand">
+        <template #default="props">
+          <div
+            class="content-expand"
+            style="
+              padding: 10px 20px;
+              white-space: pre-wrap;
+              line-height: 1.5;
+              background-color: #f9f9f9;
+              border-radius: 4px;
+              border-left: 3px solid #409eff;
+            "
+          >
+            <div
+              class="content-title"
+              style="margin-bottom: 8px; color: #606266; font-size: 14px; font-weight: bold"
+            >
+              完整内容:
+            </div>
+            {{ props.row.content }}
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="切片内容"
+        align="center"
+        prop="content"
+        min-width="250px"
+        :show-overflow-tooltip="true"
+      />
+      <el-table-column label="字符数" align="center" prop="contentLength" />
+      <el-table-column label="token 数量" align="center" prop="tokens" />
+      <el-table-column label="召回次数" align="center" prop="retrievalCount" />
+      <el-table-column label="是否启用" align="center" prop="status">
+        <template #default="scope">
+          <el-switch
+            v-model="scope.row.status"
+            :active-value="0"
+            :inactive-value="1"
+            @change="handleStatusChange(scope.row)"
+            :disabled="!checkPermi(['ai:knowledge:update'])"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center" min-width="120px">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['ai:knowledge:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['ai:knowledge:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <KnowledgeSegmentForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { KnowledgeSegmentApi, KnowledgeSegmentVO } from '@/api/ai/knowledge/segment'
+import KnowledgeSegmentForm from './KnowledgeSegmentForm.vue'
+import { CommonStatusEnum } from '@/utils/constants'
+import { checkPermi } from '@/utils/permission'
+
+/** AI 知识库分段 列表 */
+defineOptions({ name: 'KnowledgeSegment' })
+
+const message = useMessage() // 消息弹窗
+const router = useRouter() // 路由
+const route = useRoute() // 路由
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<KnowledgeSegmentVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  documentId: undefined,
+  content: undefined,
+  status: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await KnowledgeSegmentApi.getKnowledgeSegmentPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id, queryParams.documentId)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await KnowledgeSegmentApi.deleteKnowledgeSegment(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 修改状态操作 */
+const handleStatusChange = async (row: KnowledgeSegmentVO) => {
+  try {
+    // 修改状态的二次确认
+    const text = row.status === CommonStatusEnum.ENABLE ? '启用' : '禁用'
+    await message.confirm('确认要"' + text + '"该分段吗?')
+    // 发起修改状态
+    await KnowledgeSegmentApi.updateKnowledgeSegmentStatus({ id: row.id, status: row.status })
+    message.success(t('common.updateSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {
+    // 取消后,进行恢复按钮
+    row.status =
+      row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  // 如果文档 ID 不存在,显示错误提示并关闭页面
+  if (!route.query.documentId) {
+    message.error('文档 ID 不存在,无法查看分段列表')
+    // 关闭当前路由,返回到文档列表页面
+    router.push({ name: 'AiKnowledgeDocument' })
+    return
+  }
+
+  // 从路由参数中获取文档 ID
+  queryParams.documentId = route.query.documentId as any
+  getList()
+})
+</script>

+ 2 - 2
src/views/ai/mindmap/index/components/Left.vue

@@ -8,7 +8,7 @@
         <el-input
           v-model="formData.prompt"
           maxlength="1024"
-          rows="5"
+          :rows="5"
           class="w-100% mt-15px"
           input-style="border-radius: 7px;"
           placeholder="请输入提示词,让AI帮你完善"
@@ -29,7 +29,7 @@
         <el-input
           v-model="generatedContent"
           maxlength="1024"
-          rows="5"
+          :rows="5"
           class="w-100% mt-15px"
           input-style="border-radius: 7px;"
           placeholder="例如:童话里的小屋应该是什么样子?"

+ 2 - 0
src/views/ai/mindmap/manager/index.vue

@@ -1,4 +1,6 @@
 <template>
+  <doc-alert title="AI 思维导图" url="https://doc.iocoder.cn/ai/mindmap/" />
+
   <ContentWrap>
     <!-- 搜索工作栏 -->
     <el-form

+ 2 - 0
src/views/ai/model/apiKey/index.vue

@@ -1,4 +1,6 @@
 <template>
+  <doc-alert title="AI 手册" url="https://doc.iocoder.cn/ai/build/" />
+
   <ContentWrap>
     <!-- 搜索工作栏 -->
     <el-form

+ 37 - 9
src/views/ai/model/chatRole/ChatRoleForm.vue

@@ -16,10 +16,10 @@
       <el-form-item label="绑定模型" prop="modelId" v-if="!isUser">
         <el-select v-model="formData.modelId" placeholder="请选择模型" clearable>
           <el-option
-            v-for="chatModel in chatModelList"
-            :key="chatModel.id"
-            :label="chatModel.name"
-            :value="chatModel.id"
+            v-for="model in models"
+            :key="model.id"
+            :label="model.name"
+            :value="model.id"
           />
         </el-select>
       </el-form-item>
@@ -32,6 +32,21 @@
       <el-form-item label="角色设定" prop="systemMessage">
         <el-input type="textarea" v-model="formData.systemMessage" placeholder="请输入角色设定" />
       </el-form-item>
+      <el-form-item label="引用知识库" prop="knowledgeIds">
+        <el-select v-model="formData.knowledgeIds" placeholder="请选择知识库" clearable multiple>
+          <el-option
+            v-for="item in knowledgeList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="引用工具" prop="toolIds">
+        <el-select v-model="formData.toolIds" placeholder="请选择工具" clearable multiple>
+          <el-option v-for="item in toolList" :key="item.id" :label="item.name" :value="item.id" />
+        </el-select>
+      </el-form-item>
       <el-form-item label="是否公开" prop="publicStatus" v-if="!isUser">
         <el-radio-group v-model="formData.publicStatus">
           <el-radio
@@ -68,8 +83,11 @@
 import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
 import { ChatRoleApi, ChatRoleVO } from '@/api/ai/model/chatRole'
 import { CommonStatusEnum } from '@/utils/constants'
-import { ChatModelApi, ChatModelVO } from '@/api/ai/model/chatModel'
+import { ModelApi, ModelVO } from '@/api/ai/model/model'
 import { FormRules } from 'element-plus'
+import { AiModelTypeEnum } from '@/views/ai/utils/constants'
+import { KnowledgeApi, KnowledgeVO } from '@/api/ai/knowledge/knowledge'
+import { ToolApi, ToolVO } from '@/api/ai/model/tool'
 
 /** AI 聊天角色 表单 */
 defineOptions({ name: 'ChatRoleForm' })
@@ -91,10 +109,14 @@ const formData = ref({
   description: undefined,
   systemMessage: undefined,
   publicStatus: true,
-  status: CommonStatusEnum.ENABLE
+  status: CommonStatusEnum.ENABLE,
+  knowledgeIds: [] as number[],
+  toolIds: [] as number[]
 })
 const formRef = ref() // 表单 Ref
-const chatModelList = ref([] as ChatModelVO[]) // 聊天模型列表
+const models = ref([] as ModelVO[]) // 聊天模型列表
+const knowledgeList = ref([] as KnowledgeVO[]) // 知识库列表
+const toolList = ref([] as ToolVO[]) // 工具列表
 
 /** 是否【我】自己创建,私有角色 */
 const isUser = computed(() => {
@@ -128,7 +150,11 @@ const open = async (type: string, id?: number, title?: string) => {
     }
   }
   // 获得下拉数据
-  chatModelList.value = await ChatModelApi.getChatModelSimpleList(CommonStatusEnum.ENABLE)
+  models.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.CHAT)
+  // 获取知识库列表
+  knowledgeList.value = await KnowledgeApi.getSimpleKnowledgeList()
+  // 获取工具列表
+  toolList.value = await ToolApi.getToolSimpleList()
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
@@ -176,7 +202,9 @@ const resetForm = () => {
     description: undefined,
     systemMessage: undefined,
     publicStatus: true,
-    status: CommonStatusEnum.ENABLE
+    status: CommonStatusEnum.ENABLE,
+    knowledgeIds: [],
+    toolIds: []
   }
   formRef.value?.resetFields()
 }

+ 14 - 0
src/views/ai/model/chatRole/index.vue

@@ -1,4 +1,6 @@
 <template>
+  <doc-alert title="AI 对话聊天" url="https://doc.iocoder.cn/ai/chat/" />
+
   <ContentWrap>
     <!-- 搜索工作栏 -->
     <el-form
@@ -69,6 +71,18 @@
       <el-table-column label="角色类别" align="center" prop="category" />
       <el-table-column label="角色描述" align="center" prop="description" />
       <el-table-column label="角色设定" align="center" prop="systemMessage" />
+      <el-table-column label="知识库" align="center" prop="knowledgeIds">
+        <template #default="scope">
+          <span v-if="!scope.row.knowledgeIds || scope.row.knowledgeIds.length === 0">-</span>
+          <span v-else>引用 {{ scope.row.knowledgeIds.length }} 个</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="工具" align="center" prop="toolIds">
+        <template #default="scope">
+          <span v-if="!scope.row.toolIds || scope.row.toolIds.length === 0">-</span>
+          <span v-else>引用 {{ scope.row.toolIds.length }} 个</span>
+        </template>
+      </el-table-column>
       <el-table-column label="是否公开" align="center" prop="publicStatus">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.publicStatus" />

+ 51 - 12
src/views/ai/model/chatModel/ChatModelForm.vue → src/views/ai/model/model/ModelForm.vue

@@ -17,6 +17,21 @@
           />
         </el-select>
       </el-form-item>
+      <el-form-item label="模型类型" prop="type">
+        <el-select
+          v-model="formData.type"
+          placeholder="请输入模型类型"
+          clearable
+          :disabled="formData.id"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.AI_MODEL_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
       <el-form-item label="API 秘钥" prop="keyId">
         <el-select v-model="formData.keyId" placeholder="请选择 API 秘钥" clearable>
           <el-option
@@ -47,29 +62,44 @@
           </el-radio>
         </el-radio-group>
       </el-form-item>
-      <el-form-item label="温度参数" prop="temperature">
+      <el-form-item
+        label="温度参数"
+        prop="temperature"
+        v-if="formData.type === AiModelTypeEnum.CHAT"
+      >
         <el-input-number
           v-model="formData.temperature"
           placeholder="请输入温度参数"
           :min="0"
           :max="2"
           :precision="2"
+          class="!w-1/1"
         />
       </el-form-item>
-      <el-form-item label="回复数 Token 数" prop="maxTokens">
+      <el-form-item
+        label="回复数 Token 数"
+        prop="maxTokens"
+        v-if="formData.type === AiModelTypeEnum.CHAT"
+      >
         <el-input-number
           v-model="formData.maxTokens"
           placeholder="请输入回复数 Token 数"
           :min="0"
-          :max="4096"
+          :max="8192"
+          class="!w-1/1"
         />
       </el-form-item>
-      <el-form-item label="上下文数量" prop="maxContexts">
+      <el-form-item
+        label="上下文数量"
+        prop="maxContexts"
+        v-if="formData.type === AiModelTypeEnum.CHAT"
+      >
         <el-input-number
           v-model="formData.maxContexts"
           placeholder="请输入上下文数量"
           :min="0"
           :max="20"
+          class="!w-1/1"
         />
       </el-form-item>
     </el-form>
@@ -80,13 +110,14 @@
   </Dialog>
 </template>
 <script setup lang="ts">
-import { ChatModelApi, ChatModelVO } from '@/api/ai/model/chatModel'
+import { ModelApi, ModelVO } from '@/api/ai/model/model'
 import { ApiKeyApi, ApiKeyVO } from '@/api/ai/model/apiKey'
 import { CommonStatusEnum } from '@/utils/constants'
 import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+import { AiModelTypeEnum } from '@/views/ai/utils/constants'
 
-/** API 聊天模型 表单 */
-defineOptions({ name: 'ChatModelForm' })
+/** API 模型的表单 */
+defineOptions({ name: 'ModelForm' })
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -101,6 +132,7 @@ const formData = ref({
   name: undefined,
   model: undefined,
   platform: undefined,
+  type: undefined,
   sort: undefined,
   status: CommonStatusEnum.ENABLE,
   temperature: undefined,
@@ -112,6 +144,7 @@ const formRules = reactive({
   name: [{ required: true, message: '模型名字不能为空', trigger: 'blur' }],
   model: [{ required: true, message: '模型标识不能为空', trigger: 'blur' }],
   platform: [{ required: true, message: '所属平台不能为空', trigger: 'blur' }],
+  type: [{ required: true, message: '模型类型不能为空', trigger: 'blur' }],
   sort: [{ required: true, message: '排序不能为空', trigger: 'blur' }],
   status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
 })
@@ -128,13 +161,13 @@ const open = async (type: string, id?: number) => {
   if (id) {
     formLoading.value = true
     try {
-      formData.value = await ChatModelApi.getChatModel(id)
+      formData.value = await ModelApi.getModel(id)
     } finally {
       formLoading.value = false
     }
   }
   // 获得下拉数据
-  apiKeyList.value = await ApiKeyApi.getApiKeySimpleList(CommonStatusEnum.ENABLE)
+  apiKeyList.value = await ApiKeyApi.getApiKeySimpleList()
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
@@ -146,12 +179,17 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    const data = formData.value as unknown as ChatModelVO
+    const data = formData.value as unknown as ModelVO
+    if (data.type !== AiModelTypeEnum.CHAT) {
+      delete data.temperature
+      delete data.maxTokens
+      delete data.maxContexts
+    }
     if (formType.value === 'create') {
-      await ChatModelApi.createChatModel(data)
+      await ModelApi.createModel(data)
       message.success(t('common.createSuccess'))
     } else {
-      await ChatModelApi.updateChatModel(data)
+      await ModelApi.updateModel(data)
       message.success(t('common.updateSuccess'))
     }
     dialogVisible.value = false
@@ -170,6 +208,7 @@ const resetForm = () => {
     name: undefined,
     model: undefined,
     platform: undefined,
+    type: undefined,
     sort: undefined,
     status: CommonStatusEnum.ENABLE,
     temperature: undefined,

+ 27 - 20
src/views/ai/model/chatModel/index.vue → src/views/ai/model/model/index.vue

@@ -1,4 +1,6 @@
 <template>
+  <doc-alert title="AI 手册" url="https://doc.iocoder.cn/ai/build/" />
+
   <ContentWrap>
     <!-- 搜索工作栏 -->
     <el-form
@@ -42,7 +44,7 @@
           type="primary"
           plain
           @click="openForm('create')"
-          v-hasPermi="['ai:chat-model:create']"
+          v-hasPermi="['ai:model:create']"
         >
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
@@ -53,34 +55,39 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="所属平台" align="center" prop="platform">
+      <el-table-column label="所属平台" align="center" prop="platform" min-width="100">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.AI_PLATFORM" :value="scope.row.platform" />
         </template>
       </el-table-column>
-      <el-table-column label="模型名字" align="center" prop="name" min-width="120" />
-      <el-table-column label="模型标识" align="center" prop="model" min-width="120" />
+      <el-table-column label="模型类型" align="center" prop="platform" min-width="100">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.AI_MODEL_TYPE" :value="scope.row.type" />
+        </template>
+      </el-table-column>
+      <el-table-column label="模型名字" align="center" prop="name" min-width="180" />
+      <el-table-column label="模型标识" align="center" prop="model" min-width="180" />
       <el-table-column label="API 秘钥" align="center" prop="keyId" min-width="140">
         <template #default="scope">
           <span>{{ apiKeyList.find((item) => item.id === scope.row.keyId)?.name }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="排序" align="center" prop="sort" />
-      <el-table-column label="状态" align="center" prop="status">
+      <el-table-column label="排序" align="center" prop="sort" min-width="80" />
+      <el-table-column label="状态" align="center" prop="status" min-width="80">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
         </template>
       </el-table-column>
-      <el-table-column label="温度参数" align="center" prop="temperature" />
+      <el-table-column label="温度参数" align="center" prop="temperature" min-width="80" />
       <el-table-column label="回复数 Token 数" align="center" prop="maxTokens" min-width="140" />
-      <el-table-column label="上下文数量" align="center" prop="maxContexts" />
-      <el-table-column label="操作" align="center">
+      <el-table-column label="上下文数量" align="center" prop="maxContexts" min-width="100" />
+      <el-table-column label="操作" align="center" width="180" fixed="right">
         <template #default="scope">
           <el-button
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
-            v-hasPermi="['ai:chat-model:update']"
+            v-hasPermi="['ai:model:update']"
           >
             编辑
           </el-button>
@@ -88,7 +95,7 @@
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['ai:chat-model:delete']"
+            v-hasPermi="['ai:model:delete']"
           >
             删除
           </el-button>
@@ -105,23 +112,23 @@
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->
-  <ChatModelForm ref="formRef" @success="getList" />
+  <ModelForm ref="formRef" @success="getList" />
 </template>
 
 <script setup lang="ts">
-import { ChatModelApi, ChatModelVO } from '@/api/ai/model/chatModel'
-import ChatModelForm from './ChatModelForm.vue'
+import { ModelApi, ModelVO } from '@/api/ai/model/model'
+import ModelForm from './ModelForm.vue'
 import { DICT_TYPE } from '@/utils/dict'
 import { ApiKeyApi, ApiKeyVO } from '@/api/ai/model/apiKey'
 
-/** API 聊天模型 列表 */
-defineOptions({ name: 'AiChatModel' })
+/** API 模型列表 */
+defineOptions({ name: 'AiModel' })
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
 const loading = ref(true) // 列表的加载中
-const list = ref<ChatModelVO[]>([]) // 列表的数据
+const list = ref<ModelVO[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
 const queryParams = reactive({
   pageNo: 1,
@@ -137,7 +144,7 @@ const apiKeyList = ref([] as ApiKeyVO[]) // API 密钥列表
 const getList = async () => {
   loading.value = true
   try {
-    const data = await ChatModelApi.getChatModelPage(queryParams)
+    const data = await ModelApi.getModelPage(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -169,7 +176,7 @@ const handleDelete = async (id: number) => {
     // 删除的二次确认
     await message.delConfirm()
     // 发起删除
-    await ChatModelApi.deleteChatModel(id)
+    await ModelApi.deleteModel(id)
     message.success(t('common.delSuccess'))
     // 刷新列表
     await getList()
@@ -178,7 +185,7 @@ const handleDelete = async (id: number) => {
 
 /** 初始化 **/
 onMounted(async () => {
-  getList()
+  await getList()
   // 获得下拉数据
   apiKeyList.value = await ApiKeyApi.getApiKeySimpleList()
 })

+ 112 - 0
src/views/ai/model/tool/ToolForm.vue

@@ -0,0 +1,112 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="工具名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入工具名称" />
+      </el-form-item>
+      <el-form-item label="工具描述" prop="description">
+        <el-input type="textarea" v-model="formData.description" placeholder="请输入工具描述" />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { ToolApi, ToolVO } from '@/api/ai/model/tool'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** AI 工具表单 */
+defineOptions({ name: 'ToolForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  description: undefined,
+  status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive({
+  name: [{ required: true, message: '工具名称不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await ToolApi.getTool(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as ToolVO
+    if (formType.value === 'create') {
+      await ToolApi.createTool(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await ToolApi.updateTool(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    description: undefined,
+    status: CommonStatusEnum.ENABLE
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 178 - 0
src/views/ai/model/tool/index.vue

@@ -0,0 +1,178 @@
+<template>
+  <doc-alert title="AI 工具调用(function calling)" url="https://doc.iocoder.cn/ai/tool/" />
+
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="工具名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入工具名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-220px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button type="primary" plain @click="openForm('create')" v-hasPermi="['ai:tool:create']">
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="工具编号" align="center" prop="id" />
+      <el-table-column label="工具名称" align="center" prop="name" />
+      <el-table-column label="工具描述" align="center" prop="description" />
+      <el-table-column label="状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center" min-width="120px">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['ai:tool:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['ai:tool:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <ToolForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { ToolApi, ToolVO } from '@/api/ai/model/tool'
+import ToolForm from './ToolForm.vue'
+
+/** AI 工具 列表 */
+defineOptions({ name: 'AiTool' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<ToolVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  description: undefined,
+  status: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ToolApi.getToolPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await ToolApi.deleteTool(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 2 - 0
src/views/ai/music/manager/index.vue

@@ -1,4 +1,6 @@
 <template>
+  <doc-alert title="AI 音乐创作" url="https://doc.iocoder.cn/ai/music/" />
+
   <ContentWrap>
     <!-- 搜索工作栏 -->
     <el-form

+ 9 - 25
src/views/ai/utils/constants.ts

@@ -23,6 +23,15 @@ export const AiPlatformEnum = {
   SUNO: 'Suno' // Suno AI
 }
 
+export const AiModelTypeEnum = {
+  CHAT: 1, // 聊天
+  IMAGE: 2, // 图像
+  VOICE: 3, // 音频
+  VIDEO: 4, // 视频
+  EMBEDDING: 5, // 向量
+  RERANK: 6 // 重排
+}
+
 export const OtherPlatformEnum: ImageModelVO[] = [
   {
     key: AiPlatformEnum.TONG_YI,
@@ -211,31 +220,6 @@ export const StableDiffusionStylePresets: ImageModelVO[] = [
   }
 ]
 
-export const TongYiWanXiangModels: ImageModelVO[] = [
-  {
-    key: 'wanx-v1',
-    name: 'wanx-v1'
-  },
-  {
-    key: 'wanx-sketch-to-image-v1',
-    name: 'wanx-sketch-to-image-v1'
-  }
-]
-
-export const QianFanModels: ImageModelVO[] = [
-  {
-    key: 'sd_xl',
-    name: 'sd_xl'
-  }
-]
-
-export const ChatGlmModels: ImageModelVO[] = [
-  {
-    key: 'cogview-3',
-    name: 'cogview-3'
-  }
-]
-
 export const StableDiffusionClipGuidancePresets: ImageModelVO[] = [
   {
     key: 'NONE',

+ 8 - 37
src/views/ai/write/manager/index.vue

@@ -1,4 +1,6 @@
 <template>
+  <doc-alert title="AI 写作助手" url="https://doc.iocoder.cn/ai/write/" />
+
   <ContentWrap>
     <!-- 搜索工作栏 -->
     <el-form
@@ -39,7 +41,12 @@
         </el-select>
       </el-form-item>
       <el-form-item label="平台" prop="platform">
-        <el-select v-model="queryParams.platform" placeholder="请选择平台" clearable class="!w-240px">
+        <el-select
+          v-model="queryParams.platform"
+          placeholder="请选择平台"
+          clearable
+          class="!w-240px"
+        >
           <el-option
             v-for="dict in getStrDictOptions(DICT_TYPE.AI_PLATFORM)"
             :key="dict.value"
@@ -62,24 +69,6 @@
       <el-form-item>
         <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
         <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
-        <el-button
-          type="primary"
-          plain
-          @click="openForm('create')"
-          v-hasPermi="['ai:write:create']"
-        >
-          <Icon icon="ep:plus" class="mr-5px" /> 新增
-        </el-button>
-        <!-- TODO @YunaiV  目前没有导出接口,需要导出吗 -->
-        <el-button
-          type="success"
-          plain
-          @click="handleExport"
-          :loading="exportLoading"
-          v-hasPermi="['ai:write:export']"
-        >
-          <Icon icon="ep:download" class="mr-5px" /> 导出
-        </el-button>
       </el-form-item>
     </el-form>
   </ContentWrap>
@@ -143,15 +132,6 @@
       <el-table-column label="错误信息" align="center" prop="errorMessage" />
       <el-table-column label="操作" align="center">
         <template #default="scope">
-<!--          TODO @YunaiV 目前没有修改接口,写作要可以更改吗-->
-          <el-button
-            link
-            type="primary"
-            @click="openForm('update', scope.row.id)"
-            v-hasPermi="['ai:write:update']"
-          >
-            编辑
-          </el-button>
           <el-button
             link
             type="danger"
@@ -225,15 +205,6 @@ const resetQuery = () => {
   handleQuery()
 }
 
-/** 新增方法,跳转到写作页面 **/
-const openForm = (type: string, id?: number) => {
-  switch (type) {
-    case 'create':
-      router.push('/ai/write')
-      break
-  }
-}
-
 /** 删除按钮操作 */
 const handleDelete = async (id: number) => {
   try {

+ 9 - 9
src/views/bpm/model/CategoryDraggableModel.vue

@@ -89,7 +89,7 @@
               </el-tooltip>
               <el-image v-if="row.icon" :src="row.icon" class="h-38px w-38px mr-10px rounded" />
               <div v-else class="flow-icon">
-                <span style="font-size: 12px; color: #fff">{{ sliceName(row.name,0,2) }}</span>
+                <span style="font-size: 12px; color: #fff">{{ subString(row.name, 0, 2) }}</span>
               </div>
               {{ row.name }}
             </div>
@@ -113,6 +113,11 @@
             </el-text>
           </template>
         </el-table-column>
+        <el-table-column label="流程类型" prop="type" min-width="120">
+          <template #default="{ row }">
+            <dict-tag :value="row.type" :type="DICT_TYPE.BPM_MODEL_TYPE" />
+          </template>
+        </el-table-column>
         <el-table-column label="表单信息" prop="formType" min-width="150">
           <template #default="scope">
             <el-button
@@ -260,6 +265,7 @@
 </template>
 
 <script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
 import { CategoryApi, CategoryVO } from '@/api/bpm/category'
 import Sortable from 'sortablejs'
 import { formatDate } from '@/utils/formatTime'
@@ -271,9 +277,8 @@ import { checkPermi } from '@/utils/permission'
 import { useUserStoreWithOut } from '@/store/modules/user'
 import { useAppStore } from '@/store/modules/app'
 import { cloneDeep, isEqual } from 'lodash-es'
-import { useTagsView } from '@/hooks/web/useTagsView'
 import { useDebounceFn } from '@vueuse/core'
-import { sliceName } from '@/utils/index'
+import { subString } from '@/utils/index'
 
 defineOptions({ name: 'BpmModel' })
 
@@ -582,8 +587,7 @@ const handleDeleteCategory = async () => {
   } catch {}
 }
 
-/** 添加流程模型弹窗 */
-const tagsView = useTagsView()
+/** 添加/修改/复制流程模型弹窗 */
 const openModelForm = async (type: string, id?: number) => {
   if (type === 'create') {
     await push({ name: 'BpmModelCreate' })
@@ -592,10 +596,6 @@ const openModelForm = async (type: string, id?: number) => {
       name: 'BpmModelUpdate',
       params: { id, type }
     })
-    // 设置标题
-    if (type === 'copy') {
-      tagsView.setTitle('复制流程')
-    }
   }
 }
 

+ 73 - 42
src/views/bpm/definition/index.vue → src/views/bpm/model/definition/index.vue

@@ -3,40 +3,60 @@
 
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
-      <el-table-column label="定义编号" align="center" prop="id" width="400" />
-      <el-table-column label="流程名称" align="center" prop="name" width="200">
-        <template #default="scope">
-          <el-button type="primary" link @click="handleBpmnDetail(scope.row)">
-            <span>{{ scope.row.name }}</span>
-          </el-button>
+      <el-table-column label="定义编号" align="center" prop="id" min-width="250" />
+      <el-table-column label="流程名称" align="center" prop="name" min-width="150" />
+      <el-table-column label="流程图标" align="center" min-width="50">
+        <template #default="{ row }">
+          <el-image v-if="row.icon" :src="row.icon" class="h-24px w-24pxrounded" />
+        </template>
+      </el-table-column>
+      <el-table-column label="可见范围" prop="startUserIds" min-width="100">
+        <template #default="{ row }">
+          <el-text v-if="!row.startUsers?.length"> 全部可见 </el-text>
+          <el-text v-else-if="row.startUsers.length === 1">
+            {{ row.startUsers[0].nickname }}
+          </el-text>
+          <el-text v-else>
+            <el-tooltip
+              class="box-item"
+              effect="dark"
+              placement="top"
+              :content="row.startUsers.map((user: any) => user.nickname).join('、')"
+            >
+              {{ row.startUsers[0].nickname }}等 {{ row.startUsers.length }} 人可见
+            </el-tooltip>
+          </el-text>
         </template>
       </el-table-column>
-      <el-table-column label="定义分类" align="center" prop="categoryName" width="100" />
-      <el-table-column label="表单信息" align="center" prop="formType" width="200">
+      <el-table-column label="流程类型" prop="modelType" min-width="120">
+        <template #default="{ row }">
+          <dict-tag :value="row.modelType" :type="DICT_TYPE.BPM_MODEL_TYPE" />
+        </template>
+      </el-table-column>
+      <el-table-column label="表单信息" prop="formType" min-width="150">
         <template #default="scope">
           <el-button
-            v-if="scope.row.formType === 10"
+            v-if="scope.row.formType === BpmModelFormType.NORMAL"
             type="primary"
             link
             @click="handleFormDetail(scope.row)"
           >
             <span>{{ scope.row.formName }}</span>
           </el-button>
-          <el-button v-else type="primary" link @click="handleFormDetail(scope.row)">
+          <el-button
+            v-else-if="scope.row.formType === BpmModelFormType.CUSTOM"
+            type="primary"
+            link
+            @click="handleFormDetail(scope.row)"
+          >
             <span>{{ scope.row.formCustomCreatePath }}</span>
           </el-button>
+          <label v-else>暂无表单</label>
         </template>
       </el-table-column>
-      <el-table-column label="流程版本" align="center" prop="processDefinition.version" width="80">
+      <el-table-column label="流程版本" align="center" min-width="80">
         <template #default="scope">
-          <el-tag v-if="scope.row">v{{ scope.row.version }}</el-tag>
-          <el-tag type="warning" v-else>未部署</el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column label="状态" align="center" prop="version" width="80">
-        <template #default="scope">
-          <el-tag type="success" v-if="scope.row.suspensionState === 1">激活</el-tag>
-          <el-tag type="warning" v-if="scope.row.suspensionState === 2">挂起</el-tag>
+          <el-tag>v{{ scope.row.version }}</el-tag>
         </template>
       </el-table-column>
       <el-table-column
@@ -46,13 +66,18 @@
         width="180"
         :formatter="dateFormatter"
       />
-      <el-table-column
-        label="定义描述"
-        align="center"
-        prop="description"
-        width="300"
-        show-overflow-tooltip
-      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openModelForm(scope.row.id)"
+            v-hasPermi="['bpm:model:update']"
+          >
+            恢复
+          </el-button>
+        </template>
+      </el-table-column>
     </el-table>
     <!-- 分页 -->
     <Pagination
@@ -67,18 +92,14 @@
   <Dialog title="表单详情" v-model="formDetailVisible" width="800">
     <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
   </Dialog>
-
-  <!-- 弹窗:流程模型图的预览 -->
-  <Dialog title="流程图" v-model="bpmnDetailVisible" width="800">
-    <MyProcessViewer style="height: 700px" key="designer" :xml="bpmnXml" />
-  </Dialog>
 </template>
 
 <script lang="ts" setup>
 import { dateFormatter } from '@/utils/formatTime'
-import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
 import * as DefinitionApi from '@/api/bpm/definition'
 import { setConfAndFields2 } from '@/utils/formCreate'
+import { DICT_TYPE } from '@/utils/dict'
+import { BpmModelFormType } from '@/utils/constants'
 
 defineOptions({ name: 'BpmProcessDefinition' })
 
@@ -113,7 +134,7 @@ const formDetailPreview = ref({
   option: {}
 })
 const handleFormDetail = async (row: any) => {
-  if (row.formType == 10) {
+  if (row.formType == BpmModelFormType.NORMAL) {
     // 设置表单
     setConfAndFields2(formDetailPreview, row.formConf, row.formFields)
     // 弹窗打开
@@ -125,15 +146,12 @@ const handleFormDetail = async (row: any) => {
   }
 }
 
-/** 流程图的详情按钮操作 */
-const bpmnDetailVisible = ref(false)
-const bpmnXml = ref('')
-const handleBpmnDetail = async (row: any) => {
-  // 设置可见
-  bpmnXml.value = ''
-  bpmnDetailVisible.value = true
-  // 加载 BPMN XML
-  bpmnXml.value = (await DefinitionApi.getProcessDefinition(row.id))?.bpmnXml
+/** 恢复流程模型弹窗 */
+const openModelForm = async (id?: number) => {
+  await push({
+    name: 'BpmModelUpdate',
+    params: { id, type: 'definition' }
+  })
 }
 
 /** 初始化 **/
@@ -141,3 +159,16 @@ onMounted(() => {
   getList()
 })
 </script>
+
+<style lang="scss" scoped>
+.flow-icon {
+  display: flex;
+  width: 38px;
+  height: 38px;
+  margin-right: 10px;
+  background-color: var(--el-color-primary);
+  border-radius: 0.25rem;
+  align-items: center;
+  justify-content: center;
+}
+</style>

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

@@ -140,6 +140,46 @@
         </el-select>
       </div>
     </el-form-item>
+    <el-form-item class="mb-20px">
+      <template #label>
+        <el-text size="large" tag="b">流程前置通知</el-text>
+      </template>
+      <div class="flex flex-col w-100%">
+        <div class="flex">
+          <el-switch
+            v-model="processBeforeTriggerEnable"
+            @change="handlePreProcessNotifyEnableChange"
+          />
+          <div class="ml-80px">流程启动后通知</div>
+        </div>
+        <HttpRequestSetting
+          v-if="processBeforeTriggerEnable"
+          v-model:setting="modelData.processBeforeTriggerSetting"
+          :responseEnable="true"
+          :formItemPrefix="'processBeforeTriggerSetting'"
+        />
+      </div>
+    </el-form-item>
+    <el-form-item class="mb-20px">
+      <template #label>
+        <el-text size="large" tag="b">流程后置通知</el-text>
+      </template>
+      <div class="flex flex-col w-100%">
+        <div class="flex">
+          <el-switch
+            v-model="processAfterTriggerEnable"
+            @change="handlePostProcessNotifyEnableChange"
+          />
+          <div class="ml-80px">流程启动后通知</div>
+        </div>
+        <HttpRequestSetting
+          v-if="processAfterTriggerEnable"
+          v-model:setting="modelData.processAfterTriggerSetting"
+          :responseEnable="true"
+          :formItemPrefix="'processAfterTriggerSetting'"
+        />
+      </div>
+    </el-form-item>
   </el-form>
 </template>
 
@@ -149,6 +189,7 @@ import { BpmAutoApproveType, BpmModelFormType } from '@/utils/constants'
 import * as FormApi from '@/api/bpm/form'
 import { parseFormFields } from '@/components/FormCreate/src/utils'
 import { ProcessVariableEnum } from '@/components/SimpleProcessDesignerV2/src/consts'
+import HttpRequestSetting from '@/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestSetting.vue'
 
 const modelData = defineModel<any>()
 
@@ -205,6 +246,36 @@ const numberExample = computed(() => {
   }
 })
 
+/** 是否开启流程前置通知 */
+const processBeforeTriggerEnable = ref(false)
+const handlePreProcessNotifyEnableChange = (val: boolean | string | number) => {
+  if (val) {
+    modelData.value.processBeforeTriggerSetting = {
+      url: '',
+      header: [],
+      body: [],
+      response: []
+    }
+  } else {
+    modelData.value.processBeforeTriggerSetting = null
+  }
+}
+
+/** 是否开启流程后置通知 */
+const processAfterTriggerEnable = ref(false)
+const handlePostProcessNotifyEnableChange = (val: boolean | string | number) => {
+  if (val) {
+    modelData.value.processAfterTriggerSetting = {
+      url: '',
+      header: [],
+      body: [],
+      response: []
+    }
+  } else {
+    modelData.value.processAfterTriggerSetting = null
+  }
+}
+
 /** 表单选项 */
 const formField = ref<Array<{ field: string; title: string }>>([])
 const formFieldOptions4Title = computed(() => {
@@ -264,6 +335,12 @@ const initData = () => {
       summary: []
     }
   }
+  if (modelData.value.processBeforeTriggerSetting) {
+    processBeforeTriggerEnable.value = true
+  }
+  if (modelData.value.processAfterTriggerSetting) {
+    processAfterTriggerEnable.value = true
+  }
 }
 defineExpose({ initData })
 

+ 6 - 5
src/views/bpm/model/form/FormDesign.vue

@@ -11,12 +11,12 @@
         </el-radio>
       </el-radio-group>
     </el-form-item>
-    <el-form-item v-if="modelData.formType === 10" label="流程表单" prop="formId">
+    <el-form-item v-if="modelData.formType === BpmModelFormType.NORMAL" label="流程表单" prop="formId">
       <el-select v-model="modelData.formId" clearable style="width: 100%">
         <el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" />
       </el-select>
     </el-form-item>
-    <el-form-item v-if="modelData.formType === 20" label="表单提交路由" prop="formCustomCreatePath">
+    <el-form-item v-if="modelData.formType === BpmModelFormType.CUSTOM" label="表单提交路由" prop="formCustomCreatePath">
       <el-input
         v-model="modelData.formCustomCreatePath"
         placeholder="请输入表单提交路由"
@@ -31,7 +31,7 @@
         <Icon icon="ep:question" class="ml-5px" />
       </el-tooltip>
     </el-form-item>
-    <el-form-item v-if="modelData.formType === 20" label="表单查看地址" prop="formCustomViewPath">
+    <el-form-item v-if="modelData.formType === BpmModelFormType.CUSTOM" label="表单查看地址" prop="formCustomViewPath">
       <el-input
         v-model="modelData.formCustomViewPath"
         placeholder="请输入表单查看的组件地址"
@@ -48,7 +48,7 @@
     </el-form-item>
     <!-- 表单预览 -->
     <div
-      v-if="modelData.formType === 10 && modelData.formId && formPreview.rule.length > 0"
+      v-if="modelData.formType === BpmModelFormType.NORMAL && modelData.formId && formPreview.rule.length > 0"
       class="mt-20px"
     >
       <div class="flex items-center mb-15px">
@@ -68,6 +68,7 @@
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import * as FormApi from '@/api/bpm/form'
 import { setConfAndFields2 } from '@/utils/formCreate'
+import { BpmModelFormType } from '@/utils/constants'
 
 const props = defineProps({
   formList: {
@@ -96,7 +97,7 @@ const formPreview = ref({
 watch(
   () => modelData.value.formId,
   async (newFormId) => {
-    if (newFormId && modelData.value.formType === 10) {
+    if (newFormId && modelData.value.formType === BpmModelFormType.NORMAL) {
       const data = await FormApi.getForm(newFormId)
       setConfAndFields2(formPreview.value, data.conf, data.fields)
       // 设置只读

+ 1 - 1
src/views/bpm/model/form/ProcessDesign.vue

@@ -25,7 +25,7 @@
 
 <script lang="ts" setup>
 import { BpmModelType } from '@/utils/constants'
-import BpmModelEditor from '../editor/index.vue'
+import BpmModelEditor from './editor/index.vue'
 import SimpleModelDesign from '../../simple/SimpleModelDesign.vue'
 
 // 创建本地数据副本

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است