Merge commit '20a2ced413d4b98488f58b65f149347ac847801d'
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 25 KiB |
|
|
@ -87,7 +87,7 @@
|
|||
"source.fixAll.stylelint": "explicit"
|
||||
},
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "Vue.volar"
|
||||
"editor.defaultFormatter": "octref.vetur"
|
||||
},
|
||||
"i18n-ally.localesPaths": ["src/locales"],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
|
|
|
|||
112
README.md
|
|
@ -81,16 +81,13 @@
|
|||
|
||||
系统内置多种多种业务功能,可以用于快速你的业务系统:
|
||||
|
||||
* 系统功能
|
||||
* 基础设施
|
||||
* 工作流程
|
||||
* 支付系统
|
||||
* 会员中心
|
||||
* 数据报表
|
||||
* 商城系统
|
||||
* 微信公众号
|
||||
* ERP 系统
|
||||
* CRM 系统
|
||||
系统内置多种多种业务功能,可以用于快速你的业务系统:
|
||||
|
||||

|
||||
|
||||
* 通用模块(必选):系统功能、基础设施
|
||||
* 通用模块(可选):工作流程、支付系统、数据报表、会员中心
|
||||
* 业务系统(按需):ERP 系统、CRM 系统、商城系统、微信公众号、AI 大模型
|
||||
|
||||
### 系统功能
|
||||
|
||||
|
|
@ -120,54 +117,77 @@
|
|||
|
||||
### 工作流程
|
||||
|
||||
| | 功能 | 描述 |
|
||||
|----|-------|-----------------------------------------|
|
||||
| 🚀 | 流程模型 | 配置工作流的流程模型,支持 BPMN 和仿钉钉/飞书设计器 |
|
||||
| 🚀 | 流程表单 | 拖动表单元素生成相应的工作流表单,覆盖 Element UI 所有的表单组件 |
|
||||
| 🚀 | 用户分组 | 自定义用户分组,可用于工作流的审批分组 |
|
||||
| 🚀 | 我的流程 | 查看我发起的工作流程,支持新建、取消流程等操作,高亮流程图、审批时间线 |
|
||||
| 🚀 | 待办任务 | 查看自己【未】审批的工作任务,支持通过、不通过、转派、委派、退回、加减签等操作 |
|
||||
| 🚀 | 已办任务 | 查看自己【已】审批的工作任务,支持流程预测,展示未来审批人信息 |
|
||||
| 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 |
|
||||
|
||||

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

|
||||
|
||||
|
|
@ -213,6 +233,14 @@ ps:核心功能已经实现,正在对接微信小程序中...
|
|||
|
||||

|
||||
|
||||
### AI 大模型
|
||||
|
||||
演示地址:<https://doc.iocoder.cn/ai-preview/>
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 🐷 演示图
|
||||
|
||||
### 系统功能
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
13048
pnpm-lock.yaml
|
|
@ -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 聊天
|
||||
|
|
|
|||
|
|
@ -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 // 图片高度
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -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` })
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,8 @@ export interface ChatRoleVO {
|
|||
welcomeMessage: string // 角色设定
|
||||
publicStatus: boolean // 是否公开
|
||||
status: number // 状态
|
||||
knowledgeIds?: number[] // 引用的知识库 ID 列表
|
||||
toolIds?: number[] // 引用的工具 ID 列表
|
||||
}
|
||||
|
||||
// AI 聊天角色 分页请求 vo
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -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` })
|
||||
}
|
||||
}
|
||||
|
|
@ -20,3 +20,9 @@ export const getProcessDefinitionList = async (params) => {
|
|||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const getSimpleProcessDefinitionList = async () => {
|
||||
return await request.get({
|
||||
url: '/bpm/process-definition/simple-list'
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
|
||||
// 获取表单字段权限
|
||||
|
|
|
|||
|
|
@ -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 } })
|
||||
}
|
||||
}
|
||||
|
|
@ -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` })
|
||||
}
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -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' })
|
||||
}
|
||||
}
|
||||
|
|
@ -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' })
|
||||
}
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 9.8 KiB |
|
|
@ -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>
|
||||
|
After Width: | Height: | Size: 465 B |
|
|
@ -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>
|
||||
|
After Width: | Height: | Size: 964 B |
|
|
@ -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>
|
||||
|
After Width: | Height: | Size: 697 B |
|
|
@ -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>
|
||||
|
After Width: | Height: | Size: 1011 B |
|
|
@ -105,7 +105,7 @@ const editorConfig = computed((): IEditorConfig => {
|
|||
},
|
||||
|
||||
// 超时时间,默认为 10 秒
|
||||
timeout: 5 * 1000, // 5 秒
|
||||
timeout: 15 * 1000, // 15 秒
|
||||
|
||||
// form-data fieldName,后端接口参数名称,默认值wangeditor-uploaded-image
|
||||
fieldName: 'file',
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ watch(
|
|||
|
||||
<template>
|
||||
<ElIcon :class="prefixCls" :color="color" :size="size">
|
||||
<svg v-if="isLocal" :class="getSvgClass" aria-hidden="true">
|
||||
<svg v-if="isLocal" :class="getSvgClass">
|
||||
<use :xlink:href="symbolId" />
|
||||
</svg>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import SimpleProcessModel from './SimpleProcessModel.vue'
|
||||
import { updateBpmSimpleModel, getBpmSimpleModel } from '@/api/bpm/simple'
|
||||
import { SimpleFlowNode, NodeType, NodeId, NODE_DEFAULT_TEXT } from './consts'
|
||||
import { getModel } from '@/api/bpm/model'
|
||||
import { getForm, FormVO } from '@/api/bpm/form'
|
||||
|
|
@ -35,6 +34,7 @@ import * as DeptApi from '@/api/system/dept'
|
|||
import * as PostApi from '@/api/system/post'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
import * as UserGroupApi from '@/api/bpm/userGroup'
|
||||
import { BpmModelFormType } from '@/utils/constants'
|
||||
|
||||
defineOptions({
|
||||
name: 'SimpleProcessDesigner'
|
||||
|
|
@ -168,7 +168,7 @@ onMounted(async () => {
|
|||
const bpmnModel = await getModel(props.modelId)
|
||||
if (bpmnModel) {
|
||||
formType.value = bpmnModel.formType
|
||||
if (formType.value === 10) {
|
||||
if (formType.value === BpmModelFormType.NORMAL && bpmnModel.formId) {
|
||||
const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO
|
||||
formFields.value = bpmnForm?.fields
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -45,4 +45,3 @@ watch(
|
|||
provide('tasks', approveTasks)
|
||||
provide('processInstance', currentProcessInstance)
|
||||
</script>
|
||||
p
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -36,147 +36,198 @@
|
|||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- HTTP 请求触发器 -->
|
||||
<div
|
||||
v-if="configForm.type === TriggerTypeEnum.HTTP_REQUEST && configForm.httpRequestSetting"
|
||||
>
|
||||
<el-form-item>
|
||||
<el-alert
|
||||
title="仅支持 POST 请求,以请求体方式接收参数"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 请求地址-->
|
||||
<el-form-item label="请求地址" prop="httpRequestSetting.url">
|
||||
<el-input v-model="configForm.httpRequestSetting.url" />
|
||||
</el-form-item>
|
||||
<!-- 请求头,请求体设置-->
|
||||
<HttpRequestParamSetting
|
||||
:header="configForm.httpRequestSetting.header"
|
||||
:body="configForm.httpRequestSetting.body"
|
||||
:bind="'httpRequestSetting'"
|
||||
/>
|
||||
<!-- 返回值设置-->
|
||||
<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'
|
||||
}"
|
||||
>
|
||||
<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="`httpRequestSetting.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(configForm.httpRequestSetting.response!, index)"
|
||||
/>
|
||||
</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
|
||||
[TriggerTypeEnum.HTTP_REQUEST, TriggerTypeEnum.HTTP_CALLBACK].includes(
|
||||
configForm.type
|
||||
) && configForm.httpRequestSetting
|
||||
"
|
||||
>
|
||||
<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>
|
||||
<HttpRequestSetting
|
||||
v-model:setting="configForm.httpRequestSetting"
|
||||
:responseEnable="configForm.type === TriggerTypeEnum.HTTP_REQUEST"
|
||||
:formItemPrefix="'httpRequestSetting'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 表单数据修改触发器 -->
|
||||
<div v-if="configForm.type === TriggerTypeEnum.FORM_UPDATE">
|
||||
<div v-for="(formSetting, index) in configForm.formSettings" :key="index">
|
||||
<el-card class="w-580px mt-4">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>修改表单设置 {{ index + 1 }}</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
circle
|
||||
v-if="configForm.formSettings!.length > 1"
|
||||
@click="deleteFormSetting(index)"
|
||||
>
|
||||
<Icon icon="ep:close" />
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 条件设置 -->
|
||||
<ConditionDialog
|
||||
:ref="`condition-${index}`"
|
||||
@update-condition="(val) => handleConditionUpdate(index, val)"
|
||||
/>
|
||||
<div class="cursor-pointer" v-if="formSetting.conditionType">
|
||||
<el-tag
|
||||
type="success"
|
||||
effect="light"
|
||||
closable
|
||||
@close="deleteFormSettingCondition(formSetting)"
|
||||
@click="openFormSettingCondition(index, formSetting)"
|
||||
>
|
||||
{{ showConditionText(formSetting) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-button
|
||||
v-else
|
||||
type="primary"
|
||||
text
|
||||
@click="addFormSettingCondition(index, formSetting)"
|
||||
>
|
||||
<Icon icon="ep:link" class="mr-5px" />添加条件
|
||||
</el-button>
|
||||
<el-divider content-position="left">修改表单字段设置</el-divider>
|
||||
<!-- 表单字段修改设置 -->
|
||||
<div
|
||||
class="flex items-center"
|
||||
v-for="key in Object.keys(formSetting.updateFormFields || {})"
|
||||
:key="key"
|
||||
>
|
||||
<div class="mr-2 flex items-center">
|
||||
<el-form-item>
|
||||
<el-select
|
||||
class="w-160px!"
|
||||
:model-value="key"
|
||||
@update:model-value="(newKey) => updateFormFieldKey(formSetting, key, newKey)"
|
||||
placeholder="请选择表单字段"
|
||||
:disabled="key !== ''"
|
||||
>
|
||||
<el-option
|
||||
v-for="(field, fIdx) in optionalUpdateFormFields"
|
||||
:key="fIdx"
|
||||
:label="field.title"
|
||||
:value="field.field"
|
||||
:disabled="field.disabled"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="mx-2"><el-form-item>的值设置为</el-form-item></div>
|
||||
<div class="mr-2">
|
||||
<el-form-item
|
||||
:prop="`formSettings.${index}.updateFormFields.${key}`"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '值不能为空',
|
||||
trigger: 'blur'
|
||||
}"
|
||||
>
|
||||
<el-input
|
||||
class="w-160px"
|
||||
v-model="formSetting.updateFormFields![key]"
|
||||
placeholder="请输入"
|
||||
:disabled="!key"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="mr-1 pt-1 cursor-pointer">
|
||||
<el-form-item>
|
||||
<Icon
|
||||
icon="ep:delete"
|
||||
:size="18"
|
||||
@click="deleteFormFieldSetting(formSetting, key)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加表单字段按钮 -->
|
||||
<el-button type="primary" text @click="addFormFieldSetting(formSetting)">
|
||||
<Icon icon="ep:memo" class="mr-5px" />添加修改字段
|
||||
</el-button>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 添加新的设置 -->
|
||||
<el-button class="mt-6" type="primary" text @click="addFormSetting">
|
||||
<Icon icon="ep:setting" class="mr-5px" />添加设置
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 表单数据删除触发器 -->
|
||||
<div v-if="configForm.type === TriggerTypeEnum.FORM_DELETE">
|
||||
<div v-for="(formSetting, index) in configForm.formSettings" :key="index">
|
||||
<el-card class="w-580px mt-4">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>删除表单设置 {{ index + 1 }}</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
circle
|
||||
v-if="configForm.formSettings!.length > 1"
|
||||
@click="deleteFormSetting(index)"
|
||||
>
|
||||
<Icon icon="ep:close" />
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 条件设置 -->
|
||||
<ConditionDialog
|
||||
:ref="`condition-${index}`"
|
||||
@update-condition="(val) => handleConditionUpdate(index, val)"
|
||||
/>
|
||||
<div class="cursor-pointer" v-if="formSetting.conditionType">
|
||||
<el-tag
|
||||
type="warning"
|
||||
effect="light"
|
||||
closable
|
||||
@close="deleteFormSettingCondition(formSetting)"
|
||||
@click="openFormSettingCondition(index, formSetting)"
|
||||
>
|
||||
{{ showConditionText(formSetting) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-button
|
||||
v-else
|
||||
type="primary"
|
||||
text
|
||||
@click="addFormSettingCondition(index, formSetting)"
|
||||
>
|
||||
<Icon icon="ep:link" class="mr-5px" />添加条件
|
||||
</el-button>
|
||||
|
||||
<el-divider content-position="left">删除表单字段设置</el-divider>
|
||||
<!-- 表单字段删除设置 -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<el-select
|
||||
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 addFormFieldSetting = () => {
|
||||
if (configForm.value.normalFormSetting!.updateFormFields === undefined) {
|
||||
configForm.value.normalFormSetting!.updateFormFields = {}
|
||||
}
|
||||
configForm.value.normalFormSetting!.updateFormFields[''] = undefined
|
||||
/** 添加条件配置 */
|
||||
const addFormSettingCondition = (index: number, formSetting: FormTriggerSetting) => {
|
||||
const conditionDialog = proxy.$refs[`condition-${index}`][0]
|
||||
conditionDialog.open(formSetting)
|
||||
}
|
||||
/** 删除修改表单设置项 */
|
||||
const deleteFormFieldSetting = (key: string) => {
|
||||
if (!configForm.value.normalFormSetting?.updateFormFields) return
|
||||
delete configForm.value.normalFormSetting.updateFormFields[key]
|
||||
/** 删除条件配置 */
|
||||
const deleteFormSettingCondition = (formSetting: FormTriggerSetting) => {
|
||||
formSetting.conditionType = undefined
|
||||
}
|
||||
/** 打开条件配置弹窗 */
|
||||
const openFormSettingCondition = (index: number, formSetting: FormTriggerSetting) => {
|
||||
const conditionDialog = proxy.$refs[`condition-${index}`][0]
|
||||
conditionDialog.open(formSetting)
|
||||
}
|
||||
/** 处理条件配置保存 */
|
||||
const handleConditionUpdate = (index: number, condition: any) => {
|
||||
configForm.value.formSettings![index].conditionType = condition.conditionType
|
||||
configForm.value.formSettings![index].conditionExpression = condition.conditionExpression
|
||||
configForm.value.formSettings![index].conditionGroups = condition.conditionGroups
|
||||
}
|
||||
/** 条件配置展示 */
|
||||
const showConditionText = (formSetting: FormTriggerSetting) => {
|
||||
return getConditionShowText(
|
||||
formSetting.conditionType,
|
||||
formSetting.conditionExpression,
|
||||
formSetting.conditionGroups,
|
||||
formFields
|
||||
)
|
||||
}
|
||||
|
||||
/** 添加修改字段设置项 */
|
||||
const addFormFieldSetting = (formSetting: FormTriggerSetting) => {
|
||||
if (!formSetting) return
|
||||
if (!formSetting.updateFormFields) {
|
||||
formSetting.updateFormFields = {}
|
||||
}
|
||||
formSetting.updateFormFields[''] = undefined
|
||||
}
|
||||
/** 更新字段 KEY */
|
||||
const updateFormFieldKey = (formSetting: FormTriggerSetting, oldKey: string, newKey: string) => {
|
||||
if (!formSetting?.updateFormFields) return
|
||||
const value = formSetting.updateFormFields[oldKey]
|
||||
delete formSetting.updateFormFields[oldKey]
|
||||
formSetting.updateFormFields[newKey] = value
|
||||
}
|
||||
|
||||
/** 删除修改字段设置项 */
|
||||
const deleteFormFieldSetting = (formSetting: FormTriggerSetting, key: string) => {
|
||||
if (!formSetting?.updateFormFields) return
|
||||
delete formSetting.updateFormFields[key]
|
||||
}
|
||||
|
||||
/** 保存配置 */
|
||||
|
|
@ -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: []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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-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-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-form-item>
|
||||
<el-select
|
||||
filterable
|
||||
v-model="timeUnit"
|
||||
class="mr-2"
|
||||
:style="{ width: '100px' }"
|
||||
@change="timeUnitChange"
|
||||
<el-form-item
|
||||
label="执行动作"
|
||||
prop="timeoutHandlerType"
|
||||
v-if="configForm.timeoutHandlerEnable"
|
||||
>
|
||||
<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-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' }"
|
||||
@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-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>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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,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>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 || ' '
|
||||
}
|
||||
|
||||
provide('configGlobal', props)
|
||||
let bpmnModeler: any = null
|
||||
const defaultZoom = ref(1)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -254,33 +254,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,
|
||||
|
|
@ -356,7 +332,6 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
|||
}
|
||||
},
|
||||
{
|
||||
// TODO @zws:1)建议,在加一个路由。然后标题是“复制流程”,这样体验会好点;2)复制出来的数据,在名字前面,加“副本 ”,和钉钉保持一致!
|
||||
path: 'manager/model/:type/:id',
|
||||
component: () => import('@/views/bpm/model/form/index.vue'),
|
||||
name: 'BpmModelUpdate',
|
||||
|
|
@ -633,6 +608,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'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -655,15 +689,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',
|
||||
|
|
@ -672,9 +706,20 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
|||
title: '设备详情',
|
||||
noCache: true,
|
||||
hidden: true,
|
||||
activeMenu: '/iot/device'
|
||||
activeMenu: '/iot/device/device'
|
||||
},
|
||||
component: () => import('@/views/iot/device/detail/index.vue')
|
||||
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/plugin/detail/index.vue')
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,6 +230,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 生成模式
|
||||
|
|
@ -246,9 +247,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' // 桥梁类型
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -155,6 +155,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(',')
|
||||
}
|
||||
|
||||
/**
|
||||
* 首字母大写
|
||||
*/
|
||||
|
|
@ -484,7 +556,22 @@ export function jsonParse(str: string) {
|
|||
try {
|
||||
return JSON.parse(str)
|
||||
} catch (e) {
|
||||
console.error(`str[${str}] 不是一个 JSON 字符串`)
|
||||
console.log(`str[${str}] 不是一个 JSON 字符串`)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 截取字符串
|
||||
*
|
||||
* @param str 字符串
|
||||
* @param start 开始位置
|
||||
* @param end 结束位置
|
||||
*/
|
||||
|
||||
export const subString = (str: string, start: number, end: number) => {
|
||||
if (str.length > end) {
|
||||
return str.slice(start, end)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 方法,用于打开弹窗
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<template>
|
||||
<doc-alert title="AI 对话聊天" url="https://doc.iocoder.cn/ai/chat/" />
|
||||
|
||||
<ContentWrap>
|
||||
<el-tabs>
|
||||
<el-tab-pane label="对话列表">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// 根据选择的平台筛选模型
|
||||
platformModels.value = props.models.filter((item: ModelVO) => item.platform === platform)
|
||||
|
||||
// 切换平台,默认选择一个模型
|
||||
if (platformModels.value.length > 0) {
|
||||
modelId.value = platformModels.value[0].id // 使用 model 属性作为值
|
||||
} else {
|
||||
models.value = []
|
||||
}
|
||||
// 切换平台,默认选择一个风格
|
||||
if (models.value.length > 0) {
|
||||
model.value = models.value[0].key
|
||||
} 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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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, // 图片高度
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<template>
|
||||
<doc-alert title="AI 绘图创作" url="https://doc.iocoder.cn/ai/image/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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="例如:童话里的小屋应该是什么样子?"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<template>
|
||||
<doc-alert title="AI 思维导图" url="https://doc.iocoder.cn/ai/mindmap/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<template>
|
||||
<doc-alert title="AI 手册" url="https://doc.iocoder.cn/ai/build/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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" />
|
||||
<el-table-column label="模型标识" align="center" prop="model" />
|
||||
<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()
|
||||
})
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
<template>
|
||||
<doc-alert title="AI 音乐创作" url="https://doc.iocoder.cn/ai/music/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -50,11 +50,13 @@ import FcDesigner from '@form-create/designer'
|
|||
import { encodeConf, encodeFields, setConfAndFields } from '@/utils/formCreate'
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
import { useFormCreateDesigner } from '@/components/FormCreate'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
defineOptions({ name: 'BpmFormEditor' })
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息
|
||||
const route = useRoute() // 路由
|
||||
const { push, currentRoute } = useRouter() // 路由
|
||||
const { query } = useRoute() // 路由信息
|
||||
const { delView } = useTagsViewStore() // 视图操作
|
||||
|
|
@ -150,6 +152,14 @@ onMounted(async () => {
|
|||
const data = await FormApi.getForm(id)
|
||||
formData.value = data
|
||||
setConfAndFields(designer, data.conf, data.fields)
|
||||
|
||||
if (route.query.type !== 'copy') {
|
||||
return
|
||||
}
|
||||
// 场景三: 复制表单
|
||||
const { id: foo, ...copied } = data
|
||||
formData.value = copied
|
||||
formData.value.name += '_copy'
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||