mirror of
https://gitee.com/myxzgzs/boyue-ui-admin-vue3
synced 2025-08-08 16:32:43 +08:00
Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm
This commit is contained in:
commit
7d352397e8
Binary file not shown.
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 25 KiB |
25
README.md
25
README.md
@ -81,16 +81,13 @@
|
|||||||
|
|
||||||
系统内置多种多种业务功能,可以用于快速你的业务系统:
|
系统内置多种多种业务功能,可以用于快速你的业务系统:
|
||||||
|
|
||||||
* 系统功能
|
系统内置多种多种业务功能,可以用于快速你的业务系统:
|
||||||
* 基础设施
|
|
||||||
* 工作流程
|

|
||||||
* 支付系统
|
|
||||||
* 会员中心
|
* 通用模块(必选):系统功能、基础设施
|
||||||
* 数据报表
|
* 通用模块(可选):工作流程、支付系统、数据报表、会员中心
|
||||||
* 商城系统
|
* 业务系统(按需):ERP 系统、CRM 系统、商城系统、微信公众号、AI 大模型
|
||||||
* 微信公众号
|
|
||||||
* ERP 系统
|
|
||||||
* CRM 系统
|
|
||||||
|
|
||||||
### 系统功能
|
### 系统功能
|
||||||
|
|
||||||
@ -213,6 +210,14 @@ ps:核心功能已经实现,正在对接微信小程序中...
|
|||||||
|
|
||||||

|

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

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## 🐷 演示图
|
## 🐷 演示图
|
||||||
|
|
||||||
### 系统功能
|
### 系统功能
|
||||||
|
@ -14,9 +14,16 @@ export interface ChatMessageVO {
|
|||||||
modelId: number // 模型编号
|
modelId: number // 模型编号
|
||||||
content: string // 聊天内容
|
content: string // 聊天内容
|
||||||
tokens: number // 消耗 Token 数量
|
tokens: number // 消耗 Token 数量
|
||||||
|
segmentIds?: number[] // 段落编号
|
||||||
|
segments?: {
|
||||||
|
id: number // 段落编号
|
||||||
|
content: string // 段落内容
|
||||||
|
documentId: number // 文档编号
|
||||||
|
documentName: string // 文档名称
|
||||||
|
}[]
|
||||||
createTime: Date // 创建时间
|
createTime: Date // 创建时间
|
||||||
roleAvatar: string // 角色头像
|
roleAvatar: string // 角色头像
|
||||||
userAvatar: string // 创建时间
|
userAvatar: string // 用户头像
|
||||||
}
|
}
|
||||||
|
|
||||||
// AI chat 聊天
|
// AI chat 聊天
|
||||||
|
@ -20,9 +20,8 @@ export interface ImageVO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ImageDrawReqVO {
|
export interface ImageDrawReqVO {
|
||||||
platform: string // 平台
|
|
||||||
prompt: string // 提示词
|
prompt: string // 提示词
|
||||||
model: string // 模型
|
modelId: number // 模型
|
||||||
style: string // 图像生成的风格
|
style: string // 图像生成的风格
|
||||||
width: string // 图片宽度
|
width: string // 图片宽度
|
||||||
height: string // 图片高度
|
height: string // 图片高度
|
||||||
@ -31,7 +30,7 @@ export interface ImageDrawReqVO {
|
|||||||
|
|
||||||
export interface ImageMidjourneyImagineReqVO {
|
export interface ImageMidjourneyImagineReqVO {
|
||||||
prompt: string // 提示词
|
prompt: string // 提示词
|
||||||
model: string // 模型 mj nijj
|
modelId: number // 模型
|
||||||
base64Array: string[] // size不能为空
|
base64Array: string[] // size不能为空
|
||||||
width: string // 图片宽度
|
width: string // 图片宽度
|
||||||
height: string // 图片高度
|
height: string // 图片高度
|
||||||
|
54
src/api/ai/knowledge/document/index.ts
Normal file
54
src/api/ai/knowledge/document/index.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import request from '@/config/axios'
|
||||||
|
|
||||||
|
// AI 知识库文档 VO
|
||||||
|
export interface KnowledgeDocumentVO {
|
||||||
|
id: number // 编号
|
||||||
|
knowledgeId: number // 知识库编号
|
||||||
|
name: string // 文档名称
|
||||||
|
contentLength: number // 字符数
|
||||||
|
tokens: number // token 数
|
||||||
|
segmentMaxTokens: number // 分片最大 token 数
|
||||||
|
retrievalCount: number // 召回次数
|
||||||
|
status: number // 是否启用
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI 知识库文档 API
|
||||||
|
export const KnowledgeDocumentApi = {
|
||||||
|
// 查询知识库文档分页
|
||||||
|
getKnowledgeDocumentPage: async (params: any) => {
|
||||||
|
return await request.get({ url: `/ai/knowledge/document/page`, params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 查询知识库文档详情
|
||||||
|
getKnowledgeDocument: async (id: number) => {
|
||||||
|
return await request.get({ url: `/ai/knowledge/document/get?id=` + id })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增知识库文档(单个)
|
||||||
|
createKnowledgeDocument: async (data: any) => {
|
||||||
|
return await request.post({ url: `/ai/knowledge/document/create`, data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增知识库文档(多个)
|
||||||
|
createKnowledgeDocumentList: async (data: any) => {
|
||||||
|
return await request.post({ url: `/ai/knowledge/document/create-list`, data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 修改知识库文档
|
||||||
|
updateKnowledgeDocument: async (data: any) => {
|
||||||
|
return await request.put({ url: `/ai/knowledge/document/update`, data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 修改知识库文档状态
|
||||||
|
updateKnowledgeDocumentStatus: async (data: any) => {
|
||||||
|
return await request.put({
|
||||||
|
url: `/ai/knowledge/document/update-status`,
|
||||||
|
data
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除知识库文档
|
||||||
|
deleteKnowledgeDocument: async (id: number) => {
|
||||||
|
return await request.delete({ url: `/ai/knowledge/document/delete?id=` + id })
|
||||||
|
}
|
||||||
|
}
|
44
src/api/ai/knowledge/knowledge/index.ts
Normal file
44
src/api/ai/knowledge/knowledge/index.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import request from '@/config/axios'
|
||||||
|
|
||||||
|
// AI 知识库 VO
|
||||||
|
export interface KnowledgeVO {
|
||||||
|
id: number // 编号
|
||||||
|
name: string // 知识库名称
|
||||||
|
description: string // 知识库描述
|
||||||
|
embeddingModelId: number // 嵌入模型编号,高质量模式时维护
|
||||||
|
topK: number // topK
|
||||||
|
similarityThreshold: number // 相似度阈值
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI 知识库 API
|
||||||
|
export const KnowledgeApi = {
|
||||||
|
// 查询知识库分页
|
||||||
|
getKnowledgePage: async (params: any) => {
|
||||||
|
return await request.get({ url: `/ai/knowledge/page`, params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 查询知识库详情
|
||||||
|
getKnowledge: async (id: number) => {
|
||||||
|
return await request.get({ url: `/ai/knowledge/get?id=` + id })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增知识库
|
||||||
|
createKnowledge: async (data: KnowledgeVO) => {
|
||||||
|
return await request.post({ url: `/ai/knowledge/create`, data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 修改知识库
|
||||||
|
updateKnowledge: async (data: KnowledgeVO) => {
|
||||||
|
return await request.put({ url: `/ai/knowledge/update`, data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除知识库
|
||||||
|
deleteKnowledge: async (id: number) => {
|
||||||
|
return await request.delete({ url: `/ai/knowledge/delete?id=` + id })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取知识库简单列表
|
||||||
|
getSimpleKnowledgeList: async () => {
|
||||||
|
return await request.get({ url: `/ai/knowledge/simple-list` })
|
||||||
|
}
|
||||||
|
}
|
75
src/api/ai/knowledge/segment/index.ts
Normal file
75
src/api/ai/knowledge/segment/index.ts
Normal file
@ -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 // 角色设定
|
welcomeMessage: string // 角色设定
|
||||||
publicStatus: boolean // 是否公开
|
publicStatus: boolean // 是否公开
|
||||||
status: number // 状态
|
status: number // 状态
|
||||||
|
knowledgeIds?: number[] // 引用的知识库 ID 列表
|
||||||
|
toolIds?: number[] // 引用的工具 ID 列表
|
||||||
}
|
}
|
||||||
|
|
||||||
// AI 聊天角色 分页请求 vo
|
// AI 聊天角色 分页请求 vo
|
||||||
|
54
src/api/ai/model/model/index.ts
Normal file
54
src/api/ai/model/model/index.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import request from '@/config/axios'
|
||||||
|
|
||||||
|
// AI 模型 VO
|
||||||
|
export interface ModelVO {
|
||||||
|
id: number // 编号
|
||||||
|
keyId: number // API 秘钥编号
|
||||||
|
name: string // 模型名字
|
||||||
|
model: string // 模型标识
|
||||||
|
platform: string // 模型平台
|
||||||
|
type: number // 模型类型
|
||||||
|
sort: number // 排序
|
||||||
|
status: number // 状态
|
||||||
|
temperature?: number // 温度参数
|
||||||
|
maxTokens?: number // 单条回复的最大 Token 数量
|
||||||
|
maxContexts?: number // 上下文的最大 Message 数量
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI 模型 API
|
||||||
|
export const ModelApi = {
|
||||||
|
// 查询模型分页
|
||||||
|
getModelPage: async (params: any) => {
|
||||||
|
return await request.get({ url: `/ai/model/page`, params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获得模型列表
|
||||||
|
getModelSimpleList: async (type?: number) => {
|
||||||
|
return await request.get({
|
||||||
|
url: `/ai/model/simple-list`,
|
||||||
|
params: {
|
||||||
|
type
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 查询模型详情
|
||||||
|
getModel: async (id: number) => {
|
||||||
|
return await request.get({ url: `/ai/model/get?id=` + id })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增模型
|
||||||
|
createModel: async (data: ModelVO) => {
|
||||||
|
return await request.post({ url: `/ai/model/create`, data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 修改模型
|
||||||
|
updateModel: async (data: ModelVO) => {
|
||||||
|
return await request.put({ url: `/ai/model/update`, data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除模型
|
||||||
|
deleteModel: async (id: number) => {
|
||||||
|
return await request.delete({ url: `/ai/model/delete?id=` + id })
|
||||||
|
}
|
||||||
|
}
|
42
src/api/ai/model/tool/index.ts
Normal file
42
src/api/ai/model/tool/index.ts
Normal file
@ -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` })
|
||||||
|
}
|
||||||
|
}
|
@ -608,6 +608,65 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
|||||||
icon: 'ep:home-filled',
|
icon: 'ep:home-filled',
|
||||||
noCache: false
|
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'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -219,6 +219,7 @@ export enum DICT_TYPE {
|
|||||||
|
|
||||||
// ========== AI - 人工智能模块 ==========
|
// ========== AI - 人工智能模块 ==========
|
||||||
AI_PLATFORM = 'ai_platform', // AI 平台
|
AI_PLATFORM = 'ai_platform', // AI 平台
|
||||||
|
AI_MODEL_TYPE = 'ai_model_type', // AI 模型类型
|
||||||
AI_IMAGE_STATUS = 'ai_image_status', // AI 图片状态
|
AI_IMAGE_STATUS = 'ai_image_status', // AI 图片状态
|
||||||
AI_MUSIC_STATUS = 'ai_music_status', // AI 音乐状态
|
AI_MUSIC_STATUS = 'ai_music_status', // AI 音乐状态
|
||||||
AI_GENERATE_MODE = 'ai_generate_mode', // AI 生成模式
|
AI_GENERATE_MODE = 'ai_generate_mode', // AI 生成模式
|
||||||
|
@ -116,6 +116,64 @@ export function toAnyString() {
|
|||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据支持的文件类型生成 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(',')
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 首字母大写
|
* 首字母大写
|
||||||
*/
|
*/
|
||||||
|
@ -11,17 +11,17 @@
|
|||||||
<el-input
|
<el-input
|
||||||
type="textarea"
|
type="textarea"
|
||||||
v-model="formData.systemMessage"
|
v-model="formData.systemMessage"
|
||||||
rows="4"
|
:rows="4"
|
||||||
placeholder="请输入角色设定"
|
placeholder="请输入角色设定"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="模型" prop="modelId">
|
<el-form-item label="模型" prop="modelId">
|
||||||
<el-select v-model="formData.modelId" placeholder="请选择模型">
|
<el-select v-model="formData.modelId" placeholder="请选择模型">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="chatModel in chatModelList"
|
v-for="model in models"
|
||||||
:key="chatModel.id"
|
:key="model.id"
|
||||||
:label="chatModel.name"
|
:label="model.name"
|
||||||
:value="chatModel.id"
|
:value="model.id"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -32,6 +32,7 @@
|
|||||||
:min="0"
|
:min="0"
|
||||||
:max="2"
|
:max="2"
|
||||||
:precision="2"
|
:precision="2"
|
||||||
|
class="!w-1/1"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="回复数 Token 数" prop="maxTokens">
|
<el-form-item label="回复数 Token 数" prop="maxTokens">
|
||||||
@ -39,7 +40,8 @@
|
|||||||
v-model="formData.maxTokens"
|
v-model="formData.maxTokens"
|
||||||
placeholder="请输入回复数 Token 数"
|
placeholder="请输入回复数 Token 数"
|
||||||
:min="0"
|
:min="0"
|
||||||
:max="4096"
|
:max="8192"
|
||||||
|
class="!w-1/1"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="上下文数量" prop="maxContexts">
|
<el-form-item label="上下文数量" prop="maxContexts">
|
||||||
@ -48,6 +50,7 @@
|
|||||||
placeholder="请输入上下文数量"
|
placeholder="请输入上下文数量"
|
||||||
:min="0"
|
:min="0"
|
||||||
:max="20"
|
:max="20"
|
||||||
|
class="!w-1/1"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@ -58,9 +61,9 @@
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CommonStatusEnum } from '@/utils/constants'
|
import { ModelApi, ModelVO } from '@/api/ai/model/model'
|
||||||
import { ChatModelApi, ChatModelVO } from '@/api/ai/model/chatModel'
|
|
||||||
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
|
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
|
||||||
|
import { AiModelTypeEnum } from '@/views/ai/utils/constants'
|
||||||
|
|
||||||
/** AI 聊天对话的更新表单 */
|
/** AI 聊天对话的更新表单 */
|
||||||
defineOptions({ name: 'ChatConversationUpdateForm' })
|
defineOptions({ name: 'ChatConversationUpdateForm' })
|
||||||
@ -85,7 +88,7 @@ const formRules = reactive({
|
|||||||
maxContexts: [{ required: true, message: '上下文数量不能为空', trigger: 'blur' }]
|
maxContexts: [{ required: true, message: '上下文数量不能为空', trigger: 'blur' }]
|
||||||
})
|
})
|
||||||
const formRef = ref() // 表单 Ref
|
const formRef = ref() // 表单 Ref
|
||||||
const chatModelList = ref([] as ChatModelVO[]) // 聊天模型列表
|
const models = ref([] as ModelVO[]) // 聊天模型列表
|
||||||
|
|
||||||
/** 打开弹窗 */
|
/** 打开弹窗 */
|
||||||
const open = async (id: number) => {
|
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 方法,用于打开弹窗
|
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||||
|
|
||||||
|
104
src/views/ai/chat/index/components/message/MessageKnowledge.vue
Normal file
104
src/views/ai/chat/index/components/message/MessageKnowledge.vue
Normal file
@ -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>
|
||||||
<div class="left-text-container" ref="markdownViewRef">
|
<div class="left-text-container" ref="markdownViewRef">
|
||||||
<MarkdownView class="left-text" :content="item.content" />
|
<MarkdownView class="left-text" :content="item.content" />
|
||||||
|
<MessageKnowledge v-if="item.segments" :segments="item.segments" />
|
||||||
</div>
|
</div>
|
||||||
<div class="left-btns">
|
<div class="left-btns">
|
||||||
<el-button class="btn-cus" link @click="copyContent(item.content)">
|
<el-button class="btn-cus" link @click="copyContent(item.content)">
|
||||||
@ -62,6 +63,7 @@
|
|||||||
import { PropType } from 'vue'
|
import { PropType } from 'vue'
|
||||||
import { formatDate } from '@/utils/formatTime'
|
import { formatDate } from '@/utils/formatTime'
|
||||||
import MarkdownView from '@/components/MarkdownView/index.vue'
|
import MarkdownView from '@/components/MarkdownView/index.vue'
|
||||||
|
import MessageKnowledge from './MessageKnowledge.vue'
|
||||||
import { useClipboard } from '@vueuse/core'
|
import { useClipboard } from '@vueuse/core'
|
||||||
import { ArrowDownBold, Edit, RefreshRight } from '@element-plus/icons-vue'
|
import { ArrowDownBold, Edit, RefreshRight } from '@element-plus/icons-vue'
|
||||||
import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message'
|
import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message'
|
||||||
|
@ -41,9 +41,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ChatRoleVO} from '@/api/ai/model/chatRole'
|
import { ChatRoleVO } from '@/api/ai/model/chatRole'
|
||||||
import {PropType, ref} from 'vue'
|
import { PropType, ref } from 'vue'
|
||||||
import {More} from '@element-plus/icons-vue'
|
import { More } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const tabsRef = ref<any>() // tabs ref
|
const tabsRef = ref<any>() // tabs ref
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<doc-alert title="AI 对话聊天" url="https://doc.iocoder.cn/ai/chat/" />
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<el-tabs>
|
<el-tabs>
|
||||||
<el-tab-pane label="对话列表">
|
<el-tab-pane label="对话列表">
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt">
|
<div class="prompt">
|
||||||
<el-text tag="b">画面描述</el-text>
|
<el-text tag="b">画面描述</el-text>
|
||||||
<el-text tag="p">建议使用“形容词+动词+风格”的格式,使用“,”隔开</el-text>
|
<el-text tag="p">建议使用“形容词 + 动词 + 风格”的格式,使用“,”隔开</el-text>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="prompt"
|
v-model="prompt"
|
||||||
maxlength="1024"
|
maxlength="1024"
|
||||||
rows="5"
|
:rows="5"
|
||||||
class="w-100% mt-15px"
|
class="w-100% mt-15px"
|
||||||
input-style="border-radius: 7px;"
|
input-style="border-radius: 7px;"
|
||||||
placeholder="例如:童话里的小屋应该是什么样子?"
|
placeholder="例如:童话里的小屋应该是什么样子?"
|
||||||
@ -57,8 +57,13 @@
|
|||||||
<el-text tag="b">模型</el-text>
|
<el-text tag="b">模型</el-text>
|
||||||
</div>
|
</div>
|
||||||
<el-space wrap class="group-item-body">
|
<el-space wrap class="group-item-body">
|
||||||
<el-select v-model="model" placeholder="Select" size="large" class="!w-350px">
|
<el-select v-model="modelId" placeholder="Select" size="large" class="!w-350px">
|
||||||
<el-option v-for="item in models" :key="item.key" :label="item.name" :value="item.key" />
|
<el-option
|
||||||
|
v-for="item in platformModels"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-space>
|
</el-space>
|
||||||
</div>
|
</div>
|
||||||
@ -72,25 +77,34 @@
|
|||||||
</el-space>
|
</el-space>
|
||||||
</div>
|
</div>
|
||||||
<div class="btns">
|
<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 ? '生成中' : '生成内容' }}
|
{{ drawIn ? '生成中' : '生成内容' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ImageApi, ImageDrawReqVO, ImageVO } from '@/api/ai/image'
|
import { ImageApi, ImageDrawReqVO, ImageVO } from '@/api/ai/image'
|
||||||
import {
|
import { AiPlatformEnum, ImageHotWords, OtherPlatformEnum } from '@/views/ai/utils/constants'
|
||||||
AiPlatformEnum,
|
import { ModelVO } from '@/api/ai/model/model'
|
||||||
ChatGlmModels,
|
|
||||||
ImageHotWords,
|
|
||||||
ImageModelVO,
|
|
||||||
OtherPlatformEnum,
|
|
||||||
QianFanModels,
|
|
||||||
TongYiWanXiangModels
|
|
||||||
} from '@/views/ai/utils/constants'
|
|
||||||
|
|
||||||
const message = useMessage() // 消息弹窗
|
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 drawIn = ref<boolean>(false) // 生成中
|
||||||
const selectHotWord = ref<string>('') // 选中的热词
|
const selectHotWord = ref<string>('') // 选中的热词
|
||||||
@ -99,10 +113,8 @@ const prompt = ref<string>('') // 提示词
|
|||||||
const width = ref<number>(512) // 图片宽度
|
const width = ref<number>(512) // 图片宽度
|
||||||
const height = ref<number>(512) // 图片高度
|
const height = ref<number>(512) // 图片高度
|
||||||
const otherPlatform = ref<string>(AiPlatformEnum.TONG_YI) // 平台
|
const otherPlatform = ref<string>(AiPlatformEnum.TONG_YI) // 平台
|
||||||
const models = ref<ImageModelVO[]>(TongYiWanXiangModels) // 模型 TongYiWanXiangModels、QianFanModels
|
const platformModels = ref<ModelVO[]>([]) // 模型列表
|
||||||
const model = ref<string>(models.value[0].key) // 模型
|
const modelId = ref<number>() // 选中的模型
|
||||||
|
|
||||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
|
|
||||||
|
|
||||||
/** 选择热词 */
|
/** 选择热词 */
|
||||||
const handleHotWordClick = async (hotWord: string) => {
|
const handleHotWordClick = async (hotWord: string) => {
|
||||||
@ -125,11 +137,11 @@ const handleGenerateImage = async () => {
|
|||||||
// 加载中
|
// 加载中
|
||||||
drawIn.value = true
|
drawIn.value = true
|
||||||
// 回调
|
// 回调
|
||||||
emits('onDrawStart', AiPlatformEnum.STABLE_DIFFUSION)
|
emits('onDrawStart', otherPlatform.value)
|
||||||
// 发送请求
|
// 发送请求
|
||||||
const form = {
|
const form = {
|
||||||
platform: otherPlatform.value,
|
platform: otherPlatform.value,
|
||||||
model: model.value, // 模型
|
modelId: modelId.value, // 模型
|
||||||
prompt: prompt.value, // 提示词
|
prompt: prompt.value, // 提示词
|
||||||
width: width.value, // 图片宽度
|
width: width.value, // 图片宽度
|
||||||
height: height.value, // 图片高度
|
height: height.value, // 图片高度
|
||||||
@ -138,7 +150,7 @@ const handleGenerateImage = async () => {
|
|||||||
await ImageApi.drawImage(form)
|
await ImageApi.drawImage(form)
|
||||||
} finally {
|
} finally {
|
||||||
// 回调
|
// 回调
|
||||||
emits('onDrawComplete', AiPlatformEnum.STABLE_DIFFUSION)
|
emits('onDrawComplete', otherPlatform.value)
|
||||||
// 加载结束
|
// 加载结束
|
||||||
drawIn.value = false
|
drawIn.value = false
|
||||||
}
|
}
|
||||||
@ -153,33 +165,29 @@ const settingValues = async (detail: ImageVO) => {
|
|||||||
|
|
||||||
/** 平台切换 */
|
/** 平台切换 */
|
||||||
const handlerPlatformChange = async (platform: string) => {
|
const handlerPlatformChange = async (platform: string) => {
|
||||||
// 切换平台,切换模型、风格
|
// 根据选择的平台筛选模型
|
||||||
if (AiPlatformEnum.TONG_YI === platform) {
|
platformModels.value = props.models.filter((item: ModelVO) => item.platform === platform)
|
||||||
models.value = TongYiWanXiangModels
|
|
||||||
} else if (AiPlatformEnum.YI_YAN === platform) {
|
// 切换平台,默认选择一个模型
|
||||||
models.value = QianFanModels
|
if (platformModels.value.length > 0) {
|
||||||
} else if (AiPlatformEnum.ZHI_PU === platform) {
|
modelId.value = platformModels.value[0].id // 使用 model 属性作为值
|
||||||
models.value = ChatGlmModels
|
|
||||||
} else {
|
} else {
|
||||||
models.value = []
|
modelId.value = undefined
|
||||||
}
|
|
||||||
// 切换平台,默认选择一个风格
|
|
||||||
if (models.value.length > 0) {
|
|
||||||
model.value = models.value[0].key
|
|
||||||
} else {
|
|
||||||
model.value = ''
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 监听 models 变化 */
|
||||||
|
watch(
|
||||||
|
() => props.models,
|
||||||
|
() => {
|
||||||
|
handlerPlatformChange(otherPlatform.value)
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
)
|
||||||
/** 暴露组件方法 */
|
/** 暴露组件方法 */
|
||||||
defineExpose({ settingValues })
|
defineExpose({ settingValues })
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
// 提示词
|
|
||||||
.prompt {
|
|
||||||
}
|
|
||||||
|
|
||||||
// 热词
|
|
||||||
.hot-words {
|
.hot-words {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
@ -2,11 +2,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt">
|
<div class="prompt">
|
||||||
<el-text tag="b">画面描述</el-text>
|
<el-text tag="b">画面描述</el-text>
|
||||||
<el-text tag="p">建议使用“形容词+动词+风格”的格式,使用“,”隔开</el-text>
|
<el-text tag="p">建议使用"形容词 + 动词 + 风格"的格式,使用","隔开</el-text>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="prompt"
|
v-model="prompt"
|
||||||
maxlength="1024"
|
maxlength="1024"
|
||||||
rows="5"
|
:rows="5"
|
||||||
class="w-100% mt-15px"
|
class="w-100% mt-15px"
|
||||||
input-style="border-radius: 7px;"
|
input-style="border-radius: 7px;"
|
||||||
placeholder="例如:童话里的小屋应该是什么样子?"
|
placeholder="例如:童话里的小屋应该是什么样子?"
|
||||||
@ -82,7 +82,14 @@
|
|||||||
</el-space>
|
</el-space>
|
||||||
</div>
|
</div>
|
||||||
<div class="btns">
|
<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 ? '生成中' : '生成内容' }}
|
{{ drawIn ? '生成中' : '生成内容' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@ -95,11 +102,22 @@ import {
|
|||||||
ImageHotWords,
|
ImageHotWords,
|
||||||
Dall3SizeList,
|
Dall3SizeList,
|
||||||
ImageModelVO,
|
ImageModelVO,
|
||||||
AiPlatformEnum
|
AiPlatformEnum,
|
||||||
|
ImageSizeVO
|
||||||
} from '@/views/ai/utils/constants'
|
} from '@/views/ai/utils/constants'
|
||||||
|
import { ModelVO } from '@/api/ai/model/model'
|
||||||
|
|
||||||
const message = useMessage() // 消息弹窗
|
const message = useMessage() // 消息弹窗
|
||||||
|
|
||||||
|
// 接收父组件传入的模型列表
|
||||||
|
const props = defineProps({
|
||||||
|
models: {
|
||||||
|
type: Array<ModelVO>,
|
||||||
|
default: () => [] as ModelVO[]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
|
||||||
|
|
||||||
// 定义属性
|
// 定义属性
|
||||||
const prompt = ref<string>('') // 提示词
|
const prompt = ref<string>('') // 提示词
|
||||||
const drawIn = ref<boolean>(false) // 生成中
|
const drawIn = ref<boolean>(false) // 生成中
|
||||||
@ -108,8 +126,6 @@ const selectModel = ref<string>('dall-e-3') // 模型
|
|||||||
const selectSize = ref<string>('1024x1024') // 选中 size
|
const selectSize = ref<string>('1024x1024') // 选中 size
|
||||||
const style = ref<string>('vivid') // style 样式
|
const style = ref<string>('vivid') // style 样式
|
||||||
|
|
||||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
|
|
||||||
|
|
||||||
/** 选择热词 */
|
/** 选择热词 */
|
||||||
const handleHotWordClick = async (hotWord: string) => {
|
const handleHotWordClick = async (hotWord: string) => {
|
||||||
// 情况一:取消选中
|
// 情况一:取消选中
|
||||||
@ -126,6 +142,27 @@ const handleHotWordClick = async (hotWord: string) => {
|
|||||||
/** 选择 model 模型 */
|
/** 选择 model 模型 */
|
||||||
const handleModelClick = async (model: ImageModelVO) => {
|
const handleModelClick = async (model: ImageModelVO) => {
|
||||||
selectModel.value = model.key
|
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 样式 */
|
/** 选择 style 样式 */
|
||||||
@ -140,6 +177,15 @@ const handleSizeClick = async (imageSize: ImageSizeVO) => {
|
|||||||
|
|
||||||
/** 图片生产 */
|
/** 图片生产 */
|
||||||
const handleGenerateImage = async () => {
|
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(`确认生成内容?`)
|
await message.confirm(`确认生成内容?`)
|
||||||
try {
|
try {
|
||||||
@ -151,7 +197,8 @@ const handleGenerateImage = async () => {
|
|||||||
const form = {
|
const form = {
|
||||||
platform: AiPlatformEnum.OPENAI,
|
platform: AiPlatformEnum.OPENAI,
|
||||||
prompt: prompt.value, // 提示词
|
prompt: prompt.value, // 提示词
|
||||||
model: selectModel.value, // 模型
|
modelId: matchedModel.id, // 使用匹配到的模型
|
||||||
|
style: style.value, // 图像生成的风格
|
||||||
width: imageSize.width, // size 不能为空
|
width: imageSize.width, // size 不能为空
|
||||||
height: imageSize.height, // size 不能为空
|
height: imageSize.height, // size 不能为空
|
||||||
options: {
|
options: {
|
||||||
@ -183,10 +230,6 @@ const settingValues = async (detail: ImageVO) => {
|
|||||||
defineExpose({ settingValues })
|
defineExpose({ settingValues })
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
// 提示词
|
|
||||||
.prompt {
|
|
||||||
}
|
|
||||||
|
|
||||||
// 热词
|
// 热词
|
||||||
.hot-words {
|
.hot-words {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<el-input
|
<el-input
|
||||||
v-model="prompt"
|
v-model="prompt"
|
||||||
maxlength="1024"
|
maxlength="1024"
|
||||||
rows="5"
|
:rows="5"
|
||||||
class="w-100% mt-15px"
|
class="w-100% mt-15px"
|
||||||
input-style="border-radius: 7px;"
|
input-style="border-radius: 7px;"
|
||||||
placeholder="例如:童话里的小屋应该是什么样子?"
|
placeholder="例如:童话里的小屋应该是什么样子?"
|
||||||
@ -95,7 +95,13 @@
|
|||||||
</el-space>
|
</el-space>
|
||||||
</div>
|
</div>
|
||||||
<div class="btns">
|
<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 ? '生成中' : '生成内容' }}
|
{{ drawIn ? '生成中' : '生成内容' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@ -112,9 +118,19 @@ import {
|
|||||||
MidjourneyVersions,
|
MidjourneyVersions,
|
||||||
NijiVersionList
|
NijiVersionList
|
||||||
} from '@/views/ai/utils/constants'
|
} from '@/views/ai/utils/constants'
|
||||||
|
import { ModelVO } from '@/api/ai/model/model'
|
||||||
|
|
||||||
const message = useMessage() // 消息弹窗
|
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 drawIn = ref<boolean>(false) // 生成中
|
||||||
const selectHotWord = ref<string>('') // 选中的热词
|
const selectHotWord = ref<string>('') // 选中的热词
|
||||||
@ -125,7 +141,6 @@ const selectModel = ref<string>('midjourney') // 选中的模型
|
|||||||
const selectSize = ref<string>('1:1') // 选中 size
|
const selectSize = ref<string>('1:1') // 选中 size
|
||||||
const selectVersion = ref<any>('6.0') // 选中的 version
|
const selectVersion = ref<any>('6.0') // 选中的 version
|
||||||
const versionList = ref<any>(MidjourneyVersions) // version 列表
|
const versionList = ref<any>(MidjourneyVersions) // version 列表
|
||||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
|
|
||||||
|
|
||||||
/** 选择热词 */
|
/** 选择热词 */
|
||||||
const handleHotWordClick = async (hotWord: string) => {
|
const handleHotWordClick = async (hotWord: string) => {
|
||||||
@ -158,6 +173,15 @@ const handleModelClick = async (model: ImageModelVO) => {
|
|||||||
|
|
||||||
/** 图片生成 */
|
/** 图片生成 */
|
||||||
const handleGenerateImage = async () => {
|
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(`确认生成内容?`)
|
await message.confirm(`确认生成内容?`)
|
||||||
try {
|
try {
|
||||||
@ -171,7 +195,7 @@ const handleGenerateImage = async () => {
|
|||||||
) as ImageSizeVO
|
) as ImageSizeVO
|
||||||
const req = {
|
const req = {
|
||||||
prompt: prompt.value,
|
prompt: prompt.value,
|
||||||
model: selectModel.value,
|
modelId: matchedModel.id,
|
||||||
width: imageSize.width,
|
width: imageSize.width,
|
||||||
height: imageSize.height,
|
height: imageSize.height,
|
||||||
version: selectVersion.value,
|
version: selectVersion.value,
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt">
|
<div class="prompt">
|
||||||
<el-text tag="b">画面描述</el-text>
|
<el-text tag="b">画面描述</el-text>
|
||||||
<el-text tag="p">建议使用“形容词+动词+风格”的格式,使用“,”隔开</el-text>
|
<el-text tag="p">建议使用“形容词 + 动词 + 风格”的格式,使用“,”隔开</el-text>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="prompt"
|
v-model="prompt"
|
||||||
maxlength="1024"
|
maxlength="1024"
|
||||||
rows="5"
|
:rows="5"
|
||||||
class="w-100% mt-15px"
|
class="w-100% mt-15px"
|
||||||
input-style="border-radius: 7px;"
|
input-style="border-radius: 7px;"
|
||||||
placeholder="例如:童话里的小屋应该是什么样子?"
|
placeholder="例如:童话里的小屋应该是什么样子?"
|
||||||
@ -128,7 +128,14 @@
|
|||||||
</el-space>
|
</el-space>
|
||||||
</div>
|
</div>
|
||||||
<div class="btns">
|
<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 ? '生成中' : '生成内容' }}
|
{{ drawIn ? '生成中' : '生成内容' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@ -143,9 +150,19 @@ import {
|
|||||||
StableDiffusionSamplers,
|
StableDiffusionSamplers,
|
||||||
StableDiffusionStylePresets
|
StableDiffusionStylePresets
|
||||||
} from '@/views/ai/utils/constants'
|
} from '@/views/ai/utils/constants'
|
||||||
|
import { ModelVO } from '@/api/ai/model/model'
|
||||||
|
|
||||||
const message = useMessage() // 消息弹窗
|
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 drawIn = ref<boolean>(false) // 生成中
|
||||||
const selectHotWord = ref<string>('') // 选中的热词
|
const selectHotWord = ref<string>('') // 选中的热词
|
||||||
@ -160,8 +177,6 @@ const scale = ref<number>(7.5) // 引导系数
|
|||||||
const clipGuidancePreset = ref<string>('NONE') // 文本提示相匹配的图像(clip_guidance_preset) 简称 CLIP
|
const clipGuidancePreset = ref<string>('NONE') // 文本提示相匹配的图像(clip_guidance_preset) 简称 CLIP
|
||||||
const stylePreset = ref<string>('3d-model') // 风格
|
const stylePreset = ref<string>('3d-model') // 风格
|
||||||
|
|
||||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
|
|
||||||
|
|
||||||
/** 选择热词 */
|
/** 选择热词 */
|
||||||
const handleHotWordClick = async (hotWord: string) => {
|
const handleHotWordClick = async (hotWord: string) => {
|
||||||
// 情况一:取消选中
|
// 情况一:取消选中
|
||||||
@ -177,6 +192,16 @@ const handleHotWordClick = async (hotWord: string) => {
|
|||||||
|
|
||||||
/** 图片生成 */
|
/** 图片生成 */
|
||||||
const handleGenerateImage = async () => {
|
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)) {
|
if (hasChinese(prompt.value)) {
|
||||||
message.alert('暂不支持中文!')
|
message.alert('暂不支持中文!')
|
||||||
@ -191,8 +216,7 @@ const handleGenerateImage = async () => {
|
|||||||
emits('onDrawStart', AiPlatformEnum.STABLE_DIFFUSION)
|
emits('onDrawStart', AiPlatformEnum.STABLE_DIFFUSION)
|
||||||
// 发送请求
|
// 发送请求
|
||||||
const form = {
|
const form = {
|
||||||
platform: AiPlatformEnum.STABLE_DIFFUSION,
|
modelId: matchedModel.id,
|
||||||
model: 'stable-diffusion-v1-6',
|
|
||||||
prompt: prompt.value, // 提示词
|
prompt: prompt.value, // 提示词
|
||||||
width: width.value, // 图片宽度
|
width: width.value, // 图片宽度
|
||||||
height: height.value, // 图片高度
|
height: height.value, // 图片高度
|
||||||
|
@ -6,21 +6,28 @@
|
|||||||
<el-segmented v-model="selectPlatform" :options="platformOptions" />
|
<el-segmented v-model="selectPlatform" :options="platformOptions" />
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-switch-container">
|
<div class="modal-switch-container">
|
||||||
|
<Common
|
||||||
|
v-if="selectPlatform === 'common'"
|
||||||
|
ref="commonRef"
|
||||||
|
:models="models"
|
||||||
|
@on-draw-complete="handleDrawComplete"
|
||||||
|
/>
|
||||||
<Dall3
|
<Dall3
|
||||||
v-if="selectPlatform === AiPlatformEnum.OPENAI"
|
v-if="selectPlatform === AiPlatformEnum.OPENAI"
|
||||||
ref="dall3Ref"
|
ref="dall3Ref"
|
||||||
|
:models="models"
|
||||||
@on-draw-start="handleDrawStart"
|
@on-draw-start="handleDrawStart"
|
||||||
@on-draw-complete="handleDrawComplete"
|
@on-draw-complete="handleDrawComplete"
|
||||||
/>
|
/>
|
||||||
<Midjourney v-if="selectPlatform === AiPlatformEnum.MIDJOURNEY" ref="midjourneyRef" />
|
<Midjourney
|
||||||
|
v-if="selectPlatform === AiPlatformEnum.MIDJOURNEY"
|
||||||
|
ref="midjourneyRef"
|
||||||
|
:models="models"
|
||||||
|
/>
|
||||||
<StableDiffusion
|
<StableDiffusion
|
||||||
v-if="selectPlatform === AiPlatformEnum.STABLE_DIFFUSION"
|
v-if="selectPlatform === AiPlatformEnum.STABLE_DIFFUSION"
|
||||||
ref="stableDiffusionRef"
|
ref="stableDiffusionRef"
|
||||||
@on-draw-complete="handleDrawComplete"
|
:models="models"
|
||||||
/>
|
|
||||||
<Other
|
|
||||||
v-if="selectPlatform === 'other'"
|
|
||||||
ref="otherRef"
|
|
||||||
@on-draw-complete="handleDrawComplete"
|
@on-draw-complete="handleDrawComplete"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -38,17 +45,23 @@ import { ImageVO } from '@/api/ai/image'
|
|||||||
import Dall3 from './components/dall3/index.vue'
|
import Dall3 from './components/dall3/index.vue'
|
||||||
import Midjourney from './components/midjourney/index.vue'
|
import Midjourney from './components/midjourney/index.vue'
|
||||||
import StableDiffusion from './components/stableDiffusion/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 imageListRef = ref<any>() // image 列表 ref
|
||||||
const dall3Ref = ref<any>() // dall3(openai) ref
|
const dall3Ref = ref<any>() // dall3(openai) ref
|
||||||
const midjourneyRef = ref<any>() // midjourney ref
|
const midjourneyRef = ref<any>() // midjourney ref
|
||||||
const stableDiffusionRef = ref<any>() // stable diffusion 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 = [
|
const platformOptions = [
|
||||||
|
{
|
||||||
|
label: '通用',
|
||||||
|
value: 'common'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'DALL3 绘画',
|
label: 'DALL3 绘画',
|
||||||
value: AiPlatformEnum.OPENAI
|
value: AiPlatformEnum.OPENAI
|
||||||
@ -58,15 +71,13 @@ const platformOptions = [
|
|||||||
value: AiPlatformEnum.MIDJOURNEY
|
value: AiPlatformEnum.MIDJOURNEY
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Stable Diffusion',
|
label: 'SD 绘图',
|
||||||
value: AiPlatformEnum.STABLE_DIFFUSION
|
value: AiPlatformEnum.STABLE_DIFFUSION
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '其它',
|
|
||||||
value: 'other'
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const models = ref<ModelVO[]>([]) // 模型列表
|
||||||
|
|
||||||
/** 绘画 start */
|
/** 绘画 start */
|
||||||
const handleDrawStart = async (platform: string) => {}
|
const handleDrawStart = async (platform: string) => {}
|
||||||
|
|
||||||
@ -75,7 +86,7 @@ const handleDrawComplete = async (platform: string) => {
|
|||||||
await imageListRef.value.getImageList()
|
await imageListRef.value.getImageList()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 重新生成:将画图详情填充到对应平台 */
|
/** 重新生成:将画图详情填充到对应平台 */
|
||||||
const handleRegeneration = async (image: ImageVO) => {
|
const handleRegeneration = async (image: ImageVO) => {
|
||||||
// 切换平台
|
// 切换平台
|
||||||
selectPlatform.value = image.platform
|
selectPlatform.value = image.platform
|
||||||
@ -90,6 +101,12 @@ const handleRegeneration = async (image: ImageVO) => {
|
|||||||
}
|
}
|
||||||
// TODO @fan:貌似 other 重新设置不行?
|
// TODO @fan:貌似 other 重新设置不行?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 组件挂载的时候 */
|
||||||
|
onMounted(async () => {
|
||||||
|
// 获取模型列表
|
||||||
|
models.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.IMAGE)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@ -109,10 +126,7 @@ const handleRegeneration = async (image: ImageVO) => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
width: 350px;
|
width: 390px;
|
||||||
|
|
||||||
.segmented {
|
|
||||||
}
|
|
||||||
|
|
||||||
.segmented .el-segmented {
|
.segmented .el-segmented {
|
||||||
--el-border-radius-base: 16px;
|
--el-border-radius-base: 16px;
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<doc-alert title="AI 绘图创作" url="https://doc.iocoder.cn/ai/image/" />
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
146
src/views/ai/knowledge/document/form/ProcessStep.vue
Normal file
146
src/views/ai/knowledge/document/form/ProcessStep.vue
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- 文件处理列表 -->
|
||||||
|
<div class="mt-15px grid grid-cols-1 gap-2">
|
||||||
|
<div
|
||||||
|
v-for="(file, index) in modelValue.list"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-center py-4px px-12px border-l-4 border-l-[#409eff] rounded-sm shadow-sm hover:bg-[#ecf5ff] transition-all duration-300"
|
||||||
|
>
|
||||||
|
<!-- 文件图标和名称 -->
|
||||||
|
<div class="flex items-center min-w-[200px] mr-10px">
|
||||||
|
<Icon icon="ep:document" class="mr-8px text-[#409eff]" />
|
||||||
|
<span class="text-[13px] text-[#303133] break-all">{{ file.name }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 处理进度 -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<el-progress
|
||||||
|
:percentage="file.progress || 0"
|
||||||
|
:stroke-width="10"
|
||||||
|
:status="isProcessComplete(file) ? 'success' : ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分段数量 -->
|
||||||
|
<div class="ml-10px text-[13px] text-[#606266]">
|
||||||
|
分段数量:{{ file.count ? file.count : '-' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部完成按钮 -->
|
||||||
|
<div class="flex justify-end mt-20px">
|
||||||
|
<el-button
|
||||||
|
:type="allProcessComplete ? 'success' : 'primary'"
|
||||||
|
:disabled="!allProcessComplete"
|
||||||
|
@click="handleComplete"
|
||||||
|
>
|
||||||
|
完成
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { KnowledgeSegmentApi } from '@/api/ai/knowledge/segment'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
const parent = inject('parent') as any
|
||||||
|
const pollingTimer = ref<number | null>(null) // 轮询定时器 ID,用于跟踪和清除轮询进程
|
||||||
|
|
||||||
|
/** 判断文件处理是否完成 */
|
||||||
|
const isProcessComplete = (file) => {
|
||||||
|
return file.progress === 100
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断所有文件是否都处理完成 */
|
||||||
|
const allProcessComplete = computed(() => {
|
||||||
|
return props.modelValue.list.every((file) => isProcessComplete(file))
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 完成按钮点击事件处理 */
|
||||||
|
const handleComplete = () => {
|
||||||
|
if (parent?.exposed?.handleBack) {
|
||||||
|
parent.exposed.handleBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取文件处理进度 */
|
||||||
|
const getProcessList = async () => {
|
||||||
|
try {
|
||||||
|
// 1. 调用 API 获取处理进度
|
||||||
|
const documentIds = props.modelValue.list.filter((item) => item.id).map((item) => item.id)
|
||||||
|
if (documentIds.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const result = await KnowledgeSegmentApi.getKnowledgeSegmentProcessList(documentIds)
|
||||||
|
|
||||||
|
// 2.1更新进度
|
||||||
|
const updatedList = props.modelValue.list.map((file) => {
|
||||||
|
const processInfo = result.find((item) => item.documentId === file.id)
|
||||||
|
if (processInfo) {
|
||||||
|
// 计算进度百分比:已嵌入数量 / 总数量 * 100
|
||||||
|
const progress =
|
||||||
|
processInfo.embeddingCount && processInfo.count
|
||||||
|
? Math.floor((processInfo.embeddingCount / processInfo.count) * 100)
|
||||||
|
: 0
|
||||||
|
return {
|
||||||
|
...file,
|
||||||
|
progress: progress,
|
||||||
|
count: processInfo.count || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return file
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2.2 更新数据
|
||||||
|
emit('update:modelValue', {
|
||||||
|
...props.modelValue,
|
||||||
|
list: updatedList
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3. 如果未完成,继续轮询
|
||||||
|
if (!updatedList.every((file) => isProcessComplete(file))) {
|
||||||
|
pollingTimer.value = window.setTimeout(getProcessList, 3000)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 出错后也继续轮询
|
||||||
|
console.error('获取处理进度失败:', error)
|
||||||
|
pollingTimer.value = window.setTimeout(getProcessList, 5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 组件挂载时开始轮询 */
|
||||||
|
onMounted(() => {
|
||||||
|
// 1. 初始化进度为 0
|
||||||
|
const initialList = props.modelValue.list.map((file) => ({
|
||||||
|
...file,
|
||||||
|
progress: 0
|
||||||
|
}))
|
||||||
|
|
||||||
|
emit('update:modelValue', {
|
||||||
|
...props.modelValue,
|
||||||
|
list: initialList
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. 开始轮询获取进度
|
||||||
|
getProcessList()
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 组件卸载前清除轮询 */
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
// 1. 清除定时器
|
||||||
|
if (pollingTimer.value) {
|
||||||
|
clearTimeout(pollingTimer.value)
|
||||||
|
pollingTimer.value = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
238
src/views/ai/knowledge/document/form/SplitStep.vue
Normal file
238
src/views/ai/knowledge/document/form/SplitStep.vue
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- 上部分段设置部分 -->
|
||||||
|
<div class="mb-20px">
|
||||||
|
<div class="mb-20px flex justify-between items-center">
|
||||||
|
<div class="text-16px font-bold flex items-center">
|
||||||
|
分段设置
|
||||||
|
<el-tooltip
|
||||||
|
content="系统会自动将文档内容分割成多个段落,您可以根据需要调整分段方式和内容。"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<Icon icon="ep:warning" class="ml-5px text-gray-400" />
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<el-button type="primary" plain size="small" @click="handleAutoSegment">
|
||||||
|
预览分段
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="segment-settings mb-20px">
|
||||||
|
<el-form label-width="120px">
|
||||||
|
<el-form-item label="最大 Token 数">
|
||||||
|
<el-input-number v-model="modelData.segmentMaxTokens" :min="1" :max="2048" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 下部文件预览部分 -->
|
||||||
|
<div class="mb-10px">
|
||||||
|
<div class="text-16px font-bold mb-10px">分段预览</div>
|
||||||
|
|
||||||
|
<!-- 文件选择器 -->
|
||||||
|
<div class="file-selector mb-10px">
|
||||||
|
<el-dropdown v-if="modelData.list && modelData.list.length > 0" trigger="click">
|
||||||
|
<div class="flex items-center cursor-pointer">
|
||||||
|
<Icon icon="ep:document" class="text-danger mr-5px" />
|
||||||
|
<span>{{ currentFile?.name || '请选择文件' }}</span>
|
||||||
|
<span v-if="currentFile?.segments" class="ml-5px text-gray-500 text-12px">
|
||||||
|
({{ currentFile.segments.length }}个分片)
|
||||||
|
</span>
|
||||||
|
<Icon icon="ep:arrow-down" class="ml-5px" />
|
||||||
|
</div>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item
|
||||||
|
v-for="(file, index) in modelData.list"
|
||||||
|
:key="index"
|
||||||
|
@click="selectFile(index)"
|
||||||
|
>
|
||||||
|
{{ file.name }}
|
||||||
|
<span v-if="file.segments" class="ml-5px text-gray-500 text-12px">
|
||||||
|
({{ file.segments.length }}个分片)
|
||||||
|
</span>
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
<div v-else class="text-gray-400">暂无上传文件</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文件内容预览 -->
|
||||||
|
<div class="file-preview bg-gray-50 p-15px rounded-md max-h-600px overflow-y-auto">
|
||||||
|
<div v-if="splitLoading" class="flex justify-center items-center py-20px">
|
||||||
|
<Icon icon="ep:loading" class="is-loading" />
|
||||||
|
<span class="ml-10px">正在加载分段内容...</span>
|
||||||
|
</div>
|
||||||
|
<template
|
||||||
|
v-else-if="currentFile && currentFile.segments && currentFile.segments.length > 0"
|
||||||
|
>
|
||||||
|
<div v-for="(segment, index) in currentFile.segments" :key="index" class="mb-10px">
|
||||||
|
<div class="text-gray-500 text-12px mb-5px">
|
||||||
|
分片-{{ index + 1 }} · {{ segment.contentLength || 0 }} 字符数 ·
|
||||||
|
{{ segment.tokens || 0 }} Token
|
||||||
|
</div>
|
||||||
|
<div class="bg-white p-10px rounded-md">{{ segment.content }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-empty v-else description="暂无预览内容" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加底部按钮 -->
|
||||||
|
<div class="mt-20px flex justify-between">
|
||||||
|
<div>
|
||||||
|
<el-button v-if="!modelData.id" @click="handlePrevStep">上一步</el-button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<el-button type="primary" :loading="submitLoading" @click="handleSave">
|
||||||
|
保存并处理
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, getCurrentInstance, inject, onMounted, PropType, ref } from 'vue'
|
||||||
|
import { Icon } from '@/components/Icon'
|
||||||
|
import { KnowledgeSegmentApi } from '@/api/ai/knowledge/segment'
|
||||||
|
import { useMessage } from '@/hooks/web/useMessage'
|
||||||
|
import { KnowledgeDocumentApi } from '@/api/ai/knowledge/document'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Object as PropType<any>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
const message = useMessage() // 消息提示
|
||||||
|
const parent = inject('parent', null) // 获取父组件实例
|
||||||
|
|
||||||
|
const modelData = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', val)
|
||||||
|
}) // 表单数据
|
||||||
|
|
||||||
|
const splitLoading = ref(false) // 分段加载状态
|
||||||
|
const currentFile = ref<any>(null) // 当前选中的文件
|
||||||
|
const submitLoading = ref(false) // 提交按钮加载状态
|
||||||
|
|
||||||
|
/** 选择文件 */
|
||||||
|
const selectFile = async (index: number) => {
|
||||||
|
currentFile.value = modelData.value.list[index]
|
||||||
|
await splitContent(currentFile.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取文件分段内容 */
|
||||||
|
const splitContent = async (file: any) => {
|
||||||
|
if (!file || !file.url) {
|
||||||
|
message.warning('文件 URL 不存在')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
splitLoading.value = true
|
||||||
|
try {
|
||||||
|
// 调用后端分段接口,获取文档的分段内容、字符数和 Token 数
|
||||||
|
file.segments = await KnowledgeSegmentApi.splitContent(
|
||||||
|
file.url,
|
||||||
|
modelData.value.segmentMaxTokens
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取分段内容失败:', file, error)
|
||||||
|
} finally {
|
||||||
|
splitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理预览分段 */
|
||||||
|
const handleAutoSegment = async () => {
|
||||||
|
// 如果没有选中文件,默认选中第一个
|
||||||
|
if (!currentFile.value && modelData.value.list && modelData.value.list.length > 0) {
|
||||||
|
currentFile.value = modelData.value.list[0]
|
||||||
|
}
|
||||||
|
// 如果没有选中文件,提示请先选择文件
|
||||||
|
if (!currentFile.value) {
|
||||||
|
message.warning('请先选择文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取分段内容
|
||||||
|
await splitContent(currentFile.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 上一步按钮处理 */
|
||||||
|
const handlePrevStep = () => {
|
||||||
|
const parentEl = parent || getCurrentInstance()?.parent
|
||||||
|
if (parentEl && typeof parentEl.exposed?.goToPrevStep === 'function') {
|
||||||
|
parentEl.exposed.goToPrevStep()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存操作 */
|
||||||
|
const handleSave = async () => {
|
||||||
|
// 保存前验证
|
||||||
|
if (!currentFile?.value?.segments || currentFile.value.segments.length === 0) {
|
||||||
|
message.warning('请先预览分段内容')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置按钮加载状态
|
||||||
|
submitLoading.value = true
|
||||||
|
try {
|
||||||
|
if (modelData.value.id) {
|
||||||
|
// 修改场景
|
||||||
|
await KnowledgeDocumentApi.updateKnowledgeDocument({
|
||||||
|
id: modelData.value.id,
|
||||||
|
segmentMaxTokens: modelData.value.segmentMaxTokens
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 新增场景
|
||||||
|
const data = await KnowledgeDocumentApi.createKnowledgeDocumentList({
|
||||||
|
knowledgeId: modelData.value.knowledgeId,
|
||||||
|
segmentMaxTokens: modelData.value.segmentMaxTokens,
|
||||||
|
list: modelData.value.list.map((item: any) => ({
|
||||||
|
name: item.name,
|
||||||
|
url: item.url
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
modelData.value.list.forEach((document: any, index: number) => {
|
||||||
|
document.id = data[index]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进入下一步
|
||||||
|
const parentEl = parent || getCurrentInstance()?.parent
|
||||||
|
if (parentEl && typeof parentEl.exposed?.goToNextStep === 'function') {
|
||||||
|
parentEl.exposed.goToNextStep()
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('保存失败:', modelData.value, error)
|
||||||
|
} finally {
|
||||||
|
// 关闭按钮加载状态
|
||||||
|
submitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(async () => {
|
||||||
|
// 确保 segmentMaxTokens 存在
|
||||||
|
if (!modelData.value.segmentMaxTokens) {
|
||||||
|
modelData.value.segmentMaxTokens = 500
|
||||||
|
}
|
||||||
|
// 如果没有选中文件,默认选中第一个
|
||||||
|
if (!currentFile.value && modelData.value.list && modelData.value.list.length > 0) {
|
||||||
|
currentFile.value = modelData.value.list[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有选中的文件,获取分段内容
|
||||||
|
if (currentFile.value) {
|
||||||
|
await splitContent(currentFile.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
273
src/views/ai/knowledge/document/form/UploadStep.vue
Normal file
273
src/views/ai/knowledge/document/form/UploadStep.vue
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
<template>
|
||||||
|
<el-form ref="formRef" :model="modelData" label-width="0" class="mt-20px">
|
||||||
|
<el-form-item class="mb-20px">
|
||||||
|
<div class="w-full">
|
||||||
|
<div
|
||||||
|
class="w-full border-2 border-dashed border-[#dcdfe6] rounded-md p-20px text-center hover:border-[#409eff]"
|
||||||
|
>
|
||||||
|
<el-upload
|
||||||
|
ref="uploadRef"
|
||||||
|
class="upload-demo"
|
||||||
|
drag
|
||||||
|
:action="uploadUrl"
|
||||||
|
:auto-upload="true"
|
||||||
|
:on-success="handleUploadSuccess"
|
||||||
|
:on-error="handleUploadError"
|
||||||
|
:on-change="handleFileChange"
|
||||||
|
:on-remove="handleFileRemove"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:http-request="httpRequest"
|
||||||
|
:file-list="fileList"
|
||||||
|
:multiple="true"
|
||||||
|
:show-file-list="false"
|
||||||
|
:accept="acceptedFileTypes"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center justify-center py-20px">
|
||||||
|
<Icon icon="ep:upload-filled" class="text-[48px] text-[#c0c4cc] mb-10px" />
|
||||||
|
<div class="el-upload__text text-[16px] text-[#606266]">
|
||||||
|
拖拽文件至此,或者
|
||||||
|
<em class="text-[#409eff] not-italic cursor-pointer">选择文件</em>
|
||||||
|
</div>
|
||||||
|
<div class="el-upload__tip mt-10px text-[#909399] text-[12px]">
|
||||||
|
已支持 {{ supportedFileTypes.join('、') }},每个文件不超过 {{ maxFileSize }} MB。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-upload>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="modelData.list && modelData.list.length > 0"
|
||||||
|
class="mt-15px grid grid-cols-1 gap-2"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(file, index) in modelData.list"
|
||||||
|
:key="index"
|
||||||
|
class="flex justify-between items-center py-4px px-12px border-l-4 border-l-[#409eff] rounded-sm shadow-sm hover:bg-[#ecf5ff] transition-all duration-300"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon icon="ep:document" class="mr-8px text-[#409eff]" />
|
||||||
|
<span class="text-[13px] text-[#303133] break-all">{{ file.name }}</span>
|
||||||
|
</div>
|
||||||
|
<el-button type="danger" link @click="removeFile(index)" class="ml-2">
|
||||||
|
<Icon icon="ep:delete" />
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 添加下一步按钮 -->
|
||||||
|
<el-form-item>
|
||||||
|
<div class="flex justify-end w-full">
|
||||||
|
<el-button type="primary" @click="handleNextStep" :disabled="!isAllUploaded">
|
||||||
|
下一步
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { PropType, ref, computed, inject, getCurrentInstance, onMounted } from 'vue'
|
||||||
|
import { useMessage } from '@/hooks/web/useMessage'
|
||||||
|
import { useUpload } from '@/components/UploadFile/src/useUpload'
|
||||||
|
import { generateAcceptedFileTypes } from '@/utils'
|
||||||
|
import { Icon } from '@/components/Icon'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Object as PropType<any>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const formRef = ref() // 表单引用
|
||||||
|
const uploadRef = ref() // 上传组件引用
|
||||||
|
const parent = inject('parent', null) // 获取父组件实例
|
||||||
|
const { uploadUrl, httpRequest } = useUpload() // 使用上传组件的钩子
|
||||||
|
const message = useMessage() // 消息弹窗
|
||||||
|
const fileList = ref([]) // 文件列表
|
||||||
|
const uploadingCount = ref(0) // 上传中的文件数量
|
||||||
|
|
||||||
|
// 支持的文件类型和大小限制
|
||||||
|
const supportedFileTypes = [
|
||||||
|
'TXT',
|
||||||
|
'MARKDOWN',
|
||||||
|
'MDX',
|
||||||
|
'PDF',
|
||||||
|
'HTML',
|
||||||
|
'XLSX',
|
||||||
|
'XLS',
|
||||||
|
'DOC',
|
||||||
|
'DOCX',
|
||||||
|
'CSV',
|
||||||
|
'EML',
|
||||||
|
'MSG',
|
||||||
|
'PPTX',
|
||||||
|
'XML',
|
||||||
|
'EPUB',
|
||||||
|
'PPT',
|
||||||
|
'MD',
|
||||||
|
'HTM'
|
||||||
|
]
|
||||||
|
const allowedExtensions = supportedFileTypes.map((ext) => ext.toLowerCase()) // 小写的扩展名列表
|
||||||
|
const maxFileSize = 15 // 最大文件大小(MB)
|
||||||
|
|
||||||
|
// 构建 accept 属性值,用于限制文件选择对话框中可见的文件类型
|
||||||
|
const acceptedFileTypes = computed(() => generateAcceptedFileTypes(supportedFileTypes))
|
||||||
|
|
||||||
|
/** 表单数据 */
|
||||||
|
const modelData = computed({
|
||||||
|
get: () => {
|
||||||
|
return props.modelValue
|
||||||
|
},
|
||||||
|
set: (val) => emit('update:modelValue', val)
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 确保 list 属性存在 */
|
||||||
|
const ensureListExists = () => {
|
||||||
|
if (!props.modelValue.list) {
|
||||||
|
emit('update:modelValue', {
|
||||||
|
...props.modelValue,
|
||||||
|
list: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否所有文件都已上传完成 */
|
||||||
|
const isAllUploaded = computed(() => {
|
||||||
|
return modelData.value.list && modelData.value.list.length > 0 && uploadingCount.value === 0
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传前检查文件类型和大小
|
||||||
|
*
|
||||||
|
* @param file 待上传的文件
|
||||||
|
* @returns 是否允许上传
|
||||||
|
*/
|
||||||
|
const beforeUpload = (file) => {
|
||||||
|
// 1.1 检查文件扩展名
|
||||||
|
const fileName = file.name.toLowerCase()
|
||||||
|
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1)
|
||||||
|
if (!allowedExtensions.includes(fileExtension)) {
|
||||||
|
message.error('不支持的文件类型!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 1.2 检查文件大小
|
||||||
|
if (!(file.size / 1024 / 1024 < maxFileSize)) {
|
||||||
|
message.error(`文件大小不能超过 ${maxFileSize} MB!`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 增加上传中的文件计数
|
||||||
|
uploadingCount.value++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件上传成功处理
|
||||||
|
*
|
||||||
|
* @param response 上传响应
|
||||||
|
* @param file 上传的文件
|
||||||
|
*/
|
||||||
|
const handleUploadSuccess = (response, file) => {
|
||||||
|
// 添加到文件列表
|
||||||
|
if (response && response.data) {
|
||||||
|
ensureListExists()
|
||||||
|
emit('update:modelValue', {
|
||||||
|
...props.modelValue,
|
||||||
|
list: [
|
||||||
|
...props.modelValue.list,
|
||||||
|
{
|
||||||
|
name: file.name,
|
||||||
|
url: response.data
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
message.error(`文件 ${file.name} 上传失败`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 减少上传中的文件计数
|
||||||
|
uploadingCount.value = Math.max(0, uploadingCount.value - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件上传失败处理
|
||||||
|
*
|
||||||
|
* @param error 错误信息
|
||||||
|
* @param file 上传的文件
|
||||||
|
*/
|
||||||
|
const handleUploadError = (error, file) => {
|
||||||
|
message.error(`文件 ${file.name} 上传失败: ${error}`)
|
||||||
|
// 减少上传中的文件计数
|
||||||
|
uploadingCount.value = Math.max(0, uploadingCount.value - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件变更处理
|
||||||
|
*
|
||||||
|
* @param file 变更的文件
|
||||||
|
*/
|
||||||
|
const handleFileChange = (file) => {
|
||||||
|
if (file.status === 'success' || file.status === 'fail') {
|
||||||
|
uploadingCount.value = Math.max(0, uploadingCount.value - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件移除处理
|
||||||
|
*
|
||||||
|
* @param file 被移除的文件
|
||||||
|
*/
|
||||||
|
const handleFileRemove = (file) => {
|
||||||
|
if (file.status === 'uploading') {
|
||||||
|
uploadingCount.value = Math.max(0, uploadingCount.value - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从列表中移除文件
|
||||||
|
*
|
||||||
|
* @param index 要移除的文件索引
|
||||||
|
*/
|
||||||
|
const removeFile = (index: number) => {
|
||||||
|
// 从列表中移除文件
|
||||||
|
const newList = [...props.modelValue.list]
|
||||||
|
newList.splice(index, 1)
|
||||||
|
// 更新表单数据
|
||||||
|
emit('update:modelValue', {
|
||||||
|
...props.modelValue,
|
||||||
|
list: newList
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 下一步按钮处理 */
|
||||||
|
const handleNextStep = () => {
|
||||||
|
// 1.1 检查是否有文件上传
|
||||||
|
if (!modelData.value.list || modelData.value.list.length === 0) {
|
||||||
|
message.warning('请上传至少一个文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 1.2 检查是否有文件正在上传
|
||||||
|
if (uploadingCount.value > 0) {
|
||||||
|
message.warning('请等待所有文件上传完成')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取父组件的goToNextStep方法
|
||||||
|
const parentEl = parent || getCurrentInstance()?.parent
|
||||||
|
if (parentEl && typeof parentEl.exposed?.goToNextStep === 'function') {
|
||||||
|
parentEl.exposed.goToNextStep()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(() => {
|
||||||
|
ensureListExists()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
193
src/views/ai/knowledge/document/form/index.vue
Normal file
193
src/views/ai/knowledge/document/form/index.vue
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
<template>
|
||||||
|
<ContentWrap>
|
||||||
|
<div class="mx-auto">
|
||||||
|
<!-- 头部导航栏 -->
|
||||||
|
<div
|
||||||
|
class="absolute top-0 left-0 right-0 h-50px bg-white border-bottom z-10 flex items-center px-20px"
|
||||||
|
>
|
||||||
|
<!-- 左侧标题 -->
|
||||||
|
<div class="w-200px flex items-center overflow-hidden">
|
||||||
|
<Icon icon="ep:arrow-left" class="cursor-pointer flex-shrink-0" @click="handleBack" />
|
||||||
|
<span class="ml-10px text-16px truncate">
|
||||||
|
{{ formData.id ? '编辑知识库文档' : '创建知识库文档' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 步骤条 -->
|
||||||
|
<div class="flex-1 flex items-center justify-center h-full">
|
||||||
|
<div class="w-400px flex items-center justify-between h-full">
|
||||||
|
<div
|
||||||
|
v-for="(step, index) in steps"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-center mx-15px relative h-full"
|
||||||
|
:class="[
|
||||||
|
currentStep === index
|
||||||
|
? 'text-[#3473ff] border-[#3473ff] border-b-2 border-b-solid'
|
||||||
|
: 'text-gray-500'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-28px h-28px rounded-full flex items-center justify-center mr-8px border-2 border-solid text-15px"
|
||||||
|
:class="[
|
||||||
|
currentStep === index
|
||||||
|
? 'bg-[#3473ff] text-white border-[#3473ff]'
|
||||||
|
: 'border-gray-300 bg-white text-gray-500'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ index + 1 }}
|
||||||
|
</div>
|
||||||
|
<span class="text-16px font-bold whitespace-nowrap">{{ step.title }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧按钮 - 已移除 -->
|
||||||
|
<div class="w-200px flex items-center justify-end gap-2"> </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主体内容 -->
|
||||||
|
<div class="mt-50px">
|
||||||
|
<!-- 第一步:上传文档 -->
|
||||||
|
<div v-if="currentStep === 0" class="mx-auto w-560px">
|
||||||
|
<UploadStep v-model="formData" ref="uploadDocumentRef" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第二步:文档分段 -->
|
||||||
|
<div v-if="currentStep === 1" class="mx-auto w-560px">
|
||||||
|
<SplitStep v-model="formData" ref="documentSegmentRef" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第三步:处理并完成 -->
|
||||||
|
<div v-if="currentStep === 2" class="mx-auto w-560px">
|
||||||
|
<ProcessStep v-model="formData" ref="processCompleteRef" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ContentWrap>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||||
|
import UploadStep from './UploadStep.vue'
|
||||||
|
import SplitStep from './SplitStep.vue'
|
||||||
|
import ProcessStep from './ProcessStep.vue'
|
||||||
|
import { KnowledgeDocumentApi } from '@/api/ai/knowledge/document'
|
||||||
|
|
||||||
|
const { delView } = useTagsViewStore() // 视图操作
|
||||||
|
const route = useRoute() // 路由
|
||||||
|
const router = useRouter() // 路由
|
||||||
|
|
||||||
|
// 组件引用
|
||||||
|
const uploadDocumentRef = ref()
|
||||||
|
const documentSegmentRef = ref()
|
||||||
|
const processCompleteRef = ref()
|
||||||
|
const currentStep = ref(0) // 步骤控制
|
||||||
|
const steps = [{ title: '上传文档' }, { title: '文档分段' }, { title: '处理并完成' }]
|
||||||
|
const formData = ref({
|
||||||
|
knowledgeId: undefined, // 知识库编号
|
||||||
|
id: undefined, // 编辑的文档编号(documentId)
|
||||||
|
segmentMaxTokens: 500, // 分段最大 token 数
|
||||||
|
list: [] as Array<{
|
||||||
|
id: number // 文档编号
|
||||||
|
name: string // 文档名称
|
||||||
|
url: string // 文档 URL
|
||||||
|
segments: Array<{
|
||||||
|
content?: string
|
||||||
|
contentLength?: number
|
||||||
|
tokens?: number
|
||||||
|
}>
|
||||||
|
count?: number // 段落数量
|
||||||
|
process?: number // 处理进度
|
||||||
|
}> // 用于存储上传的文件列表
|
||||||
|
}) // 表单数据
|
||||||
|
|
||||||
|
provide('parent', getCurrentInstance()) // 提供 parent 给子组件使用
|
||||||
|
|
||||||
|
/** 初始化数据 */
|
||||||
|
const initData = async () => {
|
||||||
|
// 【新增场景】从路由参数中获取知识库 ID
|
||||||
|
if (route.query.knowledgeId) {
|
||||||
|
formData.value.knowledgeId = route.query.knowledgeId as any
|
||||||
|
}
|
||||||
|
|
||||||
|
// 【修改场景】从路由参数中获取文档 ID
|
||||||
|
const documentId = route.query.id
|
||||||
|
if (documentId) {
|
||||||
|
// 获取文档信息
|
||||||
|
formData.value.id = documentId as any
|
||||||
|
const document = await KnowledgeDocumentApi.getKnowledgeDocument(documentId as any)
|
||||||
|
formData.value.segmentMaxTokens = document.segmentMaxTokens
|
||||||
|
formData.value.list = [
|
||||||
|
{
|
||||||
|
id: document.id,
|
||||||
|
name: document.name,
|
||||||
|
url: document.url,
|
||||||
|
segments: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
// 进入下一步
|
||||||
|
goToNextStep()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换到下一步 */
|
||||||
|
const goToNextStep = () => {
|
||||||
|
if (currentStep.value < steps.length - 1) {
|
||||||
|
currentStep.value++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换到上一步 */
|
||||||
|
const goToPrevStep = () => {
|
||||||
|
if (currentStep.value > 0) {
|
||||||
|
currentStep.value--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 返回列表页 */
|
||||||
|
const handleBack = () => {
|
||||||
|
// 先删除当前页签
|
||||||
|
delView(unref(router.currentRoute))
|
||||||
|
// 跳转到列表页
|
||||||
|
router.push({ name: 'AiKnowledgeDocument', query: { knowledgeId: formData.value.knowledgeId } })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(async () => {
|
||||||
|
await initData()
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 添加组件卸载前的清理代码 */
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
// 清理所有的引用
|
||||||
|
uploadDocumentRef.value = null
|
||||||
|
documentSegmentRef.value = null
|
||||||
|
processCompleteRef.value = null
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 暴露方法给子组件使用 */
|
||||||
|
defineExpose({
|
||||||
|
goToNextStep,
|
||||||
|
goToPrevStep,
|
||||||
|
handleBack
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.border-bottom {
|
||||||
|
border-bottom: 1px solid #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-primary {
|
||||||
|
color: #3473ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-primary {
|
||||||
|
background-color: #3473ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-primary {
|
||||||
|
border-color: #3473ff;
|
||||||
|
}
|
||||||
|
</style>
|
236
src/views/ai/knowledge/document/index.vue
Normal file
236
src/views/ai/knowledge/document/index.vue
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
<template>
|
||||||
|
<ContentWrap>
|
||||||
|
<!-- 搜索工作栏 -->
|
||||||
|
<el-form
|
||||||
|
class="-mb-15px"
|
||||||
|
:model="queryParams"
|
||||||
|
ref="queryFormRef"
|
||||||
|
:inline="true"
|
||||||
|
label-width="68px"
|
||||||
|
>
|
||||||
|
<el-form-item label="文件名称" prop="name">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.name"
|
||||||
|
placeholder="请输入文件名称"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
class="!w-240px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="是否启用" prop="status">
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.status"
|
||||||
|
placeholder="请选择是否启用"
|
||||||
|
clearable
|
||||||
|
class="!w-240px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||||
|
:key="dict.value"
|
||||||
|
:label="dict.label"
|
||||||
|
:value="dict.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||||
|
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||||
|
<el-button type="primary" plain @click="handleCreate" v-hasPermi="['ai:knowledge:create']">
|
||||||
|
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</ContentWrap>
|
||||||
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<ContentWrap>
|
||||||
|
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||||
|
<el-table-column label="文档编号" align="center" prop="id" />
|
||||||
|
<el-table-column label="文件名称" align="center" prop="name" />
|
||||||
|
<el-table-column label="字符数" align="center" prop="contentLength" />
|
||||||
|
<el-table-column label="Token 数" align="center" prop="tokens" />
|
||||||
|
<el-table-column label="分片最大 Token 数" align="center" prop="segmentMaxTokens" />
|
||||||
|
<el-table-column label="召回次数" align="center" prop="retrievalCount" />
|
||||||
|
<el-table-column label="是否启用" align="center" prop="status">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-switch
|
||||||
|
v-model="scope.row.status"
|
||||||
|
:active-value="0"
|
||||||
|
:inactive-value="1"
|
||||||
|
@change="handleStatusChange(scope.row)"
|
||||||
|
:disabled="!checkPermi(['ai:knowledge:update'])"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
label="上传时间"
|
||||||
|
align="center"
|
||||||
|
prop="createTime"
|
||||||
|
:formatter="dateFormatter"
|
||||||
|
width="180px"
|
||||||
|
/>
|
||||||
|
<el-table-column label="操作" align="center" min-width="120px">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
@click="handleUpdate(scope.row.id)"
|
||||||
|
v-hasPermi="['ai:knowledge:update']"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
@click="handleSegment(scope.row.id)"
|
||||||
|
v-hasPermi="['ai:knowledge:query']"
|
||||||
|
>
|
||||||
|
分段
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
@click="handleDelete(scope.row.id)"
|
||||||
|
v-hasPermi="['ai:knowledge:delete']"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<!-- 分页 -->
|
||||||
|
<Pagination
|
||||||
|
:total="total"
|
||||||
|
v-model:page="queryParams.pageNo"
|
||||||
|
v-model:limit="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</ContentWrap>
|
||||||
|
|
||||||
|
<!-- 表单弹窗:添加/修改 -->
|
||||||
|
<!-- <KnowledgeDocumentForm ref="formRef" @success="getList" /> -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||||
|
import { dateFormatter } from '@/utils/formatTime'
|
||||||
|
import { KnowledgeDocumentApi, KnowledgeDocumentVO } from '@/api/ai/knowledge/document'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { checkPermi } from '@/utils/permission'
|
||||||
|
import { CommonStatusEnum } from '@/utils/constants'
|
||||||
|
// import KnowledgeDocumentForm from './KnowledgeDocumentForm.vue'
|
||||||
|
|
||||||
|
/** AI 知识库文档 列表 */
|
||||||
|
defineOptions({ name: 'KnowledgeDocument' })
|
||||||
|
|
||||||
|
const message = useMessage() // 消息弹窗
|
||||||
|
const { t } = useI18n() // 国际化
|
||||||
|
const route = useRoute() // 路由
|
||||||
|
const router = useRouter() // 路由
|
||||||
|
|
||||||
|
const loading = ref(true) // 列表的加载中
|
||||||
|
const list = ref<KnowledgeDocumentVO[]>([]) // 列表的数据
|
||||||
|
const total = ref(0) // 列表的总页数
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
name: undefined,
|
||||||
|
status: undefined,
|
||||||
|
knowledgeId: undefined
|
||||||
|
})
|
||||||
|
const queryFormRef = ref() // 搜索的表单
|
||||||
|
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const data = await KnowledgeDocumentApi.getKnowledgeDocumentPage(queryParams)
|
||||||
|
list.value = data.list
|
||||||
|
total.value = data.total
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNo = 1
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value.resetFields()
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 跳转到创建文档页面 */
|
||||||
|
const handleCreate = () => {
|
||||||
|
router.push({
|
||||||
|
name: 'AiKnowledgeDocumentCreate',
|
||||||
|
query: { knowledgeId: queryParams.knowledgeId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 跳转到更新文档页面 */
|
||||||
|
const handleUpdate = (id: number) => {
|
||||||
|
router.push({
|
||||||
|
name: 'AiKnowledgeDocumentUpdate',
|
||||||
|
query: { id, knowledgeId: queryParams.knowledgeId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除按钮操作 */
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
try {
|
||||||
|
// 删除的二次确认
|
||||||
|
await message.delConfirm()
|
||||||
|
// 发起删除
|
||||||
|
await KnowledgeDocumentApi.deleteKnowledgeDocument(id)
|
||||||
|
message.success(t('common.delSuccess'))
|
||||||
|
// 刷新列表
|
||||||
|
await getList()
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改状态操作 */
|
||||||
|
const handleStatusChange = async (row: KnowledgeDocumentVO) => {
|
||||||
|
try {
|
||||||
|
// 修改状态的二次确认
|
||||||
|
const text = row.status === CommonStatusEnum.ENABLE ? '启用' : '禁用'
|
||||||
|
await message.confirm('确认要"' + text + '""' + row.name + '"文档吗?')
|
||||||
|
// 发起修改状态
|
||||||
|
await KnowledgeDocumentApi.updateKnowledgeDocumentStatus({ id: row.id, status: row.status })
|
||||||
|
message.success(t('common.updateSuccess'))
|
||||||
|
// 刷新列表
|
||||||
|
await getList()
|
||||||
|
} catch {
|
||||||
|
// 取消后,进行恢复按钮
|
||||||
|
row.status =
|
||||||
|
row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 跳转到知识库分段页面 */
|
||||||
|
const handleSegment = (id: number) => {
|
||||||
|
router.push({
|
||||||
|
name: 'AiKnowledgeSegment',
|
||||||
|
query: { documentId: id }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化 **/
|
||||||
|
onMounted(() => {
|
||||||
|
// 如果知识库 ID 不存在,显示错误提示并关闭页面
|
||||||
|
if (!route.query.knowledgeId) {
|
||||||
|
message.error('知识库 ID 不存在,无法查看文档列表')
|
||||||
|
// 关闭当前路由,返回到知识库列表页面
|
||||||
|
router.push({ name: 'AiKnowledge' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从路由参数中获取知识库 ID
|
||||||
|
queryParams.knowledgeId = route.query.knowledgeId as any
|
||||||
|
getList()
|
||||||
|
})
|
||||||
|
</script>
|
162
src/views/ai/knowledge/knowledge/KnowledgeForm.vue
Normal file
162
src/views/ai/knowledge/knowledge/KnowledgeForm.vue
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="formRules"
|
||||||
|
label-width="130px"
|
||||||
|
v-loading="formLoading"
|
||||||
|
>
|
||||||
|
<el-form-item label="知识库名称" prop="name">
|
||||||
|
<el-input v-model="formData.name" placeholder="请输入知识库名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="知识库描述" prop="description">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入知识库描述"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="向量模型" prop="embeddingModelId">
|
||||||
|
<el-select
|
||||||
|
v-model="formData.embeddingModelId"
|
||||||
|
placeholder="请选择向量模型"
|
||||||
|
clearable
|
||||||
|
class="!w-full"
|
||||||
|
>
|
||||||
|
<el-option v-for="item in modelList" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="检索 topK" prop="topK">
|
||||||
|
<el-input-number
|
||||||
|
v-model="formData.topK"
|
||||||
|
placeholder="请输入检索 topK"
|
||||||
|
:min="0"
|
||||||
|
:max="10"
|
||||||
|
class="!w-1/1"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="检索相似度阈值" prop="similarityThreshold">
|
||||||
|
<el-input-number
|
||||||
|
v-model="formData.similarityThreshold"
|
||||||
|
placeholder="请输入检索相似度阈值"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
:precision="2"
|
||||||
|
class="!w-1/1"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="是否启用" prop="status">
|
||||||
|
<el-radio-group v-model="formData.status">
|
||||||
|
<el-radio
|
||||||
|
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||||
|
:key="dict.value"
|
||||||
|
:label="dict.value"
|
||||||
|
>
|
||||||
|
{{ dict.label }}
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||||
|
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||||
|
import { KnowledgeApi, KnowledgeVO } from '@/api/ai/knowledge/knowledge'
|
||||||
|
import { CommonStatusEnum } from '@/utils/constants'
|
||||||
|
import { ModelApi, ModelVO } from '@/api/ai/model/model'
|
||||||
|
import { AiModelTypeEnum } from '../../utils/constants'
|
||||||
|
|
||||||
|
/** AI 知识库表单 */
|
||||||
|
defineOptions({ name: 'KnowledgeForm' })
|
||||||
|
|
||||||
|
const { t } = useI18n() // 国际化
|
||||||
|
const message = useMessage() // 消息弹窗
|
||||||
|
|
||||||
|
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||||
|
const dialogTitle = ref('') // 弹窗的标题
|
||||||
|
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||||
|
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
||||||
|
const formData = ref({
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
description: undefined,
|
||||||
|
embeddingModelId: undefined,
|
||||||
|
topK: undefined,
|
||||||
|
similarityThreshold: undefined,
|
||||||
|
status: CommonStatusEnum.ENABLE // 默认开启
|
||||||
|
})
|
||||||
|
const formRules = reactive({
|
||||||
|
name: [{ required: true, message: '请输入知识库名称', trigger: 'blur' }],
|
||||||
|
embeddingModelId: [{ required: true, message: '请输入向量模型', trigger: 'blur' }],
|
||||||
|
topK: [{ required: true, message: '请输入检索 topK', trigger: 'blur' }],
|
||||||
|
similarityThreshold: [{ required: true, message: '请输入检索相似度阈值', trigger: 'blur' }],
|
||||||
|
status: [{ required: true, message: '请选择是否启用', trigger: 'blur' }]
|
||||||
|
})
|
||||||
|
const formRef = ref() // 表单 Ref
|
||||||
|
const modelList = ref<ModelVO[]>([]) // 向量模型选项
|
||||||
|
|
||||||
|
/** 打开弹窗 */
|
||||||
|
const open = async (type: string, id?: number) => {
|
||||||
|
dialogVisible.value = true
|
||||||
|
dialogTitle.value = t('action.' + type)
|
||||||
|
formType.value = type
|
||||||
|
resetForm()
|
||||||
|
// 获取向量模型列表
|
||||||
|
modelList.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.EMBEDDING)
|
||||||
|
// 修改时,设置数据
|
||||||
|
if (id) {
|
||||||
|
formLoading.value = true
|
||||||
|
try {
|
||||||
|
formData.value = await KnowledgeApi.getKnowledge(id)
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||||
|
|
||||||
|
/** 提交表单 */
|
||||||
|
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||||
|
const submitForm = async () => {
|
||||||
|
// 校验表单
|
||||||
|
await formRef.value.validate()
|
||||||
|
// 提交请求
|
||||||
|
formLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = formData.value as unknown as KnowledgeVO
|
||||||
|
if (formType.value === 'create') {
|
||||||
|
await KnowledgeApi.createKnowledge(data)
|
||||||
|
message.success(t('common.createSuccess'))
|
||||||
|
} else {
|
||||||
|
await KnowledgeApi.updateKnowledge(data)
|
||||||
|
message.success(t('common.updateSuccess'))
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
// 发送操作成功的事件
|
||||||
|
emit('success')
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置表单 */
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
description: undefined,
|
||||||
|
embeddingModelId: undefined,
|
||||||
|
topK: undefined,
|
||||||
|
similarityThreshold: undefined,
|
||||||
|
status: CommonStatusEnum.ENABLE // 默认开启
|
||||||
|
}
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
}
|
||||||
|
</script>
|
221
src/views/ai/knowledge/knowledge/index.vue
Normal file
221
src/views/ai/knowledge/knowledge/index.vue
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
<template>
|
||||||
|
<doc-alert title="AI 知识库" url="https://doc.iocoder.cn/ai/knowledge/" />
|
||||||
|
|
||||||
|
<ContentWrap>
|
||||||
|
<!-- 搜索工作栏 -->
|
||||||
|
<el-form
|
||||||
|
class="-mb-15px"
|
||||||
|
:model="queryParams"
|
||||||
|
ref="queryFormRef"
|
||||||
|
:inline="true"
|
||||||
|
label-width="95px"
|
||||||
|
>
|
||||||
|
<el-form-item label="知识库名称" prop="name">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.name"
|
||||||
|
placeholder="请输入知识库名称"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
class="!w-240px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="是否启用" prop="status">
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.status"
|
||||||
|
placeholder="请选择是否启用"
|
||||||
|
clearable
|
||||||
|
class="!w-240px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||||
|
:key="dict.value"
|
||||||
|
:label="dict.label"
|
||||||
|
:value="dict.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="创建时间" prop="createTime">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="queryParams.createTime"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
type="daterange"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||||
|
class="!w-220px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||||
|
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
@click="openForm('create')"
|
||||||
|
v-hasPermi="['ai:knowledge:create']"
|
||||||
|
>
|
||||||
|
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</ContentWrap>
|
||||||
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<ContentWrap>
|
||||||
|
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||||
|
<el-table-column label="编号" align="center" prop="id" />
|
||||||
|
<el-table-column label="知识库名称" align="center" prop="name" />
|
||||||
|
<el-table-column label="知识库描述" align="center" prop="description" />
|
||||||
|
<el-table-column label="向量化模型" align="center" prop="embeddingModel" />
|
||||||
|
<el-table-column label="是否启用" align="center" prop="status">
|
||||||
|
<template #default="scope">
|
||||||
|
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
label="创建时间"
|
||||||
|
align="center"
|
||||||
|
prop="createTime"
|
||||||
|
:formatter="dateFormatter"
|
||||||
|
width="180px"
|
||||||
|
/>
|
||||||
|
<el-table-column label="操作" align="center" min-width="120px">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
@click="openForm('update', scope.row.id)"
|
||||||
|
v-hasPermi="['ai:knowledge:update']"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
@click="handleDocument(scope.row.id)"
|
||||||
|
v-hasPermi="['ai:knowledge:query']"
|
||||||
|
>
|
||||||
|
文档
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
@click="handleRetrieval(scope.row.id)"
|
||||||
|
v-hasPermi="['ai:knowledge:query']"
|
||||||
|
>
|
||||||
|
召回测试
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
@click="handleDelete(scope.row.id)"
|
||||||
|
v-hasPermi="['ai:knowledge:delete']"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<!-- 分页 -->
|
||||||
|
<Pagination
|
||||||
|
:total="total"
|
||||||
|
v-model:page="queryParams.pageNo"
|
||||||
|
v-model:limit="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</ContentWrap>
|
||||||
|
|
||||||
|
<!-- 表单弹窗:添加/修改 -->
|
||||||
|
<KnowledgeForm ref="formRef" @success="getList" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||||
|
import { dateFormatter } from '@/utils/formatTime'
|
||||||
|
import { KnowledgeApi, KnowledgeVO } from '@/api/ai/knowledge/knowledge'
|
||||||
|
import KnowledgeForm from './KnowledgeForm.vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
/** AI 知识库列表 */
|
||||||
|
defineOptions({ name: 'Knowledge' })
|
||||||
|
|
||||||
|
const message = useMessage() // 消息弹窗
|
||||||
|
const { t } = useI18n() // 国际化
|
||||||
|
|
||||||
|
const loading = ref(true) // 列表的加载中
|
||||||
|
const list = ref<KnowledgeVO[]>([]) // 列表的数据
|
||||||
|
const total = ref(0) // 列表的总页数
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
name: undefined,
|
||||||
|
status: undefined,
|
||||||
|
createTime: []
|
||||||
|
})
|
||||||
|
const queryFormRef = ref() // 搜索的表单
|
||||||
|
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const data = await KnowledgeApi.getKnowledgePage(queryParams)
|
||||||
|
list.value = data.list
|
||||||
|
total.value = data.total
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNo = 1
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value.resetFields()
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加/修改操作 */
|
||||||
|
const formRef = ref()
|
||||||
|
const openForm = (type: string, id?: number) => {
|
||||||
|
formRef.value.open(type, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除按钮操作 */
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
try {
|
||||||
|
// 删除的二次确认
|
||||||
|
await message.delConfirm()
|
||||||
|
// 发起删除
|
||||||
|
await KnowledgeApi.deleteKnowledge(id)
|
||||||
|
message.success(t('common.delSuccess'))
|
||||||
|
// 刷新列表
|
||||||
|
await getList()
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 文档按钮操作 */
|
||||||
|
const router = useRouter()
|
||||||
|
const handleDocument = (id: number) => {
|
||||||
|
router.push({
|
||||||
|
name: 'AiKnowledgeDocument',
|
||||||
|
query: { knowledgeId: id }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 跳转到文档召回测试页面 */
|
||||||
|
const handleRetrieval = (id: number) => {
|
||||||
|
router.push({
|
||||||
|
name: 'AiKnowledgeRetrieval',
|
||||||
|
query: { id }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化 **/
|
||||||
|
onMounted(() => {
|
||||||
|
getList()
|
||||||
|
})
|
||||||
|
</script>
|
163
src/views/ai/knowledge/knowledge/retrieval/index.vue
Normal file
163
src/views/ai/knowledge/knowledge/retrieval/index.vue
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex gap-20px w-full">
|
||||||
|
<!-- 左侧输入区域 -->
|
||||||
|
<ContentWrap class="flex-1 min-w-300px">
|
||||||
|
<div class="mb-15px">
|
||||||
|
<h3 class="m-0 mb-5px">召回测试</h3>
|
||||||
|
<div class="text-gray-500 text-14px">根据给定的查询文本测试召回效果。</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="relative mb-10px">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.content"
|
||||||
|
type="textarea"
|
||||||
|
:rows="8"
|
||||||
|
placeholder="请输入文本"
|
||||||
|
/>
|
||||||
|
<div class="absolute bottom-10px right-10px text-gray-400 text-12px">
|
||||||
|
{{ queryParams.content?.length }} / 200
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center mb-10px">
|
||||||
|
<span class="w-60px text-gray-500">topK:</span>
|
||||||
|
<el-input-number v-model="queryParams.topK" :min="1" :max="20" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center mb-15px">
|
||||||
|
<span class="w-60px text-gray-500">相似度:</span>
|
||||||
|
<el-input-number
|
||||||
|
v-model="queryParams.similarityThreshold"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:precision="2"
|
||||||
|
:step="0.01"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<el-button type="primary" @click="getRetrievalResult" :loading="loading">测试</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ContentWrap>
|
||||||
|
|
||||||
|
<!-- 右侧召回结果区域 -->
|
||||||
|
<ContentWrap class="flex-1 min-w-300px">
|
||||||
|
<el-empty v-if="loading" description="正在检索中..." />
|
||||||
|
<div v-else-if="segments.length > 0" class="font-bold mb-15px">
|
||||||
|
{{ segments.length }} 个召回段落
|
||||||
|
</div>
|
||||||
|
<el-empty v-else description="暂无召回结果" />
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-for="(segment, index) in segments"
|
||||||
|
:key="index"
|
||||||
|
class="mb-20px border border-solid border-gray-200 rounded p-15px"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between text-12px text-gray-500 mb-5px">
|
||||||
|
<span>
|
||||||
|
分段({{ segment.id }}) · {{ segment.contentLength }} 字符数 ·
|
||||||
|
{{ segment.tokens }} Token
|
||||||
|
</span>
|
||||||
|
<span class="px-8px py-4px bg-blue-50 text-blue-500 rounded-full text-12px font-bold">
|
||||||
|
score: {{ segment.score }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="bg-gray-50 p-10px rounded mb-10px whitespace-pre-wrap overflow-hidden transition-all duration-100 text-13px"
|
||||||
|
:class="{
|
||||||
|
'line-clamp-2 max-h-50px': !segment.expanded,
|
||||||
|
'max-h-500px': segment.expanded
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ segment.content }}
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div class="flex items-center text-gray-500 text-13px">
|
||||||
|
<Icon icon="ep:document" class="mr-5px" />
|
||||||
|
<span>{{ segment.documentName || '未知文档' }}</span>
|
||||||
|
</div>
|
||||||
|
<el-button size="small" @click="toggleExpand(segment)">
|
||||||
|
{{ segment.expanded ? '收起' : '展开' }}
|
||||||
|
<Icon :icon="segment.expanded ? 'ep:arrow-up' : 'ep:arrow-down'" />
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ContentWrap>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useMessage } from '@/hooks/web/useMessage'
|
||||||
|
import { KnowledgeSegmentApi } from '@/api/ai/knowledge/segment'
|
||||||
|
import { KnowledgeApi } from '@/api/ai/knowledge/knowledge'
|
||||||
|
/** 文档召回测试 */
|
||||||
|
defineOptions({ name: 'KnowledgeDocumentRetrieval' })
|
||||||
|
|
||||||
|
const message = useMessage() // 消息弹窗
|
||||||
|
const route = useRoute() // 路由
|
||||||
|
const router = useRouter() // 路由
|
||||||
|
|
||||||
|
const loading = ref(false) // 加载状态
|
||||||
|
const segments = ref<any[]>([]) // 召回结果
|
||||||
|
const queryParams = reactive({
|
||||||
|
id: undefined,
|
||||||
|
content: '',
|
||||||
|
topK: 10,
|
||||||
|
similarityThreshold: 0.5
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 调用文档召回测试接口 */
|
||||||
|
const getRetrievalResult = async () => {
|
||||||
|
if (!queryParams.content) {
|
||||||
|
message.warning('请输入查询文本')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
segments.value = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await KnowledgeSegmentApi.searchKnowledgeSegment({
|
||||||
|
knowledgeId: queryParams.id,
|
||||||
|
content: queryParams.content,
|
||||||
|
topK: queryParams.topK,
|
||||||
|
similarityThreshold: queryParams.similarityThreshold
|
||||||
|
})
|
||||||
|
segments.value = data || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 展开/收起段落内容 */
|
||||||
|
const toggleExpand = (segment: any) => {
|
||||||
|
segment.expanded = !segment.expanded
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取知识库信息 */
|
||||||
|
const getKnowledgeInfo = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const knowledge = await KnowledgeApi.getKnowledge(id)
|
||||||
|
if (knowledge) {
|
||||||
|
queryParams.topK = knowledge.topK || queryParams.topK
|
||||||
|
queryParams.similarityThreshold =
|
||||||
|
knowledge.similarityThreshold || queryParams.similarityThreshold
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化 **/
|
||||||
|
onMounted(() => {
|
||||||
|
// 如果知识库 ID 不存在,显示错误提示并关闭页面
|
||||||
|
if (!route.query.id) {
|
||||||
|
message.error('知识库 ID 不存在,无法进行召回测试')
|
||||||
|
router.back()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
queryParams.id = route.query.id as any
|
||||||
|
|
||||||
|
// 获取知识库信息并设置默认值
|
||||||
|
getKnowledgeInfo(queryParams.id as any)
|
||||||
|
})
|
||||||
|
</script>
|
101
src/views/ai/knowledge/segment/KnowledgeSegmentForm.vue
Normal file
101
src/views/ai/knowledge/segment/KnowledgeSegmentForm.vue
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="formRules"
|
||||||
|
label-width="100px"
|
||||||
|
v-loading="formLoading"
|
||||||
|
>
|
||||||
|
<el-form-item label="切片内容" prop="content">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.content"
|
||||||
|
type="textarea"
|
||||||
|
:rows="6"
|
||||||
|
placeholder="请输入切片内容"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||||
|
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { KnowledgeSegmentApi, KnowledgeSegmentVO } from '@/api/ai/knowledge/segment'
|
||||||
|
|
||||||
|
/** AI 知识库分段表单 */
|
||||||
|
defineOptions({ name: 'KnowledgeSegmentForm' })
|
||||||
|
|
||||||
|
const { t } = useI18n() // 国际化
|
||||||
|
const message = useMessage() // 消息弹窗
|
||||||
|
|
||||||
|
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||||
|
const dialogTitle = ref('') // 弹窗的标题
|
||||||
|
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||||
|
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
||||||
|
const formData = ref({
|
||||||
|
id: undefined,
|
||||||
|
documentId: undefined,
|
||||||
|
content: undefined
|
||||||
|
})
|
||||||
|
const formRules = reactive({
|
||||||
|
content: [{ required: true, message: '切片内容不能为空', trigger: 'blur' }]
|
||||||
|
})
|
||||||
|
const formRef = ref() // 表单 Ref
|
||||||
|
|
||||||
|
/** 打开弹窗 */
|
||||||
|
const open = async (type: string, id?: number, documentId?: any) => {
|
||||||
|
dialogVisible.value = true
|
||||||
|
dialogTitle.value = t('action.' + type)
|
||||||
|
formType.value = type
|
||||||
|
resetForm()
|
||||||
|
formData.value.documentId = documentId as any
|
||||||
|
|
||||||
|
// 修改时,设置数据
|
||||||
|
if (id) {
|
||||||
|
formLoading.value = true
|
||||||
|
try {
|
||||||
|
formData.value = await KnowledgeSegmentApi.getKnowledgeSegment(id)
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||||
|
|
||||||
|
/** 提交表单 */
|
||||||
|
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||||
|
const submitForm = async () => {
|
||||||
|
// 校验表单
|
||||||
|
await formRef.value.validate()
|
||||||
|
// 提交请求
|
||||||
|
formLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = formData.value as unknown as KnowledgeSegmentVO
|
||||||
|
if (formType.value === 'create') {
|
||||||
|
await KnowledgeSegmentApi.createKnowledgeSegment(data)
|
||||||
|
message.success(t('common.createSuccess'))
|
||||||
|
} else {
|
||||||
|
await KnowledgeSegmentApi.updateKnowledgeSegment(data)
|
||||||
|
message.success(t('common.updateSuccess'))
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
// 发送操作成功的事件
|
||||||
|
emit('success')
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置表单 */
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
id: undefined,
|
||||||
|
documentId: undefined,
|
||||||
|
content: undefined
|
||||||
|
}
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
}
|
||||||
|
</script>
|
242
src/views/ai/knowledge/segment/index.vue
Normal file
242
src/views/ai/knowledge/segment/index.vue
Normal file
@ -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
|
<el-input
|
||||||
v-model="formData.prompt"
|
v-model="formData.prompt"
|
||||||
maxlength="1024"
|
maxlength="1024"
|
||||||
rows="5"
|
:rows="5"
|
||||||
class="w-100% mt-15px"
|
class="w-100% mt-15px"
|
||||||
input-style="border-radius: 7px;"
|
input-style="border-radius: 7px;"
|
||||||
placeholder="请输入提示词,让AI帮你完善"
|
placeholder="请输入提示词,让AI帮你完善"
|
||||||
@ -29,7 +29,7 @@
|
|||||||
<el-input
|
<el-input
|
||||||
v-model="generatedContent"
|
v-model="generatedContent"
|
||||||
maxlength="1024"
|
maxlength="1024"
|
||||||
rows="5"
|
:rows="5"
|
||||||
class="w-100% mt-15px"
|
class="w-100% mt-15px"
|
||||||
input-style="border-radius: 7px;"
|
input-style="border-radius: 7px;"
|
||||||
placeholder="例如:童话里的小屋应该是什么样子?"
|
placeholder="例如:童话里的小屋应该是什么样子?"
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<doc-alert title="AI 思维导图" url="https://doc.iocoder.cn/ai/mindmap/" />
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<doc-alert title="AI 手册" url="https://doc.iocoder.cn/ai/build/" />
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
@ -16,10 +16,10 @@
|
|||||||
<el-form-item label="绑定模型" prop="modelId" v-if="!isUser">
|
<el-form-item label="绑定模型" prop="modelId" v-if="!isUser">
|
||||||
<el-select v-model="formData.modelId" placeholder="请选择模型" clearable>
|
<el-select v-model="formData.modelId" placeholder="请选择模型" clearable>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="chatModel in chatModelList"
|
v-for="model in models"
|
||||||
:key="chatModel.id"
|
:key="model.id"
|
||||||
:label="chatModel.name"
|
:label="model.name"
|
||||||
:value="chatModel.id"
|
:value="model.id"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -32,6 +32,21 @@
|
|||||||
<el-form-item label="角色设定" prop="systemMessage">
|
<el-form-item label="角色设定" prop="systemMessage">
|
||||||
<el-input type="textarea" v-model="formData.systemMessage" placeholder="请输入角色设定" />
|
<el-input type="textarea" v-model="formData.systemMessage" placeholder="请输入角色设定" />
|
||||||
</el-form-item>
|
</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-form-item label="是否公开" prop="publicStatus" v-if="!isUser">
|
||||||
<el-radio-group v-model="formData.publicStatus">
|
<el-radio-group v-model="formData.publicStatus">
|
||||||
<el-radio
|
<el-radio
|
||||||
@ -68,8 +83,11 @@
|
|||||||
import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
|
import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||||
import { ChatRoleApi, ChatRoleVO } from '@/api/ai/model/chatRole'
|
import { ChatRoleApi, ChatRoleVO } from '@/api/ai/model/chatRole'
|
||||||
import { CommonStatusEnum } from '@/utils/constants'
|
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 { 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 聊天角色 表单 */
|
/** AI 聊天角色 表单 */
|
||||||
defineOptions({ name: 'ChatRoleForm' })
|
defineOptions({ name: 'ChatRoleForm' })
|
||||||
@ -91,10 +109,14 @@ const formData = ref({
|
|||||||
description: undefined,
|
description: undefined,
|
||||||
systemMessage: undefined,
|
systemMessage: undefined,
|
||||||
publicStatus: true,
|
publicStatus: true,
|
||||||
status: CommonStatusEnum.ENABLE
|
status: CommonStatusEnum.ENABLE,
|
||||||
|
knowledgeIds: [] as number[],
|
||||||
|
toolIds: [] as number[]
|
||||||
})
|
})
|
||||||
const formRef = ref() // 表单 Ref
|
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(() => {
|
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 方法,用于打开弹窗
|
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||||
|
|
||||||
@ -176,7 +202,9 @@ const resetForm = () => {
|
|||||||
description: undefined,
|
description: undefined,
|
||||||
systemMessage: undefined,
|
systemMessage: undefined,
|
||||||
publicStatus: true,
|
publicStatus: true,
|
||||||
status: CommonStatusEnum.ENABLE
|
status: CommonStatusEnum.ENABLE,
|
||||||
|
knowledgeIds: [],
|
||||||
|
toolIds: []
|
||||||
}
|
}
|
||||||
formRef.value?.resetFields()
|
formRef.value?.resetFields()
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<doc-alert title="AI 对话聊天" url="https://doc.iocoder.cn/ai/chat/" />
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
@ -69,6 +71,18 @@
|
|||||||
<el-table-column label="角色类别" align="center" prop="category" />
|
<el-table-column label="角色类别" align="center" prop="category" />
|
||||||
<el-table-column label="角色描述" align="center" prop="description" />
|
<el-table-column label="角色描述" align="center" prop="description" />
|
||||||
<el-table-column label="角色设定" align="center" prop="systemMessage" />
|
<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">
|
<el-table-column label="是否公开" align="center" prop="publicStatus">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.publicStatus" />
|
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.publicStatus" />
|
||||||
|
@ -17,6 +17,21 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</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-form-item label="API 秘钥" prop="keyId">
|
||||||
<el-select v-model="formData.keyId" placeholder="请选择 API 秘钥" clearable>
|
<el-select v-model="formData.keyId" placeholder="请选择 API 秘钥" clearable>
|
||||||
<el-option
|
<el-option
|
||||||
@ -47,29 +62,44 @@
|
|||||||
</el-radio>
|
</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="温度参数" prop="temperature">
|
<el-form-item
|
||||||
|
label="温度参数"
|
||||||
|
prop="temperature"
|
||||||
|
v-if="formData.type === AiModelTypeEnum.CHAT"
|
||||||
|
>
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="formData.temperature"
|
v-model="formData.temperature"
|
||||||
placeholder="请输入温度参数"
|
placeholder="请输入温度参数"
|
||||||
:min="0"
|
:min="0"
|
||||||
:max="2"
|
:max="2"
|
||||||
:precision="2"
|
:precision="2"
|
||||||
|
class="!w-1/1"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</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
|
<el-input-number
|
||||||
v-model="formData.maxTokens"
|
v-model="formData.maxTokens"
|
||||||
placeholder="请输入回复数 Token 数"
|
placeholder="请输入回复数 Token 数"
|
||||||
:min="0"
|
:min="0"
|
||||||
:max="4096"
|
:max="8192"
|
||||||
|
class="!w-1/1"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="上下文数量" prop="maxContexts">
|
<el-form-item
|
||||||
|
label="上下文数量"
|
||||||
|
prop="maxContexts"
|
||||||
|
v-if="formData.type === AiModelTypeEnum.CHAT"
|
||||||
|
>
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="formData.maxContexts"
|
v-model="formData.maxContexts"
|
||||||
placeholder="请输入上下文数量"
|
placeholder="请输入上下文数量"
|
||||||
:min="0"
|
:min="0"
|
||||||
:max="20"
|
:max="20"
|
||||||
|
class="!w-1/1"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@ -80,13 +110,14 @@
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<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 { ApiKeyApi, ApiKeyVO } from '@/api/ai/model/apiKey'
|
||||||
import { CommonStatusEnum } from '@/utils/constants'
|
import { CommonStatusEnum } from '@/utils/constants'
|
||||||
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
|
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
|
||||||
|
import { AiModelTypeEnum } from '@/views/ai/utils/constants'
|
||||||
|
|
||||||
/** API 聊天模型 表单 */
|
/** API 模型的表单 */
|
||||||
defineOptions({ name: 'ChatModelForm' })
|
defineOptions({ name: 'ModelForm' })
|
||||||
|
|
||||||
const { t } = useI18n() // 国际化
|
const { t } = useI18n() // 国际化
|
||||||
const message = useMessage() // 消息弹窗
|
const message = useMessage() // 消息弹窗
|
||||||
@ -101,6 +132,7 @@ const formData = ref({
|
|||||||
name: undefined,
|
name: undefined,
|
||||||
model: undefined,
|
model: undefined,
|
||||||
platform: undefined,
|
platform: undefined,
|
||||||
|
type: undefined,
|
||||||
sort: undefined,
|
sort: undefined,
|
||||||
status: CommonStatusEnum.ENABLE,
|
status: CommonStatusEnum.ENABLE,
|
||||||
temperature: undefined,
|
temperature: undefined,
|
||||||
@ -112,6 +144,7 @@ const formRules = reactive({
|
|||||||
name: [{ required: true, message: '模型名字不能为空', trigger: 'blur' }],
|
name: [{ required: true, message: '模型名字不能为空', trigger: 'blur' }],
|
||||||
model: [{ required: true, message: '模型标识不能为空', trigger: 'blur' }],
|
model: [{ required: true, message: '模型标识不能为空', trigger: 'blur' }],
|
||||||
platform: [{ required: true, message: '所属平台不能为空', trigger: 'blur' }],
|
platform: [{ required: true, message: '所属平台不能为空', trigger: 'blur' }],
|
||||||
|
type: [{ required: true, message: '模型类型不能为空', trigger: 'blur' }],
|
||||||
sort: [{ required: true, message: '排序不能为空', trigger: 'blur' }],
|
sort: [{ required: true, message: '排序不能为空', trigger: 'blur' }],
|
||||||
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
|
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
|
||||||
})
|
})
|
||||||
@ -128,13 +161,13 @@ const open = async (type: string, id?: number) => {
|
|||||||
if (id) {
|
if (id) {
|
||||||
formLoading.value = true
|
formLoading.value = true
|
||||||
try {
|
try {
|
||||||
formData.value = await ChatModelApi.getChatModel(id)
|
formData.value = await ModelApi.getModel(id)
|
||||||
} finally {
|
} finally {
|
||||||
formLoading.value = false
|
formLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 获得下拉数据
|
// 获得下拉数据
|
||||||
apiKeyList.value = await ApiKeyApi.getApiKeySimpleList(CommonStatusEnum.ENABLE)
|
apiKeyList.value = await ApiKeyApi.getApiKeySimpleList()
|
||||||
}
|
}
|
||||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||||
|
|
||||||
@ -146,12 +179,17 @@ const submitForm = async () => {
|
|||||||
// 提交请求
|
// 提交请求
|
||||||
formLoading.value = true
|
formLoading.value = true
|
||||||
try {
|
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') {
|
if (formType.value === 'create') {
|
||||||
await ChatModelApi.createChatModel(data)
|
await ModelApi.createModel(data)
|
||||||
message.success(t('common.createSuccess'))
|
message.success(t('common.createSuccess'))
|
||||||
} else {
|
} else {
|
||||||
await ChatModelApi.updateChatModel(data)
|
await ModelApi.updateModel(data)
|
||||||
message.success(t('common.updateSuccess'))
|
message.success(t('common.updateSuccess'))
|
||||||
}
|
}
|
||||||
dialogVisible.value = false
|
dialogVisible.value = false
|
||||||
@ -170,6 +208,7 @@ const resetForm = () => {
|
|||||||
name: undefined,
|
name: undefined,
|
||||||
model: undefined,
|
model: undefined,
|
||||||
platform: undefined,
|
platform: undefined,
|
||||||
|
type: undefined,
|
||||||
sort: undefined,
|
sort: undefined,
|
||||||
status: CommonStatusEnum.ENABLE,
|
status: CommonStatusEnum.ENABLE,
|
||||||
temperature: undefined,
|
temperature: undefined,
|
@ -1,4 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<doc-alert title="AI 手册" url="https://doc.iocoder.cn/ai/build/" />
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
@ -42,7 +44,7 @@
|
|||||||
type="primary"
|
type="primary"
|
||||||
plain
|
plain
|
||||||
@click="openForm('create')"
|
@click="openForm('create')"
|
||||||
v-hasPermi="['ai:chat-model:create']"
|
v-hasPermi="['ai:model:create']"
|
||||||
>
|
>
|
||||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||||
</el-button>
|
</el-button>
|
||||||
@ -53,34 +55,39 @@
|
|||||||
<!-- 列表 -->
|
<!-- 列表 -->
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
<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">
|
<template #default="scope">
|
||||||
<dict-tag :type="DICT_TYPE.AI_PLATFORM" :value="scope.row.platform" />
|
<dict-tag :type="DICT_TYPE.AI_PLATFORM" :value="scope.row.platform" />
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="模型名字" align="center" prop="name" min-width="120" />
|
<el-table-column label="模型类型" align="center" prop="platform" min-width="100">
|
||||||
<el-table-column label="模型标识" align="center" prop="model" min-width="120" />
|
<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">
|
<el-table-column label="API 秘钥" align="center" prop="keyId" min-width="140">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span>{{ apiKeyList.find((item) => item.id === scope.row.keyId)?.name }}</span>
|
<span>{{ apiKeyList.find((item) => item.id === scope.row.keyId)?.name }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="排序" align="center" prop="sort" />
|
<el-table-column label="排序" align="center" prop="sort" min-width="80" />
|
||||||
<el-table-column label="状态" align="center" prop="status">
|
<el-table-column label="状态" align="center" prop="status" min-width="80">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
|
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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="回复数 Token 数" align="center" prop="maxTokens" min-width="140" />
|
||||||
<el-table-column label="上下文数量" align="center" prop="maxContexts" />
|
<el-table-column label="上下文数量" align="center" prop="maxContexts" min-width="100" />
|
||||||
<el-table-column label="操作" align="center">
|
<el-table-column label="操作" align="center" width="180" fixed="right">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
<el-button
|
||||||
link
|
link
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="openForm('update', scope.row.id)"
|
@click="openForm('update', scope.row.id)"
|
||||||
v-hasPermi="['ai:chat-model:update']"
|
v-hasPermi="['ai:model:update']"
|
||||||
>
|
>
|
||||||
编辑
|
编辑
|
||||||
</el-button>
|
</el-button>
|
||||||
@ -88,7 +95,7 @@
|
|||||||
link
|
link
|
||||||
type="danger"
|
type="danger"
|
||||||
@click="handleDelete(scope.row.id)"
|
@click="handleDelete(scope.row.id)"
|
||||||
v-hasPermi="['ai:chat-model:delete']"
|
v-hasPermi="['ai:model:delete']"
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</el-button>
|
||||||
@ -105,23 +112,23 @@
|
|||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
|
|
||||||
<!-- 表单弹窗:添加/修改 -->
|
<!-- 表单弹窗:添加/修改 -->
|
||||||
<ChatModelForm ref="formRef" @success="getList" />
|
<ModelForm ref="formRef" @success="getList" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChatModelApi, ChatModelVO } from '@/api/ai/model/chatModel'
|
import { ModelApi, ModelVO } from '@/api/ai/model/model'
|
||||||
import ChatModelForm from './ChatModelForm.vue'
|
import ModelForm from './ModelForm.vue'
|
||||||
import { DICT_TYPE } from '@/utils/dict'
|
import { DICT_TYPE } from '@/utils/dict'
|
||||||
import { ApiKeyApi, ApiKeyVO } from '@/api/ai/model/apiKey'
|
import { ApiKeyApi, ApiKeyVO } from '@/api/ai/model/apiKey'
|
||||||
|
|
||||||
/** API 聊天模型 列表 */
|
/** API 模型列表 */
|
||||||
defineOptions({ name: 'AiChatModel' })
|
defineOptions({ name: 'AiModel' })
|
||||||
|
|
||||||
const message = useMessage() // 消息弹窗
|
const message = useMessage() // 消息弹窗
|
||||||
const { t } = useI18n() // 国际化
|
const { t } = useI18n() // 国际化
|
||||||
|
|
||||||
const loading = ref(true) // 列表的加载中
|
const loading = ref(true) // 列表的加载中
|
||||||
const list = ref<ChatModelVO[]>([]) // 列表的数据
|
const list = ref<ModelVO[]>([]) // 列表的数据
|
||||||
const total = ref(0) // 列表的总页数
|
const total = ref(0) // 列表的总页数
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
pageNo: 1,
|
pageNo: 1,
|
||||||
@ -137,7 +144,7 @@ const apiKeyList = ref([] as ApiKeyVO[]) // API 密钥列表
|
|||||||
const getList = async () => {
|
const getList = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const data = await ChatModelApi.getChatModelPage(queryParams)
|
const data = await ModelApi.getModelPage(queryParams)
|
||||||
list.value = data.list
|
list.value = data.list
|
||||||
total.value = data.total
|
total.value = data.total
|
||||||
} finally {
|
} finally {
|
||||||
@ -169,7 +176,7 @@ const handleDelete = async (id: number) => {
|
|||||||
// 删除的二次确认
|
// 删除的二次确认
|
||||||
await message.delConfirm()
|
await message.delConfirm()
|
||||||
// 发起删除
|
// 发起删除
|
||||||
await ChatModelApi.deleteChatModel(id)
|
await ModelApi.deleteModel(id)
|
||||||
message.success(t('common.delSuccess'))
|
message.success(t('common.delSuccess'))
|
||||||
// 刷新列表
|
// 刷新列表
|
||||||
await getList()
|
await getList()
|
||||||
@ -178,7 +185,7 @@ const handleDelete = async (id: number) => {
|
|||||||
|
|
||||||
/** 初始化 **/
|
/** 初始化 **/
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
getList()
|
await getList()
|
||||||
// 获得下拉数据
|
// 获得下拉数据
|
||||||
apiKeyList.value = await ApiKeyApi.getApiKeySimpleList()
|
apiKeyList.value = await ApiKeyApi.getApiKeySimpleList()
|
||||||
})
|
})
|
112
src/views/ai/model/tool/ToolForm.vue
Normal file
112
src/views/ai/model/tool/ToolForm.vue
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="formRules"
|
||||||
|
label-width="100px"
|
||||||
|
v-loading="formLoading"
|
||||||
|
>
|
||||||
|
<el-form-item label="工具名称" prop="name">
|
||||||
|
<el-input v-model="formData.name" placeholder="请输入工具名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="工具描述" prop="description">
|
||||||
|
<el-input type="textarea" v-model="formData.description" placeholder="请输入工具描述" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-radio-group v-model="formData.status">
|
||||||
|
<el-radio
|
||||||
|
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||||
|
:key="dict.value"
|
||||||
|
:label="dict.value"
|
||||||
|
>
|
||||||
|
{{ dict.label }}
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||||
|
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||||
|
import { ToolApi, ToolVO } from '@/api/ai/model/tool'
|
||||||
|
import { CommonStatusEnum } from '@/utils/constants'
|
||||||
|
|
||||||
|
/** AI 工具表单 */
|
||||||
|
defineOptions({ name: 'ToolForm' })
|
||||||
|
|
||||||
|
const { t } = useI18n() // 国际化
|
||||||
|
const message = useMessage() // 消息弹窗
|
||||||
|
|
||||||
|
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||||
|
const dialogTitle = ref('') // 弹窗的标题
|
||||||
|
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||||
|
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
||||||
|
const formData = ref({
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
description: undefined,
|
||||||
|
status: CommonStatusEnum.ENABLE
|
||||||
|
})
|
||||||
|
const formRules = reactive({
|
||||||
|
name: [{ required: true, message: '工具名称不能为空', trigger: 'blur' }]
|
||||||
|
})
|
||||||
|
const formRef = ref() // 表单 Ref
|
||||||
|
|
||||||
|
/** 打开弹窗 */
|
||||||
|
const open = async (type: string, id?: number) => {
|
||||||
|
dialogVisible.value = true
|
||||||
|
dialogTitle.value = t('action.' + type)
|
||||||
|
formType.value = type
|
||||||
|
resetForm()
|
||||||
|
// 修改时,设置数据
|
||||||
|
if (id) {
|
||||||
|
formLoading.value = true
|
||||||
|
try {
|
||||||
|
formData.value = await ToolApi.getTool(id)
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||||
|
|
||||||
|
/** 提交表单 */
|
||||||
|
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||||
|
const submitForm = async () => {
|
||||||
|
// 校验表单
|
||||||
|
await formRef.value.validate()
|
||||||
|
// 提交请求
|
||||||
|
formLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = formData.value as unknown as ToolVO
|
||||||
|
if (formType.value === 'create') {
|
||||||
|
await ToolApi.createTool(data)
|
||||||
|
message.success(t('common.createSuccess'))
|
||||||
|
} else {
|
||||||
|
await ToolApi.updateTool(data)
|
||||||
|
message.success(t('common.updateSuccess'))
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
// 发送操作成功的事件
|
||||||
|
emit('success')
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置表单 */
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
description: undefined,
|
||||||
|
status: CommonStatusEnum.ENABLE
|
||||||
|
}
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
}
|
||||||
|
</script>
|
178
src/views/ai/model/tool/index.vue
Normal file
178
src/views/ai/model/tool/index.vue
Normal file
@ -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>
|
<template>
|
||||||
|
<doc-alert title="AI 音乐创作" url="https://doc.iocoder.cn/ai/music/" />
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
|
@ -23,6 +23,15 @@ export const AiPlatformEnum = {
|
|||||||
SUNO: 'Suno' // Suno AI
|
SUNO: 'Suno' // Suno AI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const AiModelTypeEnum = {
|
||||||
|
CHAT: 1, // 聊天
|
||||||
|
IMAGE: 2, // 图像
|
||||||
|
VOICE: 3, // 音频
|
||||||
|
VIDEO: 4, // 视频
|
||||||
|
EMBEDDING: 5, // 向量
|
||||||
|
RERANK: 6 // 重排
|
||||||
|
}
|
||||||
|
|
||||||
export const OtherPlatformEnum: ImageModelVO[] = [
|
export const OtherPlatformEnum: ImageModelVO[] = [
|
||||||
{
|
{
|
||||||
key: AiPlatformEnum.TONG_YI,
|
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[] = [
|
export const StableDiffusionClipGuidancePresets: ImageModelVO[] = [
|
||||||
{
|
{
|
||||||
key: 'NONE',
|
key: 'NONE',
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<doc-alert title="AI 写作助手" url="https://doc.iocoder.cn/ai/write/" />
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<el-form
|
<el-form
|
||||||
@ -39,7 +41,12 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="平台" prop="platform">
|
<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
|
<el-option
|
||||||
v-for="dict in getStrDictOptions(DICT_TYPE.AI_PLATFORM)"
|
v-for="dict in getStrDictOptions(DICT_TYPE.AI_PLATFORM)"
|
||||||
:key="dict.value"
|
:key="dict.value"
|
||||||
@ -62,24 +69,6 @@
|
|||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
<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 @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-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
@ -143,15 +132,6 @@
|
|||||||
<el-table-column label="错误信息" align="center" prop="errorMessage" />
|
<el-table-column label="错误信息" align="center" prop="errorMessage" />
|
||||||
<el-table-column label="操作" align="center">
|
<el-table-column label="操作" align="center">
|
||||||
<template #default="scope">
|
<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
|
<el-button
|
||||||
link
|
link
|
||||||
type="danger"
|
type="danger"
|
||||||
@ -225,15 +205,6 @@ const resetQuery = () => {
|
|||||||
handleQuery()
|
handleQuery()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 新增方法,跳转到写作页面 **/
|
|
||||||
const openForm = (type: string, id?: number) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'create':
|
|
||||||
router.push('/ai/write')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 删除按钮操作 */
|
/** 删除按钮操作 */
|
||||||
const handleDelete = async (id: number) => {
|
const handleDelete = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
|
@ -376,6 +376,16 @@ const handleStepClick = async (index: number) => {
|
|||||||
|
|
||||||
// 切换步骤
|
// 切换步骤
|
||||||
currentStep.value = index
|
currentStep.value = index
|
||||||
|
|
||||||
|
// 如果切换到流程设计步骤,等待组件渲染完成后刷新设计器
|
||||||
|
if (index === 2) {
|
||||||
|
await nextTick()
|
||||||
|
// 等待更长时间确保组件完全初始化
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||||
|
if (processDesignRef.value?.refresh) {
|
||||||
|
await processDesignRef.value.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('步骤切换失败:', error)
|
console.error('步骤切换失败:', error)
|
||||||
message.warning('请先完善当前步骤必填信息')
|
message.warning('请先完善当前步骤必填信息')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user