feat(ai): 添加 AI 对话聊天和 API 密钥管理功能
- 新增 AI 对话聊天管理页面,包括对话列表和消息列表 - 新增 API 密钥管理页面,包括密钥列表和表单 - 添加相关 API 接口和数据模型 - 集成表单和表格组件,实现基本的 CRUD 操作pull/145/head
							parent
							
								
									75c5669a97
								
							
						
					
					
						commit
						3ef362508a
					
				|  | @ -0,0 +1,75 @@ | |||
| import type { PageResult } from '@vben/request'; | ||||
| 
 | ||||
| import { requestClient } from '#/api/request'; | ||||
| 
 | ||||
| export namespace AiChatConversationApi { | ||||
|   export interface ChatConversationVO { | ||||
|     id: number; // ID 编号
 | ||||
|     userId: number; // 用户编号
 | ||||
|     title: string; // 对话标题
 | ||||
|     pinned: boolean; // 是否置顶
 | ||||
|     roleId: number; // 角色编号
 | ||||
|     modelId: number; // 模型编号
 | ||||
|     model: string; // 模型标志
 | ||||
|     temperature: number; // 温度参数
 | ||||
|     maxTokens: number; // 单条回复的最大 Token 数量
 | ||||
|     maxContexts: number; // 上下文的最大 Message 数量
 | ||||
|     createTime?: Date; // 创建时间
 | ||||
|     // 额外字段
 | ||||
|     systemMessage?: string; // 角色设定
 | ||||
|     modelName?: string; // 模型名字
 | ||||
|     roleAvatar?: string; // 角色头像
 | ||||
|     modelMaxTokens?: string; // 模型的单条回复的最大 Token 数量
 | ||||
|     modelMaxContexts?: string; // 模型的上下文的最大 Message 数量
 | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // 获得【我的】聊天对话
 | ||||
| export function getChatConversationMy(id: number) { | ||||
|   return requestClient.get< | ||||
|     PageResult<AiChatConversationApi.ChatConversationVO> | ||||
|   >(`/ai/chat/conversation/get-my?id=${id}`); | ||||
| } | ||||
| 
 | ||||
| // 新增【我的】聊天对话
 | ||||
| export function createChatConversationMy( | ||||
|   data: AiChatConversationApi.ChatConversationVO, | ||||
| ) { | ||||
|   return requestClient.post('/ai/chat/conversation/create-my', data); | ||||
| } | ||||
| 
 | ||||
| //  更新【我的】聊天对话
 | ||||
| export function updateChatConversationMy( | ||||
|   data: AiChatConversationApi.ChatConversationVO, | ||||
| ) { | ||||
|   return requestClient.put(`/ai/chat/conversation/update-my`, data); | ||||
| } | ||||
| 
 | ||||
| //  删除【我的】聊天对话
 | ||||
| export function deleteChatConversationMy(id: string) { | ||||
|   return requestClient.delete(`/ai/chat/conversation/delete-my?id=${id}`); | ||||
| } | ||||
| 
 | ||||
| //  删除【我的】所有对话,置顶除外
 | ||||
| export function deleteChatConversationMyByUnpinned() { | ||||
|   return requestClient.delete(`/ai/chat/conversation/delete-by-unpinned`); | ||||
| } | ||||
| 
 | ||||
| //  获得【我的】聊天对话列表
 | ||||
| export function getChatConversationMyList() { | ||||
|   return requestClient.get<AiChatConversationApi.ChatConversationVO[]>( | ||||
|     `/ai/chat/conversation/my-list`, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| //  获得【我的】聊天对话列表
 | ||||
| export function getChatConversationPage(params: any) { | ||||
|   return requestClient.get< | ||||
|     PageResult<AiChatConversationApi.ChatConversationVO[]> | ||||
|   >(`/ai/chat/conversation/page`, { params }); | ||||
| } | ||||
| 
 | ||||
| //  管理员删除消息
 | ||||
| export function deleteChatConversationByAdmin(id: number) { | ||||
|   return requestClient.delete(`/ai/chat/conversation/delete-by-admin?id=${id}`); | ||||
| } | ||||
|  | @ -0,0 +1,96 @@ | |||
| import type { PageResult } from '@vben/request'; | ||||
| 
 | ||||
| import { fetchEventSource } from '@vben/request'; | ||||
| import { useAccessStore } from '@vben/stores'; | ||||
| 
 | ||||
| import { requestClient } from '#/api/request'; | ||||
| 
 | ||||
| const accessStore = useAccessStore(); | ||||
| export namespace AiChatMessageApi { | ||||
|   export interface ChatMessageVO { | ||||
|     id: number; // 编号
 | ||||
|     conversationId: number; // 对话编号
 | ||||
|     type: string; // 消息类型
 | ||||
|     userId: string; // 用户编号
 | ||||
|     roleId: string; // 角色编号
 | ||||
|     model: number; // 模型标志
 | ||||
|     modelId: number; // 模型编号
 | ||||
|     content: string; // 聊天内容
 | ||||
|     tokens: number; // 消耗 Token 数量
 | ||||
|     segmentIds?: number[]; // 段落编号
 | ||||
|     segments?: { | ||||
|       content: string; // 段落内容
 | ||||
|       documentId: number; // 文档编号
 | ||||
|       documentName: string; // 文档名称
 | ||||
|       id: number; // 段落编号
 | ||||
|     }[]; | ||||
|     createTime: Date; // 创建时间
 | ||||
|     roleAvatar: string; // 角色头像
 | ||||
|     userAvatar: string; // 用户头像
 | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // 消息列表
 | ||||
| export function getChatMessageListByConversationId( | ||||
|   conversationId: null | number, | ||||
| ) { | ||||
|   return requestClient.get<AiChatMessageApi.ChatMessageVO[]>( | ||||
|     `/ai/chat/message/list-by-conversation-id?conversationId=${conversationId}`, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| // 发送 Stream 消息
 | ||||
| export function sendChatMessageStream( | ||||
|   conversationId: number, | ||||
|   content: string, | ||||
|   ctrl: any, | ||||
|   enableContext: boolean, | ||||
|   onMessage: any, | ||||
|   onError: any, | ||||
|   onClose: any, | ||||
| ) { | ||||
|   const token = accessStore.accessToken; | ||||
|   return fetchEventSource( | ||||
|     `${import.meta.env.VITE_BASE_URL}/ai/chat/message/send-stream`, | ||||
|     { | ||||
|       method: 'post', | ||||
|       headers: { | ||||
|         'Content-Type': 'application/json', | ||||
|         Authorization: `Bearer ${token}`, | ||||
|       }, | ||||
|       openWhenHidden: true, | ||||
|       body: JSON.stringify({ | ||||
|         conversationId, | ||||
|         content, | ||||
|         useContext: enableContext, | ||||
|       }), | ||||
|       onmessage: onMessage, | ||||
|       onerror: onError, | ||||
|       onclose: onClose, | ||||
|       signal: ctrl.signal, | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| // 删除消息
 | ||||
| export function deleteChatMessage(id: string) { | ||||
|   return requestClient.delete(`/ai/chat/message/delete?id=${id}`); | ||||
| } | ||||
| 
 | ||||
| // 删除指定对话的消息
 | ||||
| export function deleteByConversationId(conversationId: number) { | ||||
|   return requestClient.delete( | ||||
|     `/ai/chat/message/delete-by-conversation-id?conversationId=${conversationId}`, | ||||
|   ); | ||||
| } | ||||
| // 获得消息分页
 | ||||
| export function getChatMessagePage(params: any) { | ||||
|   return requestClient.get<PageResult<AiChatMessageApi.ChatMessageVO>>( | ||||
|     '/ai/chat/message/page', | ||||
|     { params }, | ||||
|   ); | ||||
| } | ||||
| // 管理员删除消息
 | ||||
| export function deleteChatMessageByAdmin(id: number) { | ||||
|   return requestClient.delete(`/ai/chat/message/delete-by-admin?id=${id}`); | ||||
| } | ||||
|  | @ -0,0 +1,50 @@ | |||
| import type { PageParam, PageResult } from '@vben/request'; | ||||
| 
 | ||||
| import { requestClient } from '#/api/request'; | ||||
| 
 | ||||
| export namespace AiKnowledgeKnowledgeApi { | ||||
|   export interface KnowledgeVO { | ||||
|     id: number; // 编号
 | ||||
|     name: string; // 知识库名称
 | ||||
|     description: string; // 知识库描述
 | ||||
|     embeddingModelId: number; // 嵌入模型编号,高质量模式时维护
 | ||||
|     topK: number; // topK
 | ||||
|     similarityThreshold: number; // 相似度阈值
 | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // 查询知识库分页
 | ||||
| export function getKnowledgePage(params: PageParam) { | ||||
|   return requestClient.get<PageResult<AiKnowledgeKnowledgeApi.KnowledgeVO>>( | ||||
|     '/ai/knowledge/page', | ||||
|     { params }, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| // 查询知识库详情
 | ||||
| export function getKnowledge(id: number) { | ||||
|   return requestClient.get<AiKnowledgeKnowledgeApi.KnowledgeVO>( | ||||
|     `/ai/knowledge/get?id=${id}`, | ||||
|   ); | ||||
| } | ||||
| // 新增知识库
 | ||||
| export function createKnowledge(data: AiKnowledgeKnowledgeApi.KnowledgeVO) { | ||||
|   return requestClient.post('/ai/knowledge/create', data); | ||||
| } | ||||
| 
 | ||||
| // 修改知识库
 | ||||
| export function updateKnowledge(data: AiKnowledgeKnowledgeApi.KnowledgeVO) { | ||||
|   return requestClient.put('/ai/knowledge/update', data); | ||||
| } | ||||
| 
 | ||||
| // 删除知识库
 | ||||
| export function deleteKnowledge(id: number) { | ||||
|   return requestClient.delete(`/ai/knowledge/delete?id=${id}`); | ||||
| } | ||||
| 
 | ||||
| // 获取知识库简单列表
 | ||||
| export function getSimpleKnowledgeList() { | ||||
|   return requestClient.get<AiKnowledgeKnowledgeApi.KnowledgeVO[]>( | ||||
|     '/ai/knowledge/simple-list', | ||||
|   ); | ||||
| } | ||||
|  | @ -0,0 +1,50 @@ | |||
| import type { PageParam, PageResult } from '@vben/request'; | ||||
| 
 | ||||
| import { requestClient } from '#/api/request'; | ||||
| 
 | ||||
| export namespace AiModelApiKeyApi { | ||||
|   export interface ApiKeyVO { | ||||
|     id: number; // 编号
 | ||||
|     name: string; // 名称
 | ||||
|     apiKey: string; // 密钥
 | ||||
|     platform: string; // 平台
 | ||||
|     url: string; // 自定义 API 地址
 | ||||
|     status: number; // 状态
 | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // 查询 API 密钥分页
 | ||||
| export function getApiKeyPage(params: PageParam) { | ||||
|   return requestClient.get<PageResult<AiModelApiKeyApi.ApiKeyVO>>( | ||||
|     '/ai/api-key/page', | ||||
|     { params }, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| // 获得 API 密钥列表
 | ||||
| export function getApiKeySimpleList() { | ||||
|   return requestClient.get<AiModelApiKeyApi.ApiKeyVO[]>( | ||||
|     '/ai/api-key/simple-list', | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| // 查询 API 密钥详情
 | ||||
| export function getApiKey(id: number) { | ||||
|   return requestClient.get<AiModelApiKeyApi.ApiKeyVO>( | ||||
|     `/ai/api-key/get?id=${id}`, | ||||
|   ); | ||||
| } | ||||
| // 新增 API 密钥
 | ||||
| export function createApiKey(data: AiModelApiKeyApi.ApiKeyVO) { | ||||
|   return requestClient.post('/ai/api-key/create', data); | ||||
| } | ||||
| 
 | ||||
| // 修改 API 密钥
 | ||||
| export function updateApiKey(data: AiModelApiKeyApi.ApiKeyVO) { | ||||
|   return requestClient.put('/ai/api-key/update', data); | ||||
| } | ||||
| 
 | ||||
| // 删除 API 密钥
 | ||||
| export function deleteApiKey(id: number) { | ||||
|   return requestClient.delete(`/ai/api-key/delete?id=${id}`); | ||||
| } | ||||
|  | @ -0,0 +1,85 @@ | |||
| import type { PageParam, PageResult } from '@vben/request'; | ||||
| 
 | ||||
| import { requestClient } from '#/api/request'; | ||||
| 
 | ||||
| export namespace AiModelChatRoleApi { | ||||
|   export interface ChatRoleVO { | ||||
|     id: number; // 角色编号
 | ||||
|     modelId: number; // 模型编号
 | ||||
|     name: string; // 角色名称
 | ||||
|     avatar: string; // 角色头像
 | ||||
|     category: string; // 角色类别
 | ||||
|     sort: number; // 角色排序
 | ||||
|     description: string; // 角色描述
 | ||||
|     systemMessage: string; // 角色设定
 | ||||
|     welcomeMessage: string; // 角色设定
 | ||||
|     publicStatus: boolean; // 是否公开
 | ||||
|     status: number; // 状态
 | ||||
|     knowledgeIds?: number[]; // 引用的知识库 ID 列表
 | ||||
|     toolIds?: number[]; // 引用的工具 ID 列表
 | ||||
|   } | ||||
| 
 | ||||
|   // AI 聊天角色 分页请求 vo
 | ||||
|   export interface ChatRolePageReqVO { | ||||
|     name?: string; // 角色名称
 | ||||
|     category?: string; // 角色类别
 | ||||
|     publicStatus: boolean; // 是否公开
 | ||||
|     pageNo: number; // 是否公开
 | ||||
|     pageSize: number; // 是否公开
 | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // 查询聊天角色分页
 | ||||
| export function getChatRolePage(params: PageParam) { | ||||
|   return requestClient.get<PageResult<AiModelChatRoleApi.ChatRoleVO>>( | ||||
|     '/ai/chat-role/page', | ||||
|     { params }, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| // 查询聊天角色详情
 | ||||
| export function getChatRole(id: number) { | ||||
|   return requestClient.get<AiModelChatRoleApi.ChatRoleVO>( | ||||
|     `/ai/chat-role/get?id=${id}`, | ||||
|   ); | ||||
| } | ||||
| // 新增聊天角色
 | ||||
| export function createChatRole(data: AiModelChatRoleApi.ChatRoleVO) { | ||||
|   return requestClient.post('/ai/chat-role/create', data); | ||||
| } | ||||
| 
 | ||||
| // 修改聊天角色
 | ||||
| export function updateChatRole(data: AiModelChatRoleApi.ChatRoleVO) { | ||||
|   return requestClient.put('/ai/chat-role/update', data); | ||||
| } | ||||
| 
 | ||||
| // 删除聊天角色
 | ||||
| export function deleteChatRole(id: number) { | ||||
|   return requestClient.delete(`/ai/chat-role/delete?id=${id}`); | ||||
| } | ||||
| 
 | ||||
| // ======= chat 聊天
 | ||||
| // 获取 my role
 | ||||
| export function getMyPage(params: AiModelChatRoleApi.ChatRolePageReqVO) { | ||||
|   return requestClient.get('/ai/chat-role/my-page', { params }); | ||||
| } | ||||
| 
 | ||||
| // 获取角色分类
 | ||||
| export function getCategoryList() { | ||||
|   return requestClient.get('/ai/chat-role/category-list'); | ||||
| } | ||||
| 
 | ||||
| // 创建角色
 | ||||
| export function createMy(data: AiModelChatRoleApi.ChatRoleVO) { | ||||
|   return requestClient.post('/ai/chat-role/create-my', data); | ||||
| } | ||||
| 
 | ||||
| // 更新角色
 | ||||
| export function updateMy(data: AiModelChatRoleApi.ChatRoleVO) { | ||||
|   return requestClient.put('/ai/chat-role/update', data); | ||||
| } | ||||
| 
 | ||||
| // 删除角色 my
 | ||||
| export function deleteMy(id: number) { | ||||
|   return requestClient.delete(`/ai/chat-role/delete-my?id=${id}`); | ||||
| } | ||||
|  | @ -0,0 +1,55 @@ | |||
| import type { PageParam, PageResult } from '@vben/request'; | ||||
| 
 | ||||
| import { requestClient } from '#/api/request'; | ||||
| 
 | ||||
| export namespace AiModelModelApi { | ||||
|   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 数量
 | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // 查询模型分页
 | ||||
| export function getModelPage(params: PageParam) { | ||||
|   return requestClient.get<PageResult<AiModelModelApi.ModelVO>>( | ||||
|     '/ai/model/page', | ||||
|     { params }, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| // 获得模型列表
 | ||||
| export function getModelSimpleList(type?: number) { | ||||
|   return requestClient.get<AiModelModelApi.ModelVO[]>('/ai/model/simple-list', { | ||||
|     params: { | ||||
|       type, | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| // 查询模型详情
 | ||||
| export function getModel(id: number) { | ||||
|   return requestClient.get<AiModelModelApi.ModelVO>(`/ai/model/get?id=${id}`); | ||||
| } | ||||
| // 新增模型
 | ||||
| export function createModel(data: AiModelModelApi.ModelVO) { | ||||
|   return requestClient.post('/ai/model/create', data); | ||||
| } | ||||
| 
 | ||||
| // 修改模型
 | ||||
| export function updateModel(data: AiModelModelApi.ModelVO) { | ||||
|   return requestClient.put('/ai/model/update', data); | ||||
| } | ||||
| 
 | ||||
| // 删除模型
 | ||||
| export function deleteModel(id: number) { | ||||
|   return requestClient.delete(`/ai/model/delete?id=${id}`); | ||||
| } | ||||
|  | @ -0,0 +1,43 @@ | |||
| import type { PageParam, PageResult } from '@vben/request'; | ||||
| 
 | ||||
| import { requestClient } from '#/api/request'; | ||||
| 
 | ||||
| export namespace AiModelToolApi { | ||||
|   export interface ToolVO { | ||||
|     id: number; // 工具编号
 | ||||
|     name: string; // 工具名称
 | ||||
|     description: string; // 工具描述
 | ||||
|     status: number; // 状态
 | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // 查询工具分页
 | ||||
| export function getToolPage(params: PageParam) { | ||||
|   return requestClient.get<PageResult<AiModelToolApi.ToolVO>>('/ai/tool/page', { | ||||
|     params, | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| // 查询工具详情
 | ||||
| export function getTool(id: number) { | ||||
|   return requestClient.get<AiModelToolApi.ToolVO>(`/ai/tool/get?id=${id}`); | ||||
| } | ||||
| // 新增工具
 | ||||
| export function createTool(data: AiModelToolApi.ToolVO) { | ||||
|   return requestClient.post('/ai/tool/create', data); | ||||
| } | ||||
| 
 | ||||
| // 修改工具
 | ||||
| export function updateTool(data: AiModelToolApi.ToolVO) { | ||||
|   return requestClient.put('/ai/tool/update', data); | ||||
| } | ||||
| 
 | ||||
| // 删除工具
 | ||||
| export function deleteTool(id: number) { | ||||
|   return requestClient.delete(`/ai/tool/delete?id=${id}`); | ||||
| } | ||||
| 
 | ||||
| // 获取工具简单列表
 | ||||
| export function getToolSimpleList() { | ||||
|   return requestClient.get<AiModelToolApi.ToolVO[]>('/ai/tool/simple-list'); | ||||
| } | ||||
|  | @ -5,6 +5,14 @@ | |||
|  * 枚举类 | ||||
|  */ | ||||
| 
 | ||||
| export const AiModelTypeEnum = { | ||||
|   CHAT: 1, // 聊天
 | ||||
|   IMAGE: 2, // 图像
 | ||||
|   VOICE: 3, // 音频
 | ||||
|   VIDEO: 4, // 视频
 | ||||
|   EMBEDDING: 5, // 向量
 | ||||
|   RERANK: 6, // 重排
 | ||||
| }; | ||||
| // ========== COMMON 模块 ==========
 | ||||
| // 全局通用状态枚举
 | ||||
| export const CommonStatusEnum = { | ||||
|  |  | |||
|  | @ -143,10 +143,10 @@ export const getBoolDictOptions = (dictType: string) => { | |||
| enum DICT_TYPE { | ||||
|   AI_GENERATE_MODE = 'ai_generate_mode', // AI 生成模式
 | ||||
|   AI_IMAGE_STATUS = 'ai_image_status', // AI 图片状态
 | ||||
|   AI_MODEL_TYPE = 'ai_model_type', // AI 模型类型
 | ||||
|   AI_MUSIC_STATUS = 'ai_music_status', // AI 音乐状态
 | ||||
|   // ========== AI - 人工智能模块  ==========
 | ||||
|   AI_PLATFORM = 'ai_platform', // AI 平台
 | ||||
| 
 | ||||
|   AI_WRITE_FORMAT = 'ai_write_format', // AI 写作格式
 | ||||
|   AI_WRITE_LANGUAGE = 'ai_write_language', // AI 写作语言
 | ||||
|   AI_WRITE_LENGTH = 'ai_write_length', // AI 写作长度
 | ||||
|  |  | |||
|  | @ -0,0 +1,196 @@ | |||
| import type { VbenFormSchema } from '#/adapter/form'; | ||||
| import type { VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| 
 | ||||
| import { getSimpleUserList } from '#/api/system/user'; | ||||
| import { DICT_TYPE } from '#/utils'; | ||||
| 
 | ||||
| /** 列表的搜索表单 */ | ||||
| export function useGridFormSchemaConversation(): VbenFormSchema[] { | ||||
|   return [ | ||||
|     { | ||||
|       fieldName: 'userId', | ||||
|       label: '用户编号', | ||||
|       component: 'Input', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'title', | ||||
|       label: '聊天标题', | ||||
|       component: 'Input', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'createTime', | ||||
|       label: '创建时间', | ||||
|       component: 'RangePicker', | ||||
|       componentProps: { | ||||
|         placeholder: ['开始时间', '结束时间'], | ||||
|         valueFormat: 'YYYY-MM-DD HH:mm:ss', | ||||
|         allowClear: true, | ||||
|       }, | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
| 
 | ||||
| /** 列表的字段 */ | ||||
| export function useGridColumnsConversation(): VxeTableGridOptions['columns'] { | ||||
|   return [ | ||||
|     { | ||||
|       field: 'id', | ||||
|       title: '对话编号', | ||||
|       fixed: 'left', | ||||
|       minWidth: 180, | ||||
|     }, | ||||
|     { | ||||
|       field: 'title', | ||||
|       title: '对话标题', | ||||
|       minWidth: 180, | ||||
|       fixed: 'left', | ||||
|     }, | ||||
|     { | ||||
|       title: '用户', | ||||
|       width: 180, | ||||
|       slots: { default: 'userId' }, | ||||
|     }, | ||||
|     { | ||||
|       field: 'roleName', | ||||
|       title: '角色', | ||||
|       minWidth: 180, | ||||
|     }, | ||||
|     { | ||||
|       field: 'model', | ||||
|       title: '模型标识', | ||||
|       minWidth: 180, | ||||
|     }, | ||||
|     { | ||||
|       field: 'messageCount', | ||||
|       title: '消息数', | ||||
|       minWidth: 180, | ||||
|     }, | ||||
|     { | ||||
|       field: 'createTime', | ||||
|       title: '创建时间', | ||||
|       minWidth: 180, | ||||
|       formatter: 'formatDateTime', | ||||
|     }, | ||||
|     { | ||||
|       field: 'temperature', | ||||
|       title: '温度参数', | ||||
|       minWidth: 80, | ||||
|     }, | ||||
|     { | ||||
|       title: '回复数 Token 数', | ||||
|       field: 'maxTokens', | ||||
|       minWidth: 120, | ||||
|     }, | ||||
|     { | ||||
|       title: '上下文数量', | ||||
|       field: 'maxContexts', | ||||
|       minWidth: 120, | ||||
|     }, | ||||
|     { | ||||
|       title: '操作', | ||||
|       width: 130, | ||||
|       fixed: 'right', | ||||
|       slots: { default: 'actions' }, | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
| 
 | ||||
| /** 列表的搜索表单 */ | ||||
| export function useGridFormSchemaMessage(): VbenFormSchema[] { | ||||
|   return [ | ||||
|     { | ||||
|       fieldName: 'conversationId', | ||||
|       label: '对话编号', | ||||
|       component: 'Input', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'userId', | ||||
|       label: '用户编号', | ||||
|       component: 'ApiSelect', | ||||
|       componentProps: { | ||||
|         api: getSimpleUserList, | ||||
|         labelField: 'nickname', | ||||
|         valueField: 'id', | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'createTime', | ||||
|       label: '创建时间', | ||||
|       component: 'RangePicker', | ||||
|       componentProps: { | ||||
|         placeholder: ['开始时间', '结束时间'], | ||||
|         valueFormat: 'YYYY-MM-DD HH:mm:ss', | ||||
|         allowClear: true, | ||||
|       }, | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
| 
 | ||||
| /** 列表的字段 */ | ||||
| export function useGridColumnsMessage(): VxeTableGridOptions['columns'] { | ||||
|   return [ | ||||
|     { | ||||
|       field: 'id', | ||||
|       title: '消息编号', | ||||
|       fixed: 'left', | ||||
|       minWidth: 180, | ||||
|     }, | ||||
|     { | ||||
|       field: 'conversationId', | ||||
|       title: '对话编号', | ||||
|       minWidth: 180, | ||||
|       fixed: 'left', | ||||
|     }, | ||||
|     { | ||||
|       title: '用户', | ||||
|       width: 180, | ||||
|       slots: { default: 'userId' }, | ||||
|     }, | ||||
|     { | ||||
|       field: 'roleName', | ||||
|       title: '角色', | ||||
|       minWidth: 180, | ||||
|     }, | ||||
|     { | ||||
|       field: 'type', | ||||
|       title: '消息类型', | ||||
|       minWidth: 100, | ||||
|     }, | ||||
|     { | ||||
|       field: 'model', | ||||
|       title: '模型标识', | ||||
|       minWidth: 180, | ||||
|     }, | ||||
|     { | ||||
|       field: 'content', | ||||
|       title: '消息内容', | ||||
|       minWidth: 300, | ||||
|     }, | ||||
|     { | ||||
|       field: 'createTime', | ||||
|       title: '创建时间', | ||||
|       minWidth: 180, | ||||
|       formatter: 'formatDateTime', | ||||
|     }, | ||||
|     { | ||||
|       field: 'replyId', | ||||
|       title: '回复消息编号', | ||||
|       minWidth: 180, | ||||
|     }, | ||||
|     { | ||||
|       title: '携带上下文', | ||||
|       field: 'useContext', | ||||
|       cellRender: { | ||||
|         name: 'CellDict', | ||||
|         props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING }, | ||||
|       }, | ||||
|       minWidth: 100, | ||||
|     }, | ||||
|     { | ||||
|       title: '操作', | ||||
|       width: 130, | ||||
|       fixed: 'right', | ||||
|       slots: { default: 'actions' }, | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
|  | @ -1,31 +1,30 @@ | |||
| <script lang="ts" setup> | ||||
| import { ref } from 'vue'; | ||||
| 
 | ||||
| import { Page } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { Button } from 'ant-design-vue'; | ||||
| import { Card, TabPane, Tabs } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| 
 | ||||
| import ChatConversationList from './modules/ChatConversationList.vue'; | ||||
| import ChatMessageList from './modules/ChatMessageList.vue'; | ||||
| 
 | ||||
| const activeTabName = ref('conversation'); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Page> | ||||
|   <Page auto-content-height> | ||||
|     <DocAlert title="AI 对话聊天" url="https://doc.iocoder.cn/ai/chat/" /> | ||||
|     <Button | ||||
|       danger | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3" | ||||
|     > | ||||
|       该功能支持 Vue3 + element-plus 版本! | ||||
|     </Button> | ||||
|     <br /> | ||||
|     <Button | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/chat/manager/index.vue" | ||||
|     > | ||||
|       可参考 | ||||
|       https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/chat/manager/index.vue | ||||
|       代码,pull request 贡献给我们! | ||||
|     </Button> | ||||
|     <Card> | ||||
|       <Tabs v-model:active-key="activeTabName"> | ||||
|         <TabPane tab="对话列表" key="conversation"> | ||||
|           <ChatConversationList /> | ||||
|         </TabPane> | ||||
|         <TabPane tab="消息列表" key="message"> | ||||
|           <ChatMessageList /> | ||||
|         </TabPane> | ||||
|       </Tabs> | ||||
|     </Card> | ||||
|   </Page> | ||||
| </template> | ||||
|  |  | |||
|  | @ -0,0 +1,113 @@ | |||
| <script lang="ts" setup> | ||||
| import type { VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| import type { AiChatConversationApi } from '#/api/ai/chat/conversation'; | ||||
| import type { SystemUserApi } from '#/api/system/user'; | ||||
| 
 | ||||
| import { onMounted, ref } from 'vue'; | ||||
| 
 | ||||
| import { Page } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { message } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; | ||||
| import { | ||||
|   deleteChatConversationByAdmin, | ||||
|   getChatConversationPage, | ||||
| } from '#/api/ai/chat/conversation'; | ||||
| import { getSimpleUserList } from '#/api/system/user'; | ||||
| import { $t } from '#/locales'; | ||||
| 
 | ||||
| import { | ||||
|   useGridColumnsConversation, | ||||
|   useGridFormSchemaConversation, | ||||
| } from '../data'; | ||||
| 
 | ||||
| const userList = ref<SystemUserApi.User[]>([]); // 用户列表 | ||||
| /** 刷新表格 */ | ||||
| function onRefresh() { | ||||
|   gridApi.query(); | ||||
| } | ||||
| 
 | ||||
| /** 删除 */ | ||||
| async function handleDelete(row: AiChatConversationApi.ChatConversationVO) { | ||||
|   const hideLoading = message.loading({ | ||||
|     content: $t('ui.actionMessage.deleting', [row.id]), | ||||
|     key: 'action_key_msg', | ||||
|   }); | ||||
|   try { | ||||
|     await deleteChatConversationByAdmin(row.id as number); | ||||
|     message.success({ | ||||
|       content: $t('ui.actionMessage.deleteSuccess', [row.id]), | ||||
|       key: 'action_key_msg', | ||||
|     }); | ||||
|     onRefresh(); | ||||
|   } finally { | ||||
|     hideLoading(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const [Grid, gridApi] = useVbenVxeGrid({ | ||||
|   formOptions: { | ||||
|     schema: useGridFormSchemaConversation(), | ||||
|   }, | ||||
|   gridOptions: { | ||||
|     columns: useGridColumnsConversation(), | ||||
|     height: 'auto', | ||||
|     keepSource: true, | ||||
|     proxyConfig: { | ||||
|       ajax: { | ||||
|         query: async ({ page }, formValues) => { | ||||
|           return await getChatConversationPage({ | ||||
|             pageNo: page.currentPage, | ||||
|             pageSize: page.pageSize, | ||||
|             ...formValues, | ||||
|           }); | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     rowConfig: { | ||||
|       keyField: 'id', | ||||
|     }, | ||||
|     toolbarConfig: { | ||||
|       refresh: { code: 'query' }, | ||||
|       search: true, | ||||
|     }, | ||||
|   } as VxeTableGridOptions<AiChatConversationApi.ChatConversationVO>, | ||||
| }); | ||||
| onMounted(async () => { | ||||
|   // 获得用户列表 | ||||
|   userList.value = await getSimpleUserList(); | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Page auto-content-height> | ||||
|     <Grid table-title="对话列表"> | ||||
|       <template #toolbar-tools> | ||||
|         <TableAction :actions="[]" /> | ||||
|       </template> | ||||
|       <template #userId="{ row }"> | ||||
|         <span>{{ | ||||
|           userList.find((item) => item.id === row.userId)?.nickname | ||||
|         }}</span> | ||||
|       </template> | ||||
|       <template #actions="{ row }"> | ||||
|         <TableAction | ||||
|           :actions="[ | ||||
|             { | ||||
|               label: $t('common.delete'), | ||||
|               type: 'link', | ||||
|               danger: true, | ||||
|               icon: ACTION_ICON.DELETE, | ||||
|               auth: ['ai:chat-conversation:delete'], | ||||
|               popConfirm: { | ||||
|                 title: $t('ui.actionMessage.deleteConfirm', [row.id]), | ||||
|                 confirm: handleDelete.bind(null, row), | ||||
|               }, | ||||
|             }, | ||||
|           ]" | ||||
|         /> | ||||
|       </template> | ||||
|     </Grid> | ||||
|   </Page> | ||||
| </template> | ||||
|  | @ -0,0 +1,110 @@ | |||
| <script lang="ts" setup> | ||||
| import type { VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| import type { AiChatConversationApi } from '#/api/ai/chat/conversation'; | ||||
| import type { SystemUserApi } from '#/api/system/user'; | ||||
| 
 | ||||
| import { onMounted, ref } from 'vue'; | ||||
| 
 | ||||
| import { Page } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { message } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; | ||||
| import { | ||||
|   deleteChatMessageByAdmin, | ||||
|   getChatMessagePage, | ||||
| } from '#/api/ai/chat/message'; | ||||
| import { getSimpleUserList } from '#/api/system/user'; | ||||
| import { $t } from '#/locales'; | ||||
| 
 | ||||
| import { useGridColumnsMessage, useGridFormSchemaMessage } from '../data'; | ||||
| 
 | ||||
| const userList = ref<SystemUserApi.User[]>([]); // 用户列表 | ||||
| /** 刷新表格 */ | ||||
| function onRefresh() { | ||||
|   gridApi.query(); | ||||
| } | ||||
| 
 | ||||
| /** 删除 */ | ||||
| async function handleDelete(row: AiChatConversationApi.ChatConversationVO) { | ||||
|   const hideLoading = message.loading({ | ||||
|     content: $t('ui.actionMessage.deleting', [row.id]), | ||||
|     key: 'action_key_msg', | ||||
|   }); | ||||
|   try { | ||||
|     await deleteChatMessageByAdmin(row.id as number); | ||||
|     message.success({ | ||||
|       content: $t('ui.actionMessage.deleteSuccess', [row.id]), | ||||
|       key: 'action_key_msg', | ||||
|     }); | ||||
|     onRefresh(); | ||||
|   } finally { | ||||
|     hideLoading(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const [Grid, gridApi] = useVbenVxeGrid({ | ||||
|   formOptions: { | ||||
|     schema: useGridFormSchemaMessage(), | ||||
|   }, | ||||
|   gridOptions: { | ||||
|     columns: useGridColumnsMessage(), | ||||
|     height: 'auto', | ||||
|     keepSource: true, | ||||
|     proxyConfig: { | ||||
|       ajax: { | ||||
|         query: async ({ page }, formValues) => { | ||||
|           return await getChatMessagePage({ | ||||
|             pageNo: page.currentPage, | ||||
|             pageSize: page.pageSize, | ||||
|             ...formValues, | ||||
|           }); | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     rowConfig: { | ||||
|       keyField: 'id', | ||||
|     }, | ||||
|     toolbarConfig: { | ||||
|       refresh: { code: 'query' }, | ||||
|       search: true, | ||||
|     }, | ||||
|   } as VxeTableGridOptions<AiChatConversationApi.ChatConversationVO>, | ||||
| }); | ||||
| onMounted(async () => { | ||||
|   // 获得用户列表 | ||||
|   userList.value = await getSimpleUserList(); | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Page auto-content-height> | ||||
|     <Grid table-title="消息列表"> | ||||
|       <template #toolbar-tools> | ||||
|         <TableAction :actions="[]" /> | ||||
|       </template> | ||||
|       <template #userId="{ row }"> | ||||
|         <span>{{ | ||||
|           userList.find((item) => item.id === row.userId)?.nickname | ||||
|         }}</span> | ||||
|       </template> | ||||
|       <template #actions="{ row }"> | ||||
|         <TableAction | ||||
|           :actions="[ | ||||
|             { | ||||
|               label: $t('common.delete'), | ||||
|               type: 'link', | ||||
|               danger: true, | ||||
|               icon: ACTION_ICON.DELETE, | ||||
|               auth: ['ai:chat-message:delete'], | ||||
|               popConfirm: { | ||||
|                 title: $t('ui.actionMessage.deleteConfirm', [row.id]), | ||||
|                 confirm: handleDelete.bind(null, row), | ||||
|               }, | ||||
|             }, | ||||
|           ]" | ||||
|         /> | ||||
|       </template> | ||||
|     </Grid> | ||||
|   </Page> | ||||
| </template> | ||||
|  | @ -0,0 +1,126 @@ | |||
| import type { VbenFormSchema } from '#/adapter/form'; | ||||
| import type { VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| 
 | ||||
| import { z } from '#/adapter/form'; | ||||
| import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils'; | ||||
| /** 新增/修改的表单 */ | ||||
| export function useFormSchema(): VbenFormSchema[] { | ||||
|   return [ | ||||
|     { | ||||
|       component: 'Input', | ||||
|       fieldName: 'id', | ||||
|       dependencies: { | ||||
|         triggerFields: [''], | ||||
|         show: () => false, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'platform', | ||||
|       label: '所属平台', | ||||
|       component: 'Select', | ||||
|       componentProps: { | ||||
|         placeholder: '请选择所属平台', | ||||
|         options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'), | ||||
|         allowClear: true, | ||||
|       }, | ||||
|       rules: z.string().min(1, { message: '请输入平台' }), | ||||
|     }, | ||||
|     { | ||||
|       component: 'Input', | ||||
|       fieldName: 'name', | ||||
|       label: '名称', | ||||
|       rules: 'required', | ||||
|     }, | ||||
|     { | ||||
|       component: 'Input', | ||||
|       fieldName: 'apiKey', | ||||
|       label: '密钥', | ||||
|       rules: 'required', | ||||
|     }, | ||||
|     { | ||||
|       component: 'Input', | ||||
|       fieldName: 'url', | ||||
|       label: '自定义 API URL', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'status', | ||||
|       label: '状态', | ||||
|       component: 'RadioGroup', | ||||
|       componentProps: { | ||||
|         options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), | ||||
|         buttonStyle: 'solid', | ||||
|         optionType: 'button', | ||||
|       }, | ||||
|       rules: z.number().default(CommonStatusEnum.ENABLE), | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
| 
 | ||||
| /** 列表的搜索表单 */ | ||||
| export function useGridFormSchema(): VbenFormSchema[] { | ||||
|   return [ | ||||
|     { | ||||
|       fieldName: 'name', | ||||
|       label: '名称', | ||||
|       component: 'Input', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'platform', | ||||
|       label: '平台', | ||||
|       component: 'Select', | ||||
|       componentProps: { | ||||
|         allowClear: true, | ||||
|         options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'), | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'status', | ||||
|       label: '状态', | ||||
|       component: 'Select', | ||||
|       componentProps: { | ||||
|         allowClear: true, | ||||
|         options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), | ||||
|       }, | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
| 
 | ||||
| /** 列表的字段 */ | ||||
| export function useGridColumns(): VxeTableGridOptions['columns'] { | ||||
|   return [ | ||||
|     { | ||||
|       field: 'platform', | ||||
|       title: '所属平台', | ||||
|       cellRender: { | ||||
|         name: 'CellDict', | ||||
|         props: { type: DICT_TYPE.AI_PLATFORM }, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       field: 'name', | ||||
|       title: '名称', | ||||
|     }, | ||||
|     { | ||||
|       field: 'apiKey', | ||||
|       title: '密钥', | ||||
|     }, | ||||
|     { | ||||
|       field: 'url', | ||||
|       title: '自定义 API URL', | ||||
|     }, | ||||
|     { | ||||
|       field: 'status', | ||||
|       title: '状态', | ||||
|       cellRender: { | ||||
|         name: 'CellDict', | ||||
|         props: { type: DICT_TYPE.COMMON_STATUS }, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       title: '操作', | ||||
|       width: 130, | ||||
|       fixed: 'right', | ||||
|       slots: { default: 'actions' }, | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
|  | @ -1,31 +1,129 @@ | |||
| <script lang="ts" setup> | ||||
| import { Page } from '@vben/common-ui'; | ||||
| import type { VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| import type { AiModelApiKeyApi } from '#/api/ai/model/apiKey'; | ||||
| 
 | ||||
| import { Button } from 'ant-design-vue'; | ||||
| import { Page, useVbenModal } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { message } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; | ||||
| import { deleteApiKey, getApiKeyPage } from '#/api/ai/model/apiKey'; | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| import { $t } from '#/locales'; | ||||
| 
 | ||||
| import { useGridColumns, useGridFormSchema } from './data'; | ||||
| import Form from './modules/form.vue'; | ||||
| 
 | ||||
| const [FormModal, formModalApi] = useVbenModal({ | ||||
|   connectedComponent: Form, | ||||
|   destroyOnClose: true, | ||||
| }); | ||||
| 
 | ||||
| /** 刷新表格 */ | ||||
| function onRefresh() { | ||||
|   gridApi.query(); | ||||
| } | ||||
| 
 | ||||
| /** 创建 */ | ||||
| function handleCreate() { | ||||
|   formModalApi.setData(null).open(); | ||||
| } | ||||
| 
 | ||||
| /** 编辑 */ | ||||
| function handleEdit(row: AiModelApiKeyApi.ApiKeyVO) { | ||||
|   formModalApi.setData(row).open(); | ||||
| } | ||||
| 
 | ||||
| /** 删除 */ | ||||
| async function handleDelete(row: AiModelApiKeyApi.ApiKeyVO) { | ||||
|   const hideLoading = message.loading({ | ||||
|     content: $t('ui.actionMessage.deleting', [row.name]), | ||||
|     key: 'action_key_msg', | ||||
|   }); | ||||
|   try { | ||||
|     await deleteApiKey(row.id as number); | ||||
|     message.success({ | ||||
|       content: $t('ui.actionMessage.deleteSuccess', [row.name]), | ||||
|       key: 'action_key_msg', | ||||
|     }); | ||||
|     onRefresh(); | ||||
|   } finally { | ||||
|     hideLoading(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const [Grid, gridApi] = useVbenVxeGrid({ | ||||
|   formOptions: { | ||||
|     schema: useGridFormSchema(), | ||||
|   }, | ||||
|   gridOptions: { | ||||
|     columns: useGridColumns(), | ||||
|     height: 'auto', | ||||
|     keepSource: true, | ||||
|     proxyConfig: { | ||||
|       ajax: { | ||||
|         query: async ({ page }, formValues) => { | ||||
|           return await getApiKeyPage({ | ||||
|             pageNo: page.currentPage, | ||||
|             pageSize: page.pageSize, | ||||
|             ...formValues, | ||||
|           }); | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     rowConfig: { | ||||
|       keyField: 'id', | ||||
|     }, | ||||
|     toolbarConfig: { | ||||
|       refresh: { code: 'query' }, | ||||
|       search: true, | ||||
|     }, | ||||
|   } as VxeTableGridOptions<AiModelApiKeyApi.ApiKeyVO>, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Page> | ||||
|   <Page auto-content-height> | ||||
|     <DocAlert title="AI 手册" url="https://doc.iocoder.cn/ai/build/" /> | ||||
|     <Button | ||||
|       danger | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3" | ||||
|     > | ||||
|       该功能支持 Vue3 + element-plus 版本! | ||||
|     </Button> | ||||
|     <br /> | ||||
|     <Button | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/model/apiKey/index.vue" | ||||
|     > | ||||
|       可参考 | ||||
|       https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/model/apiKey/index.vue | ||||
|       代码,pull request 贡献给我们! | ||||
|     </Button> | ||||
|     <FormModal @success="onRefresh" /> | ||||
|     <Grid table-title="API  密钥列表"> | ||||
|       <template #toolbar-tools> | ||||
|         <TableAction | ||||
|           :actions="[ | ||||
|             { | ||||
|               label: $t('ui.actionTitle.create', ['API  密钥']), | ||||
|               type: 'primary', | ||||
|               icon: ACTION_ICON.ADD, | ||||
|               auth: ['ai:api-key:create'], | ||||
|               onClick: handleCreate, | ||||
|             }, | ||||
|           ]" | ||||
|         /> | ||||
|       </template> | ||||
|       <template #actions="{ row }"> | ||||
|         <TableAction | ||||
|           :actions="[ | ||||
|             { | ||||
|               label: $t('common.edit'), | ||||
|               type: 'link', | ||||
|               icon: ACTION_ICON.EDIT, | ||||
|               auth: ['ai:api-key:update'], | ||||
|               onClick: handleEdit.bind(null, row), | ||||
|             }, | ||||
|             { | ||||
|               label: $t('common.delete'), | ||||
|               type: 'link', | ||||
|               danger: true, | ||||
|               icon: ACTION_ICON.DELETE, | ||||
|               auth: ['ai:api-key:delete'], | ||||
|               popConfirm: { | ||||
|                 title: $t('ui.actionMessage.deleteConfirm', [row.name]), | ||||
|                 confirm: handleDelete.bind(null, row), | ||||
|               }, | ||||
|             }, | ||||
|           ]" | ||||
|         /> | ||||
|       </template> | ||||
|     </Grid> | ||||
|   </Page> | ||||
| </template> | ||||
|  |  | |||
|  | @ -0,0 +1,82 @@ | |||
| <script lang="ts" setup> | ||||
| import type { AiModelApiKeyApi } from '#/api/ai/model/apiKey'; | ||||
| 
 | ||||
| import { computed, ref } from 'vue'; | ||||
| 
 | ||||
| import { useVbenModal } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { message } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { useVbenForm } from '#/adapter/form'; | ||||
| import { createApiKey, getApiKey, updateApiKey } from '#/api/ai/model/apiKey'; | ||||
| import { $t } from '#/locales'; | ||||
| 
 | ||||
| import { useFormSchema } from '../data'; | ||||
| 
 | ||||
| const emit = defineEmits(['success']); | ||||
| const formData = ref<AiModelApiKeyApi.ApiKeyVO>(); | ||||
| const getTitle = computed(() => { | ||||
|   return formData.value?.id | ||||
|     ? $t('ui.actionTitle.edit', ['API  密钥']) | ||||
|     : $t('ui.actionTitle.create', ['API  密钥']); | ||||
| }); | ||||
| 
 | ||||
| const [Form, formApi] = useVbenForm({ | ||||
|   commonConfig: { | ||||
|     componentProps: { | ||||
|       class: 'w-full', | ||||
|     }, | ||||
|     formItemClass: 'col-span-2', | ||||
|     labelWidth: 100, | ||||
|   }, | ||||
|   layout: 'horizontal', | ||||
|   schema: useFormSchema(), | ||||
|   showDefaultActions: false, | ||||
| }); | ||||
| 
 | ||||
| const [Modal, modalApi] = useVbenModal({ | ||||
|   async onConfirm() { | ||||
|     const { valid } = await formApi.validate(); | ||||
|     if (!valid) { | ||||
|       return; | ||||
|     } | ||||
|     modalApi.lock(); | ||||
|     // 提交表单 | ||||
|     const data = (await formApi.getValues()) as AiModelApiKeyApi.ApiKeyVO; | ||||
|     try { | ||||
|       await (formData.value?.id ? updateApiKey(data) : createApiKey(data)); | ||||
|       // 关闭并提示 | ||||
|       await modalApi.close(); | ||||
|       emit('success'); | ||||
|       message.success($t('ui.actionMessage.operationSuccess')); | ||||
|     } finally { | ||||
|       modalApi.unlock(); | ||||
|     } | ||||
|   }, | ||||
|   async onOpenChange(isOpen: boolean) { | ||||
|     if (!isOpen) { | ||||
|       formData.value = undefined; | ||||
|       return; | ||||
|     } | ||||
|     // 加载数据 | ||||
|     const data = modalApi.getData<AiModelApiKeyApi.ApiKeyVO>(); | ||||
|     if (!data || !data.id) { | ||||
|       return; | ||||
|     } | ||||
|     modalApi.lock(); | ||||
|     try { | ||||
|       formData.value = await getApiKey(data.id as number); | ||||
|       // 设置到 values | ||||
|       await formApi.setValues(formData.value); | ||||
|     } finally { | ||||
|       modalApi.unlock(); | ||||
|     } | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Modal class="w-[600px]" :title="getTitle"> | ||||
|     <Form class="mx-4" /> | ||||
|   </Modal> | ||||
| </template> | ||||
|  | @ -0,0 +1,277 @@ | |||
| import type { VbenFormSchema } from '#/adapter/form'; | ||||
| import type { VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| 
 | ||||
| import { z } from '#/adapter/form'; | ||||
| import { getSimpleKnowledgeList } from '#/api/ai/knowledge/knowledge'; | ||||
| import { getModelSimpleList } from '#/api/ai/model/model'; | ||||
| import { getToolSimpleList } from '#/api/ai/model/tool'; | ||||
| import { | ||||
|   AiModelTypeEnum, | ||||
|   CommonStatusEnum, | ||||
|   DICT_TYPE, | ||||
|   getDictOptions, | ||||
| } from '#/utils'; | ||||
| /** 新增/修改的表单 */ | ||||
| export function useFormSchema(): VbenFormSchema[] { | ||||
|   return [ | ||||
|     { | ||||
|       component: 'Input', | ||||
|       fieldName: 'id', | ||||
|       dependencies: { | ||||
|         triggerFields: [''], | ||||
|         show: () => false, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       component: 'Input', | ||||
|       fieldName: 'formType', | ||||
|       dependencies: { | ||||
|         triggerFields: [''], | ||||
|         show: () => false, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       component: 'Input', | ||||
|       fieldName: 'name', | ||||
|       label: '角色名称', | ||||
|       rules: 'required', | ||||
|     }, | ||||
|     { | ||||
|       component: 'ImageUpload', | ||||
|       fieldName: 'avatar', | ||||
|       label: '角色头像', | ||||
|       componentProps: { | ||||
|         maxSize: 1, | ||||
|       }, | ||||
|       rules: 'required', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'modelId', | ||||
|       label: '绑定模型', | ||||
|       component: 'ApiSelect', | ||||
|       componentProps: { | ||||
|         placeholder: '请选择绑定模型', | ||||
|         api: () => getModelSimpleList(AiModelTypeEnum.CHAT), | ||||
|         labelField: 'name', | ||||
|         valueField: 'id', | ||||
|         allowClear: true, | ||||
|       }, | ||||
|       dependencies: { | ||||
|         triggerFields: ['formType'], | ||||
|         show: (values) => { | ||||
|           return values.formType === 'create' || values.formType === 'update'; | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       component: 'Input', | ||||
|       fieldName: 'category', | ||||
|       label: '角色类别', | ||||
|       rules: 'required', | ||||
|       dependencies: { | ||||
|         triggerFields: ['formType'], | ||||
|         show: (values) => { | ||||
|           return values.formType === 'create' || values.formType === 'update'; | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       component: 'Textarea', | ||||
|       fieldName: 'description', | ||||
|       label: '角色描述', | ||||
|       componentProps: { | ||||
|         placeholder: '请输入角色描述', | ||||
|       }, | ||||
|       rules: 'required', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'systemMessage', | ||||
|       label: '角色设定', | ||||
|       component: 'Textarea', | ||||
|       componentProps: { | ||||
|         placeholder: '请输入角色设定', | ||||
|       }, | ||||
|       rules: 'required', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'knowledgeIds', | ||||
|       label: '引用知识库', | ||||
|       component: 'ApiSelect', | ||||
|       componentProps: { | ||||
|         placeholder: '请选择引用知识库', | ||||
|         api: getSimpleKnowledgeList, | ||||
|         labelField: 'name', | ||||
|         mode: 'multiple', | ||||
|         valueField: 'id', | ||||
|         allowClear: true, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'toolIds', | ||||
|       label: '引用工具', | ||||
|       component: 'ApiSelect', | ||||
|       componentProps: { | ||||
|         placeholder: '请选择引用工具', | ||||
|         api: getToolSimpleList, | ||||
|         mode: 'multiple', | ||||
|         labelField: 'name', | ||||
|         valueField: 'id', | ||||
|         allowClear: true, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'publicStatus', | ||||
|       label: '是否公开', | ||||
|       component: 'RadioGroup', | ||||
|       componentProps: { | ||||
|         options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'), | ||||
|         buttonStyle: 'solid', | ||||
|         optionType: 'button', | ||||
|       }, | ||||
|       defaultValue: true, | ||||
|       dependencies: { | ||||
|         triggerFields: ['formType'], | ||||
|         show: (values) => { | ||||
|           return values.formType === 'create' || values.formType === 'update'; | ||||
|         }, | ||||
|       }, | ||||
|       rules: 'required', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'sort', | ||||
|       label: '角色排序', | ||||
|       component: 'InputNumber', | ||||
|       componentProps: { | ||||
|         controlsPosition: 'right', | ||||
|         placeholder: '请输入角色排序', | ||||
|         class: 'w-full', | ||||
|       }, | ||||
|       dependencies: { | ||||
|         triggerFields: ['formType'], | ||||
|         show: (values) => { | ||||
|           return values.formType === 'create' || values.formType === 'update'; | ||||
|         }, | ||||
|       }, | ||||
|       rules: 'required', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'status', | ||||
|       label: '开启状态', | ||||
|       component: 'RadioGroup', | ||||
|       componentProps: { | ||||
|         options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), | ||||
|         buttonStyle: 'solid', | ||||
|         optionType: 'button', | ||||
|       }, | ||||
|       dependencies: { | ||||
|         triggerFields: ['formType'], | ||||
|         show: (values) => { | ||||
|           return values.formType === 'create' || values.formType === 'update'; | ||||
|         }, | ||||
|       }, | ||||
|       rules: z.number().default(CommonStatusEnum.ENABLE), | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
| 
 | ||||
| /** 列表的搜索表单 */ | ||||
| export function useGridFormSchema(): VbenFormSchema[] { | ||||
|   return [ | ||||
|     { | ||||
|       fieldName: 'name', | ||||
|       label: '角色名称', | ||||
|       component: 'Input', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'category', | ||||
|       label: '角色类别', | ||||
|       component: 'Input', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'publicStatus', | ||||
|       label: '是否公开', | ||||
|       component: 'Select', | ||||
|       componentProps: { | ||||
|         placeholder: '请选择是否公开', | ||||
|         options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'), | ||||
|         allowClear: true, | ||||
|       }, | ||||
|       defaultValue: true, | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
| 
 | ||||
| /** 列表的字段 */ | ||||
| export function useGridColumns(): VxeTableGridOptions['columns'] { | ||||
|   return [ | ||||
|     { | ||||
|       field: 'name', | ||||
|       title: '角色名称', | ||||
|       minWidth: 100, | ||||
|     }, | ||||
|     { | ||||
|       title: '绑定模型', | ||||
|       field: 'modelName', | ||||
|       minWidth: 100, | ||||
|     }, | ||||
|     { | ||||
|       title: '角色头像', | ||||
|       slots: { default: 'avatar' }, | ||||
|       minWidth: 140, | ||||
|     }, | ||||
|     { | ||||
|       title: '角色类别', | ||||
|       field: 'category', | ||||
|       minWidth: 100, | ||||
|     }, | ||||
|     { | ||||
|       title: '角色描述', | ||||
|       field: 'description', | ||||
|       minWidth: 100, | ||||
|     }, | ||||
|     { | ||||
|       title: '角色设定', | ||||
|       field: 'systemMessage', | ||||
|       minWidth: 100, | ||||
|     }, | ||||
|     { | ||||
|       title: '知识库', | ||||
|       slots: { default: 'knowledgeIds' }, | ||||
|       minWidth: 100, | ||||
|     }, | ||||
|     { | ||||
|       title: '工具', | ||||
|       slots: { default: 'toolIds' }, | ||||
|       minWidth: 100, | ||||
|     }, | ||||
|     { | ||||
|       field: 'publicStatus', | ||||
|       title: '是否公开', | ||||
|       cellRender: { | ||||
|         name: 'CellDict', | ||||
|         props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING }, | ||||
|       }, | ||||
|       minWidth: 80, | ||||
|     }, | ||||
|     { | ||||
|       field: 'status', | ||||
|       title: '状态', | ||||
|       cellRender: { | ||||
|         name: 'CellDict', | ||||
|         props: { type: DICT_TYPE.COMMON_STATUS }, | ||||
|       }, | ||||
|       minWidth: 80, | ||||
|     }, | ||||
|     { | ||||
|       title: '角色排序', | ||||
|       field: 'sort', | ||||
|       minWidth: 80, | ||||
|     }, | ||||
|     { | ||||
|       title: '操作', | ||||
|       width: 130, | ||||
|       fixed: 'right', | ||||
|       slots: { default: 'actions' }, | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
|  | @ -1,31 +1,140 @@ | |||
| <script lang="ts" setup> | ||||
| import { Page } from '@vben/common-ui'; | ||||
| import type { VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| import type { AiModelChatRoleApi } from '#/api/ai/model/chatRole'; | ||||
| 
 | ||||
| import { Button } from 'ant-design-vue'; | ||||
| import { Page, useVbenModal } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { Image, message } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; | ||||
| import { deleteChatRole, getChatRolePage } from '#/api/ai/model/chatRole'; | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| import { $t } from '#/locales'; | ||||
| 
 | ||||
| import { useGridColumns, useGridFormSchema } from './data'; | ||||
| import Form from './modules/form.vue'; | ||||
| 
 | ||||
| const [FormModal, formModalApi] = useVbenModal({ | ||||
|   connectedComponent: Form, | ||||
|   destroyOnClose: true, | ||||
| }); | ||||
| 
 | ||||
| /** 刷新表格 */ | ||||
| function onRefresh() { | ||||
|   gridApi.query(); | ||||
| } | ||||
| 
 | ||||
| /** 创建 */ | ||||
| function handleCreate() { | ||||
|   formModalApi.setData({ formType: 'create' }).open(); | ||||
| } | ||||
| 
 | ||||
| /** 编辑 */ | ||||
| function handleEdit(row: AiModelChatRoleApi.ChatRoleVO) { | ||||
|   formModalApi.setData({ formType: 'update', ...row }).open(); | ||||
| } | ||||
| 
 | ||||
| /** 删除 */ | ||||
| async function handleDelete(row: AiModelChatRoleApi.ChatRoleVO) { | ||||
|   const hideLoading = message.loading({ | ||||
|     content: $t('ui.actionMessage.deleting', [row.name]), | ||||
|     key: 'action_key_msg', | ||||
|   }); | ||||
|   try { | ||||
|     await deleteChatRole(row.id as number); | ||||
|     message.success({ | ||||
|       content: $t('ui.actionMessage.deleteSuccess', [row.name]), | ||||
|       key: 'action_key_msg', | ||||
|     }); | ||||
|     onRefresh(); | ||||
|   } finally { | ||||
|     hideLoading(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const [Grid, gridApi] = useVbenVxeGrid({ | ||||
|   formOptions: { | ||||
|     schema: useGridFormSchema(), | ||||
|   }, | ||||
|   gridOptions: { | ||||
|     columns: useGridColumns(), | ||||
|     height: 'auto', | ||||
|     keepSource: true, | ||||
|     proxyConfig: { | ||||
|       ajax: { | ||||
|         query: async ({ page }, formValues) => { | ||||
|           return await getChatRolePage({ | ||||
|             pageNo: page.currentPage, | ||||
|             pageSize: page.pageSize, | ||||
|             ...formValues, | ||||
|           }); | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     rowConfig: { | ||||
|       keyField: 'id', | ||||
|     }, | ||||
|     toolbarConfig: { | ||||
|       refresh: { code: 'query' }, | ||||
|       search: true, | ||||
|     }, | ||||
|   } as VxeTableGridOptions<AiModelChatRoleApi.ChatRoleVO>, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Page> | ||||
|   <Page auto-content-height> | ||||
|     <DocAlert title="AI 对话聊天" url="https://doc.iocoder.cn/ai/chat/" /> | ||||
|     <Button | ||||
|       danger | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3" | ||||
|     > | ||||
|       该功能支持 Vue3 + element-plus 版本! | ||||
|     </Button> | ||||
|     <br /> | ||||
|     <Button | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/model/chatRole/index.vue" | ||||
|     > | ||||
|       可参考 | ||||
|       https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/model/chatRole/index.vue | ||||
|       代码,pull request 贡献给我们! | ||||
|     </Button> | ||||
|     <FormModal @success="onRefresh" /> | ||||
|     <Grid table-title="聊天角色列表"> | ||||
|       <template #toolbar-tools> | ||||
|         <TableAction | ||||
|           :actions="[ | ||||
|             { | ||||
|               label: $t('ui.actionTitle.create', ['聊天角色']), | ||||
|               type: 'primary', | ||||
|               icon: ACTION_ICON.ADD, | ||||
|               auth: ['ai:chat-role:create'], | ||||
|               onClick: handleCreate, | ||||
|             }, | ||||
|           ]" | ||||
|         /> | ||||
|       </template> | ||||
|       <template #knowledgeIds="{ row }"> | ||||
|         <span v-if="!row.knowledgeIds || row.knowledgeIds.length === 0">-</span> | ||||
|         <span v-else>引用 {{ row.knowledgeIds.length }} 个</span> | ||||
|       </template> | ||||
|       <template #toolIds="{ row }"> | ||||
|         <span v-if="!row.toolIds || row.toolIds.length === 0">-</span> | ||||
|         <span v-else>引用 {{ row.toolIds.length }} 个</span> | ||||
|       </template> | ||||
|       <template #avatar="{ row }"> | ||||
|         <Image :src="row.avatar" class="w-32px h-32px" /> | ||||
|       </template> | ||||
|       <template #actions="{ row }"> | ||||
|         <TableAction | ||||
|           :actions="[ | ||||
|             { | ||||
|               label: $t('common.edit'), | ||||
|               type: 'link', | ||||
|               icon: ACTION_ICON.EDIT, | ||||
|               auth: ['ai:chat-role:update'], | ||||
|               onClick: handleEdit.bind(null, row), | ||||
|             }, | ||||
|             { | ||||
|               label: $t('common.delete'), | ||||
|               type: 'link', | ||||
|               danger: true, | ||||
|               icon: ACTION_ICON.DELETE, | ||||
|               auth: ['ai:chat-role:delete'], | ||||
|               popConfirm: { | ||||
|                 title: $t('ui.actionMessage.deleteConfirm', [row.name]), | ||||
|                 confirm: handleDelete.bind(null, row), | ||||
|               }, | ||||
|             }, | ||||
|           ]" | ||||
|         /> | ||||
|       </template> | ||||
|     </Grid> | ||||
|   </Page> | ||||
| </template> | ||||
|  |  | |||
|  | @ -0,0 +1,88 @@ | |||
| <script lang="ts" setup> | ||||
| import type { AiModelChatRoleApi } from '#/api/ai/model/chatRole'; | ||||
| 
 | ||||
| import { computed, ref } from 'vue'; | ||||
| 
 | ||||
| import { useVbenModal } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { message } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { useVbenForm } from '#/adapter/form'; | ||||
| import { | ||||
|   createChatRole, | ||||
|   getChatRole, | ||||
|   updateChatRole, | ||||
| } from '#/api/ai/model/chatRole'; | ||||
| import {} from '#/api/bpm/model'; | ||||
| import { $t } from '#/locales'; | ||||
| 
 | ||||
| import { useFormSchema } from '../data'; | ||||
| 
 | ||||
| const emit = defineEmits(['success']); | ||||
| const formData = ref<AiModelChatRoleApi.ChatRoleVO>(); | ||||
| const getTitle = computed(() => { | ||||
|   return formData.value?.id | ||||
|     ? $t('ui.actionTitle.edit', ['聊天角色']) | ||||
|     : $t('ui.actionTitle.create', ['聊天角色']); | ||||
| }); | ||||
| 
 | ||||
| const [Form, formApi] = useVbenForm({ | ||||
|   commonConfig: { | ||||
|     componentProps: { | ||||
|       class: 'w-full', | ||||
|     }, | ||||
|     formItemClass: 'col-span-2', | ||||
|     labelWidth: 120, | ||||
|   }, | ||||
|   layout: 'horizontal', | ||||
|   schema: useFormSchema(), | ||||
|   showDefaultActions: false, | ||||
| }); | ||||
| 
 | ||||
| const [Modal, modalApi] = useVbenModal({ | ||||
|   async onConfirm() { | ||||
|     const { valid } = await formApi.validate(); | ||||
|     if (!valid) { | ||||
|       return; | ||||
|     } | ||||
|     modalApi.lock(); | ||||
|     // 提交表单 | ||||
|     const data = (await formApi.getValues()) as AiModelChatRoleApi.ChatRoleVO; | ||||
|     try { | ||||
|       await (formData.value?.id ? updateChatRole(data) : createChatRole(data)); | ||||
|       // 关闭并提示 | ||||
|       await modalApi.close(); | ||||
|       emit('success'); | ||||
|       message.success($t('ui.actionMessage.operationSuccess')); | ||||
|     } finally { | ||||
|       modalApi.unlock(); | ||||
|     } | ||||
|   }, | ||||
|   async onOpenChange(isOpen: boolean) { | ||||
|     if (!isOpen) { | ||||
|       formData.value = undefined; | ||||
|       return; | ||||
|     } | ||||
|     // 加载数据 | ||||
|     const data = modalApi.getData<AiModelChatRoleApi.ChatRoleVO>(); | ||||
|     if (!data || !data.id) { | ||||
|       await formApi.setValues(data); | ||||
|       return; | ||||
|     } | ||||
|     modalApi.lock(); | ||||
|     try { | ||||
|       formData.value = await getChatRole(data.id as number); | ||||
|       // 设置到 values | ||||
|       await formApi.setValues({ ...data, ...formData.value }); | ||||
|     } finally { | ||||
|       modalApi.unlock(); | ||||
|     } | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Modal class="w-[600px]" :title="getTitle"> | ||||
|     <Form class="mx-4" /> | ||||
|   </Modal> | ||||
| </template> | ||||
|  | @ -0,0 +1,248 @@ | |||
| import type { VbenFormSchema } from '#/adapter/form'; | ||||
| import type { VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| 
 | ||||
| import { z } from '#/adapter/form'; | ||||
| import { getApiKeySimpleList } from '#/api/ai/model/apiKey'; | ||||
| import { | ||||
|   AiModelTypeEnum, | ||||
|   CommonStatusEnum, | ||||
|   DICT_TYPE, | ||||
|   getDictOptions, | ||||
| } from '#/utils'; | ||||
| /** 新增/修改的表单 */ | ||||
| export function useFormSchema(): VbenFormSchema[] { | ||||
|   return [ | ||||
|     { | ||||
|       component: 'Input', | ||||
|       fieldName: 'id', | ||||
|       dependencies: { | ||||
|         triggerFields: [''], | ||||
|         show: () => false, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'platform', | ||||
|       label: '所属平台', | ||||
|       component: 'Select', | ||||
|       componentProps: { | ||||
|         placeholder: '请选择所属平台', | ||||
|         options: getDictOptions(DICT_TYPE.AI_PLATFORM, 'string'), | ||||
|         allowClear: true, | ||||
|       }, | ||||
|       rules: z.string().min(1, { message: '请输入平台' }), | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'type', | ||||
|       label: '模型类型', | ||||
|       component: 'Select', | ||||
|       componentProps: (values) => { | ||||
|         return { | ||||
|           placeholder: '请输入模型类型', | ||||
|           disabled: !!values.id, | ||||
|           options: getDictOptions(DICT_TYPE.AI_MODEL_TYPE, 'number'), | ||||
|           allowClear: true, | ||||
|         }; | ||||
|       }, | ||||
|       rules: 'required', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'keyId', | ||||
|       label: 'API 秘钥', | ||||
|       component: 'ApiSelect', | ||||
|       componentProps: { | ||||
|         placeholder: '请选择API 秘钥', | ||||
|         api: getApiKeySimpleList, | ||||
|         labelField: 'name', | ||||
|         valueField: 'id', | ||||
|         allowClear: true, | ||||
|       }, | ||||
|       rules: 'required', | ||||
|     }, | ||||
|     { | ||||
|       component: 'Input', | ||||
|       fieldName: 'name', | ||||
|       label: '模型名字', | ||||
|       rules: 'required', | ||||
|     }, | ||||
|     { | ||||
|       component: 'Input', | ||||
|       fieldName: 'model', | ||||
|       label: '模型标识', | ||||
|       rules: 'required', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'sort', | ||||
|       label: '模型排序', | ||||
|       component: 'InputNumber', | ||||
|       componentProps: { | ||||
|         controlsPosition: 'right', | ||||
|         placeholder: '请输入模型排序', | ||||
|         class: 'w-full', | ||||
|       }, | ||||
|       rules: 'required', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'status', | ||||
|       label: '开启状态', | ||||
|       component: 'RadioGroup', | ||||
|       componentProps: { | ||||
|         options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), | ||||
|         buttonStyle: 'solid', | ||||
|         optionType: 'button', | ||||
|       }, | ||||
|       rules: z.number().default(CommonStatusEnum.ENABLE), | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'temperature', | ||||
|       label: '温度参数', | ||||
|       component: 'InputNumber', | ||||
|       componentProps: { | ||||
|         controlsPosition: 'right', | ||||
|         placeholder: '请输入温度参数', | ||||
|         class: 'w-full', | ||||
|         min: 0, | ||||
|         max: 2, | ||||
|       }, | ||||
|       dependencies: { | ||||
|         triggerFields: ['type'], | ||||
|         show: (values) => { | ||||
|           return [AiModelTypeEnum.CHAT].includes(values.type); | ||||
|         }, | ||||
|       }, | ||||
|       rules: 'required', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'maxTokens', | ||||
|       label: '回复数 Token 数', | ||||
|       component: 'InputNumber', | ||||
|       componentProps: { | ||||
|         min: 0, | ||||
|         max: 8192, | ||||
|         controlsPosition: 'right', | ||||
|         placeholder: '请输入回复数 Token 数', | ||||
|         class: 'w-full', | ||||
|       }, | ||||
|       dependencies: { | ||||
|         triggerFields: ['type'], | ||||
|         show: (values) => { | ||||
|           return [AiModelTypeEnum.CHAT].includes(values.type); | ||||
|         }, | ||||
|       }, | ||||
|       rules: 'required', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'maxContexts', | ||||
|       label: '上下文数量', | ||||
|       component: 'InputNumber', | ||||
|       componentProps: { | ||||
|         min: 0, | ||||
|         max: 20, | ||||
|         controlsPosition: 'right', | ||||
|         placeholder: '请输入上下文数量', | ||||
|         class: 'w-full', | ||||
|       }, | ||||
|       dependencies: { | ||||
|         triggerFields: ['type'], | ||||
|         show: (values) => { | ||||
|           return [AiModelTypeEnum.CHAT].includes(values.type); | ||||
|         }, | ||||
|       }, | ||||
|       rules: 'required', | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
| 
 | ||||
| /** 列表的搜索表单 */ | ||||
| export function useGridFormSchema(): VbenFormSchema[] { | ||||
|   return [ | ||||
|     { | ||||
|       fieldName: 'name', | ||||
|       label: '模型名字', | ||||
|       component: 'Input', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'model', | ||||
|       label: '模型标识', | ||||
|       component: 'Input', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'platform', | ||||
|       label: '模型平台', | ||||
|       component: 'Input', | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
| 
 | ||||
| /** 列表的字段 */ | ||||
| export function useGridColumns(): VxeTableGridOptions['columns'] { | ||||
|   return [ | ||||
|     { | ||||
|       field: 'platform', | ||||
|       title: '所属平台', | ||||
|       cellRender: { | ||||
|         name: 'CellDict', | ||||
|         props: { type: DICT_TYPE.AI_PLATFORM }, | ||||
|       }, | ||||
|       minWidth: 100, | ||||
|     }, | ||||
|     { | ||||
|       field: 'type', | ||||
|       title: '模型类型', | ||||
|       cellRender: { | ||||
|         name: 'CellDict', | ||||
|         props: { type: DICT_TYPE.AI_MODEL_TYPE }, | ||||
|       }, | ||||
|       minWidth: 100, | ||||
|     }, | ||||
|     { | ||||
|       field: 'name', | ||||
|       title: '模型名字', | ||||
|       minWidth: 180, | ||||
|     }, | ||||
|     { | ||||
|       title: '模型标识', | ||||
|       field: 'model', | ||||
|       minWidth: 180, | ||||
|     }, | ||||
|     { | ||||
|       title: 'API 秘钥', | ||||
|       slots: { default: 'keyId' }, | ||||
|       minWidth: 140, | ||||
|     }, | ||||
|     { | ||||
|       title: '排序', | ||||
|       field: 'sort', | ||||
|       minWidth: 80, | ||||
|     }, | ||||
|     { | ||||
|       field: 'status', | ||||
|       title: '状态', | ||||
|       cellRender: { | ||||
|         name: 'CellDict', | ||||
|         props: { type: DICT_TYPE.COMMON_STATUS }, | ||||
|       }, | ||||
|       minWidth: 80, | ||||
|     }, | ||||
|     { | ||||
|       field: 'temperature', | ||||
|       title: '温度参数', | ||||
|       minWidth: 80, | ||||
|     }, | ||||
|     { | ||||
|       title: '回复数 Token 数', | ||||
|       field: 'maxTokens', | ||||
|       minWidth: 140, | ||||
|     }, | ||||
|     { | ||||
|       title: '上下文数量', | ||||
|       field: 'maxContexts', | ||||
|       minWidth: 100, | ||||
|     }, | ||||
|     { | ||||
|       title: '操作', | ||||
|       width: 130, | ||||
|       fixed: 'right', | ||||
|       slots: { default: 'actions' }, | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
|  | @ -1,31 +1,143 @@ | |||
| <script lang="ts" setup> | ||||
| import { Page } from '@vben/common-ui'; | ||||
| import type { VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| import type { AiModelApiKeyApi } from '#/api/ai/model/apiKey'; | ||||
| import type { AiModelModelApi } from '#/api/ai/model/model'; | ||||
| 
 | ||||
| import { Button } from 'ant-design-vue'; | ||||
| import { onMounted, ref } from 'vue'; | ||||
| 
 | ||||
| import { Page, useVbenModal } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { message } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; | ||||
| import { getApiKeySimpleList } from '#/api/ai/model/apiKey'; | ||||
| import { deleteModel, getModelPage } from '#/api/ai/model/model'; | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| import { $t } from '#/locales'; | ||||
| 
 | ||||
| import { useGridColumns, useGridFormSchema } from './data'; | ||||
| import Form from './modules/form.vue'; | ||||
| 
 | ||||
| const apiKeyList = ref([] as AiModelApiKeyApi.ApiKeyVO[]); | ||||
| const [FormModal, formModalApi] = useVbenModal({ | ||||
|   connectedComponent: Form, | ||||
|   destroyOnClose: true, | ||||
| }); | ||||
| 
 | ||||
| /** 刷新表格 */ | ||||
| function onRefresh() { | ||||
|   gridApi.query(); | ||||
| } | ||||
| 
 | ||||
| /** 创建 */ | ||||
| function handleCreate() { | ||||
|   formModalApi.setData(null).open(); | ||||
| } | ||||
| 
 | ||||
| /** 编辑 */ | ||||
| function handleEdit(row: AiModelModelApi.ModelVO) { | ||||
|   formModalApi.setData(row).open(); | ||||
| } | ||||
| 
 | ||||
| /** 删除 */ | ||||
| async function handleDelete(row: AiModelModelApi.ModelVO) { | ||||
|   const hideLoading = message.loading({ | ||||
|     content: $t('ui.actionMessage.deleting', [row.name]), | ||||
|     key: 'action_key_msg', | ||||
|   }); | ||||
|   try { | ||||
|     await deleteModel(row.id as number); | ||||
|     message.success({ | ||||
|       content: $t('ui.actionMessage.deleteSuccess', [row.name]), | ||||
|       key: 'action_key_msg', | ||||
|     }); | ||||
|     onRefresh(); | ||||
|   } finally { | ||||
|     hideLoading(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const [Grid, gridApi] = useVbenVxeGrid({ | ||||
|   formOptions: { | ||||
|     schema: useGridFormSchema(), | ||||
|   }, | ||||
|   gridOptions: { | ||||
|     columns: useGridColumns(), | ||||
|     height: 'auto', | ||||
|     keepSource: true, | ||||
|     proxyConfig: { | ||||
|       ajax: { | ||||
|         query: async ({ page }, formValues) => { | ||||
|           return await getModelPage({ | ||||
|             pageNo: page.currentPage, | ||||
|             pageSize: page.pageSize, | ||||
|             ...formValues, | ||||
|           }); | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     rowConfig: { | ||||
|       keyField: 'id', | ||||
|     }, | ||||
|     toolbarConfig: { | ||||
|       refresh: { code: 'query' }, | ||||
|       search: true, | ||||
|     }, | ||||
|   } as VxeTableGridOptions<AiModelModelApi.ModelVO>, | ||||
| }); | ||||
| onMounted(async () => { | ||||
|   // 获得下拉数据 | ||||
|   apiKeyList.value = await getApiKeySimpleList(); | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Page> | ||||
|   <Page auto-content-height> | ||||
|     <DocAlert title="AI 手册" url="https://doc.iocoder.cn/ai/build/" /> | ||||
|     <Button | ||||
|       danger | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3" | ||||
|     > | ||||
|       该功能支持 Vue3 + element-plus 版本! | ||||
|     </Button> | ||||
|     <br /> | ||||
|     <Button | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/model/model/index.vue" | ||||
|     > | ||||
|       可参考 | ||||
|       https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/model/model/index.vue | ||||
|       代码,pull request 贡献给我们! | ||||
|     </Button> | ||||
|     <FormModal @success="onRefresh" /> | ||||
|     <Grid table-title="模型配置列表"> | ||||
|       <template #toolbar-tools> | ||||
|         <TableAction | ||||
|           :actions="[ | ||||
|             { | ||||
|               label: $t('ui.actionTitle.create', ['模型配置']), | ||||
|               type: 'primary', | ||||
|               icon: ACTION_ICON.ADD, | ||||
|               auth: ['ai:model:create'], | ||||
|               onClick: handleCreate, | ||||
|             }, | ||||
|           ]" | ||||
|         /> | ||||
|       </template> | ||||
|       <template #keyId="{ row }"> | ||||
|         <span>{{ | ||||
|           apiKeyList.find((item) => item.id === row.keyId)?.name | ||||
|         }}</span> | ||||
|       </template> | ||||
|       <template #actions="{ row }"> | ||||
|         <TableAction | ||||
|           :actions="[ | ||||
|             { | ||||
|               label: $t('common.edit'), | ||||
|               type: 'link', | ||||
|               icon: ACTION_ICON.EDIT, | ||||
|               auth: ['ai:model:update'], | ||||
|               onClick: handleEdit.bind(null, row), | ||||
|             }, | ||||
|             { | ||||
|               label: $t('common.delete'), | ||||
|               type: 'link', | ||||
|               danger: true, | ||||
|               icon: ACTION_ICON.DELETE, | ||||
|               auth: ['ai:model:delete'], | ||||
|               popConfirm: { | ||||
|                 title: $t('ui.actionMessage.deleteConfirm', [row.name]), | ||||
|                 confirm: handleDelete.bind(null, row), | ||||
|               }, | ||||
|             }, | ||||
|           ]" | ||||
|         /> | ||||
|       </template> | ||||
|     </Grid> | ||||
|   </Page> | ||||
| </template> | ||||
|  |  | |||
|  | @ -0,0 +1,83 @@ | |||
| <script lang="ts" setup> | ||||
| import type { AiModelModelApi } from '#/api/ai/model/model'; | ||||
| 
 | ||||
| import { computed, ref } from 'vue'; | ||||
| 
 | ||||
| import { useVbenModal } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { message } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { useVbenForm } from '#/adapter/form'; | ||||
| import { createModel, getModel, updateModel } from '#/api/ai/model/model'; | ||||
| import {} from '#/api/bpm/model'; | ||||
| import { $t } from '#/locales'; | ||||
| 
 | ||||
| import { useFormSchema } from '../data'; | ||||
| 
 | ||||
| const emit = defineEmits(['success']); | ||||
| const formData = ref<AiModelModelApi.ModelVO>(); | ||||
| const getTitle = computed(() => { | ||||
|   return formData.value?.id | ||||
|     ? $t('ui.actionTitle.edit', ['模型配置']) | ||||
|     : $t('ui.actionTitle.create', ['模型配置']); | ||||
| }); | ||||
| 
 | ||||
| const [Form, formApi] = useVbenForm({ | ||||
|   commonConfig: { | ||||
|     componentProps: { | ||||
|       class: 'w-full', | ||||
|     }, | ||||
|     formItemClass: 'col-span-2', | ||||
|     labelWidth: 120, | ||||
|   }, | ||||
|   layout: 'horizontal', | ||||
|   schema: useFormSchema(), | ||||
|   showDefaultActions: false, | ||||
| }); | ||||
| 
 | ||||
| const [Modal, modalApi] = useVbenModal({ | ||||
|   async onConfirm() { | ||||
|     const { valid } = await formApi.validate(); | ||||
|     if (!valid) { | ||||
|       return; | ||||
|     } | ||||
|     modalApi.lock(); | ||||
|     // 提交表单 | ||||
|     const data = (await formApi.getValues()) as AiModelModelApi.ModelVO; | ||||
|     try { | ||||
|       await (formData.value?.id ? updateModel(data) : createModel(data)); | ||||
|       // 关闭并提示 | ||||
|       await modalApi.close(); | ||||
|       emit('success'); | ||||
|       message.success($t('ui.actionMessage.operationSuccess')); | ||||
|     } finally { | ||||
|       modalApi.unlock(); | ||||
|     } | ||||
|   }, | ||||
|   async onOpenChange(isOpen: boolean) { | ||||
|     if (!isOpen) { | ||||
|       formData.value = undefined; | ||||
|       return; | ||||
|     } | ||||
|     // 加载数据 | ||||
|     const data = modalApi.getData<AiModelModelApi.ModelVO>(); | ||||
|     if (!data || !data.id) { | ||||
|       return; | ||||
|     } | ||||
|     modalApi.lock(); | ||||
|     try { | ||||
|       formData.value = await getModel(data.id as number); | ||||
|       // 设置到 values | ||||
|       await formApi.setValues(formData.value); | ||||
|     } finally { | ||||
|       modalApi.unlock(); | ||||
|     } | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Modal class="w-[600px]" :title="getTitle"> | ||||
|     <Form class="mx-4" /> | ||||
|   </Modal> | ||||
| </template> | ||||
|  | @ -0,0 +1,111 @@ | |||
| import type { VbenFormSchema } from '#/adapter/form'; | ||||
| import type { VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| 
 | ||||
| import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils'; | ||||
| /** 新增/修改的表单 */ | ||||
| export function useFormSchema(): VbenFormSchema[] { | ||||
|   return [ | ||||
|     { | ||||
|       component: 'Input', | ||||
|       fieldName: 'id', | ||||
|       dependencies: { | ||||
|         triggerFields: [''], | ||||
|         show: () => false, | ||||
|       }, | ||||
|     }, | ||||
| 
 | ||||
|     { | ||||
|       component: 'Input', | ||||
|       fieldName: 'name', | ||||
|       label: '工具名称', | ||||
|       rules: 'required', | ||||
|     }, | ||||
|     { | ||||
|       component: 'Textarea', | ||||
|       fieldName: 'description', | ||||
|       label: '工具描述', | ||||
|       componentProps: { | ||||
|         placeholder: '请输入工具描述', | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'status', | ||||
|       label: '状态', | ||||
|       component: 'RadioGroup', | ||||
|       componentProps: { | ||||
|         options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), | ||||
|         buttonStyle: 'solid', | ||||
|         optionType: 'button', | ||||
|       }, | ||||
|       defaultValue: CommonStatusEnum.ENABLE, | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
| 
 | ||||
| /** 列表的搜索表单 */ | ||||
| export function useGridFormSchema(): VbenFormSchema[] { | ||||
|   return [ | ||||
|     { | ||||
|       fieldName: 'name', | ||||
|       label: '工具名称', | ||||
|       component: 'Input', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'status', | ||||
|       label: '状态', | ||||
|       component: 'Select', | ||||
|       componentProps: { | ||||
|         allowClear: true, | ||||
|         options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'createTime', | ||||
|       label: '创建时间', | ||||
|       component: 'RangePicker', | ||||
|       componentProps: { | ||||
|         placeholder: ['开始时间', '结束时间'], | ||||
|         valueFormat: 'YYYY-MM-DD HH:mm:ss', | ||||
|         allowClear: true, | ||||
|       }, | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
| 
 | ||||
| /** 列表的字段 */ | ||||
| export function useGridColumns(): VxeTableGridOptions['columns'] { | ||||
|   return [ | ||||
|     { | ||||
|       field: 'id', | ||||
|       title: '工具编号', | ||||
|     }, | ||||
|     { | ||||
|       field: 'name', | ||||
|       title: '工具名称', | ||||
|     }, | ||||
|     { | ||||
|       field: 'description', | ||||
|       title: '工具描述', | ||||
|     }, | ||||
|     { | ||||
|       field: 'status', | ||||
|       title: '状态', | ||||
|       cellRender: { | ||||
|         name: 'CellDict', | ||||
|         props: { type: DICT_TYPE.COMMON_STATUS }, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       field: 'createTime', | ||||
|       title: '创建时间', | ||||
|       minWidth: 180, | ||||
|       formatter: 'formatDateTime', | ||||
|     }, | ||||
|     { | ||||
|       title: '操作', | ||||
|       width: 130, | ||||
|       fixed: 'right', | ||||
|       slots: { default: 'actions' }, | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
|  | @ -1,34 +1,132 @@ | |||
| <script lang="ts" setup> | ||||
| import { Page } from '@vben/common-ui'; | ||||
| import type { VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| import type { AiModelToolApi } from '#/api/ai/model/tool'; | ||||
| 
 | ||||
| import { Button } from 'ant-design-vue'; | ||||
| import { Page, useVbenModal } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { message } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; | ||||
| import { deleteTool, getToolPage } from '#/api/ai/model/tool'; | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| import { $t } from '#/locales'; | ||||
| 
 | ||||
| import { useGridColumns, useGridFormSchema } from './data'; | ||||
| import Form from './modules/form.vue'; | ||||
| 
 | ||||
| const [FormModal, formModalApi] = useVbenModal({ | ||||
|   connectedComponent: Form, | ||||
|   destroyOnClose: true, | ||||
| }); | ||||
| 
 | ||||
| /** 刷新表格 */ | ||||
| function onRefresh() { | ||||
|   gridApi.query(); | ||||
| } | ||||
| 
 | ||||
| /** 创建 */ | ||||
| function handleCreate() { | ||||
|   formModalApi.setData(null).open(); | ||||
| } | ||||
| 
 | ||||
| /** 编辑 */ | ||||
| function handleEdit(row: AiModelToolApi.ToolVO) { | ||||
|   formModalApi.setData(row).open(); | ||||
| } | ||||
| 
 | ||||
| /** 删除 */ | ||||
| async function handleDelete(row: AiModelToolApi.ToolVO) { | ||||
|   const hideLoading = message.loading({ | ||||
|     content: $t('ui.actionMessage.deleting', [row.name]), | ||||
|     key: 'action_key_msg', | ||||
|   }); | ||||
|   try { | ||||
|     await deleteTool(row.id as number); | ||||
|     message.success({ | ||||
|       content: $t('ui.actionMessage.deleteSuccess', [row.name]), | ||||
|       key: 'action_key_msg', | ||||
|     }); | ||||
|     onRefresh(); | ||||
|   } finally { | ||||
|     hideLoading(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const [Grid, gridApi] = useVbenVxeGrid({ | ||||
|   formOptions: { | ||||
|     schema: useGridFormSchema(), | ||||
|   }, | ||||
|   gridOptions: { | ||||
|     columns: useGridColumns(), | ||||
|     height: 'auto', | ||||
|     keepSource: true, | ||||
|     proxyConfig: { | ||||
|       ajax: { | ||||
|         query: async ({ page }, formValues) => { | ||||
|           return await getToolPage({ | ||||
|             pageNo: page.currentPage, | ||||
|             pageSize: page.pageSize, | ||||
|             ...formValues, | ||||
|           }); | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     rowConfig: { | ||||
|       keyField: 'id', | ||||
|     }, | ||||
|     toolbarConfig: { | ||||
|       refresh: { code: 'query' }, | ||||
|       search: true, | ||||
|     }, | ||||
|   } as VxeTableGridOptions<AiModelToolApi.ToolVO>, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Page> | ||||
|   <Page auto-content-height> | ||||
|     <DocAlert | ||||
|       title="AI 工具调用(function calling)" | ||||
|       url="https://doc.iocoder.cn/ai/tool/" | ||||
|     /> | ||||
|     <Button | ||||
|       danger | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3" | ||||
|     > | ||||
|       该功能支持 Vue3 + element-plus 版本! | ||||
|     </Button> | ||||
|     <br /> | ||||
|     <Button | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/model/tool/index.vue" | ||||
|     > | ||||
|       可参考 | ||||
|       https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/model/tool/index.vue | ||||
|       代码,pull request 贡献给我们! | ||||
|     </Button> | ||||
|     <FormModal @success="onRefresh" /> | ||||
|     <Grid table-title="工具列表"> | ||||
|       <template #toolbar-tools> | ||||
|         <TableAction | ||||
|           :actions="[ | ||||
|             { | ||||
|               label: $t('ui.actionTitle.create', ['工具']), | ||||
|               type: 'primary', | ||||
|               icon: ACTION_ICON.ADD, | ||||
|               auth: ['ai:tool:create'], | ||||
|               onClick: handleCreate, | ||||
|             }, | ||||
|           ]" | ||||
|         /> | ||||
|       </template> | ||||
|       <template #actions="{ row }"> | ||||
|         <TableAction | ||||
|           :actions="[ | ||||
|             { | ||||
|               label: $t('common.edit'), | ||||
|               type: 'link', | ||||
|               icon: ACTION_ICON.EDIT, | ||||
|               auth: ['ai:tool:update'], | ||||
|               onClick: handleEdit.bind(null, row), | ||||
|             }, | ||||
|             { | ||||
|               label: $t('common.delete'), | ||||
|               type: 'link', | ||||
|               danger: true, | ||||
|               icon: ACTION_ICON.DELETE, | ||||
|               auth: ['ai:tool:delete'], | ||||
|               popConfirm: { | ||||
|                 title: $t('ui.actionMessage.deleteConfirm', [row.name]), | ||||
|                 confirm: handleDelete.bind(null, row), | ||||
|               }, | ||||
|             }, | ||||
|           ]" | ||||
|         /> | ||||
|       </template> | ||||
|     </Grid> | ||||
|   </Page> | ||||
| </template> | ||||
|  |  | |||
|  | @ -0,0 +1,82 @@ | |||
| <script lang="ts" setup> | ||||
| import type { AiModelToolApi } from '#/api/ai/model/tool'; | ||||
| 
 | ||||
| import { computed, ref } from 'vue'; | ||||
| 
 | ||||
| import { useVbenModal } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { message } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { useVbenForm } from '#/adapter/form'; | ||||
| import { createTool, getTool, updateTool } from '#/api/ai/model/tool'; | ||||
| import { $t } from '#/locales'; | ||||
| 
 | ||||
| import { useFormSchema } from '../data'; | ||||
| 
 | ||||
| const emit = defineEmits(['success']); | ||||
| const formData = ref<AiModelToolApi.ToolVO>(); | ||||
| const getTitle = computed(() => { | ||||
|   return formData.value?.id | ||||
|     ? $t('ui.actionTitle.edit', ['工具']) | ||||
|     : $t('ui.actionTitle.create', ['工具']); | ||||
| }); | ||||
| 
 | ||||
| const [Form, formApi] = useVbenForm({ | ||||
|   commonConfig: { | ||||
|     componentProps: { | ||||
|       class: 'w-full', | ||||
|     }, | ||||
|     formItemClass: 'col-span-2', | ||||
|     labelWidth: 100, | ||||
|   }, | ||||
|   layout: 'horizontal', | ||||
|   schema: useFormSchema(), | ||||
|   showDefaultActions: false, | ||||
| }); | ||||
| 
 | ||||
| const [Modal, modalApi] = useVbenModal({ | ||||
|   async onConfirm() { | ||||
|     const { valid } = await formApi.validate(); | ||||
|     if (!valid) { | ||||
|       return; | ||||
|     } | ||||
|     modalApi.lock(); | ||||
|     // 提交表单 | ||||
|     const data = (await formApi.getValues()) as AiModelToolApi.ToolVO; | ||||
|     try { | ||||
|       await (formData.value?.id ? updateTool(data) : createTool(data)); | ||||
|       // 关闭并提示 | ||||
|       await modalApi.close(); | ||||
|       emit('success'); | ||||
|       message.success($t('ui.actionMessage.operationSuccess')); | ||||
|     } finally { | ||||
|       modalApi.unlock(); | ||||
|     } | ||||
|   }, | ||||
|   async onOpenChange(isOpen: boolean) { | ||||
|     if (!isOpen) { | ||||
|       formData.value = undefined; | ||||
|       return; | ||||
|     } | ||||
|     // 加载数据 | ||||
|     const data = modalApi.getData<AiModelToolApi.ToolVO>(); | ||||
|     if (!data || !data.id) { | ||||
|       return; | ||||
|     } | ||||
|     modalApi.lock(); | ||||
|     try { | ||||
|       formData.value = await getTool(data.id as number); | ||||
|       // 设置到 values | ||||
|       await formApi.setValues(formData.value); | ||||
|     } finally { | ||||
|       modalApi.unlock(); | ||||
|     } | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Modal class="w-[600px]" :title="getTitle"> | ||||
|     <Form class="mx-4" /> | ||||
|   </Modal> | ||||
| </template> | ||||
|  | @ -20,6 +20,7 @@ | |||
|     } | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@microsoft/fetch-event-source": "^2.0.1", | ||||
|     "@vben/locales": "workspace:*", | ||||
|     "@vben/utils": "workspace:*", | ||||
|     "axios": "catalog:", | ||||
|  |  | |||
|  | @ -1,2 +1,3 @@ | |||
| export * from './request-client'; | ||||
| export * from '@microsoft/fetch-event-source'; | ||||
| export * from 'axios'; | ||||
|  |  | |||
|  | @ -1824,6 +1824,9 @@ importers: | |||
| 
 | ||||
|   packages/effects/request: | ||||
|     dependencies: | ||||
|       '@microsoft/fetch-event-source': | ||||
|         specifier: ^2.0.1 | ||||
|         version: 2.0.1 | ||||
|       '@vben/locales': | ||||
|         specifier: workspace:* | ||||
|         version: link:../../locales | ||||
|  | @ -3915,6 +3918,9 @@ packages: | |||
|     resolution: {integrity: sha512-cszYIcjiNscDoMB1CIKZ3My61+JOhpERGlGr54i6bocvGLrcL/wo9o+RNXMBrb7XgLtKaizZWUpqRduQuHQLdg==} | ||||
|     hasBin: true | ||||
| 
 | ||||
|   '@microsoft/fetch-event-source@2.0.1': | ||||
|     resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==} | ||||
| 
 | ||||
|   '@microsoft/tsdoc-config@0.17.1': | ||||
|     resolution: {integrity: sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==} | ||||
| 
 | ||||
|  | @ -13780,6 +13786,8 @@ snapshots: | |||
|     transitivePeerDependencies: | ||||
|       - '@types/node' | ||||
| 
 | ||||
|   '@microsoft/fetch-event-source@2.0.1': {} | ||||
| 
 | ||||
|   '@microsoft/tsdoc-config@0.17.1': | ||||
|     dependencies: | ||||
|       '@microsoft/tsdoc': 0.15.1 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 gjd
						gjd