【代码评审】AI:review 聊天对话的实现
							parent
							
								
									4c259cd60f
								
							
						
					
					
						commit
						46eb89695d
					
				|  | @ -1,4 +1,3 @@ | |||
| 
 | ||||
| <template> | ||||
|   <div class="chat-empty"> | ||||
| 
 | ||||
|  | @ -15,7 +14,6 @@ | |||
| </template> | ||||
| <script setup lang="ts"> | ||||
| 
 | ||||
| const router = useRouter() | ||||
| const promptList = ref<any[]>() // 角色列表 | ||||
| promptList.value = [ | ||||
|   { | ||||
|  | @ -31,9 +29,6 @@ const emits = defineEmits(['onPrompt']) | |||
| const handlerPromptClick = async ({ prompt }) => { | ||||
|   emits('onPrompt', prompt) | ||||
| } | ||||
| 
 | ||||
| onMounted(async () => { | ||||
| }) | ||||
| </script> | ||||
| <style scoped lang="scss"> | ||||
| .chat-empty { | ||||
|  |  | |||
|  | @ -27,7 +27,6 @@ | |||
| 
 | ||||
|         <el-empty v-if="loading" description="." :v-loading="loading" /> | ||||
| 
 | ||||
|         <!-- TODO done @fain:置顶、聊天记录、一星期钱、30天前,前端对数据重新做一下分组,或者后端接口改一下 --> | ||||
|         <div v-for="conversationKey in Object.keys(conversationMap)" :key="conversationKey"> | ||||
|           <div class="conversation-item classify-title" v-if="conversationMap[conversationKey].length"> | ||||
|             <el-text class="mx-1" size="small" tag="b">{{ conversationKey }}</el-text> | ||||
|  | @ -47,7 +46,6 @@ | |||
|                 <img class="avatar" :src="conversation.roleAvatar"/> | ||||
|                 <span class="title">{{ conversation.title }}</span> | ||||
|               </div> | ||||
|               <!-- TODO done @fan:缺一个【置顶】按钮,效果改成 hover 上去展示 --> | ||||
|               <div class="button-wrapper" v-show="hoverConversationId === conversation.id"> | ||||
|                 <el-button class="btn" link @click.stop="handlerTop(conversation)" > | ||||
|                   <el-icon title="置顶" v-if="!conversation.pinned"><Top /></el-icon> | ||||
|  | @ -74,6 +72,7 @@ | |||
|     </div> | ||||
| 
 | ||||
|     <!-- 左底部:工具栏 --> | ||||
|     <!-- TODO @fan:下面两个 icon,可以使用类似 <Icon icon="ep:question-filled" /> 替代哈 --> | ||||
|     <div class="tool-box"> | ||||
|       <div @click="handleRoleRepository"> | ||||
|         <Icon icon="ep:user"/> | ||||
|  | @ -87,7 +86,7 @@ | |||
| 
 | ||||
|     <!-- ============= 额外组件 ============= --> | ||||
| 
 | ||||
|     <!--   角色仓库抽屉  --> | ||||
|     <!-- 角色仓库抽屉 --> | ||||
|     <el-drawer v-model="drawer" title="角色仓库" size="754px"> | ||||
|       <Role/> | ||||
|     </el-drawer> | ||||
|  | @ -109,7 +108,7 @@ const activeConversationId = ref<string | null>(null) // 选中的对话,默 | |||
| const hoverConversationId = ref<string | null>(null) // 悬浮上去的对话 | ||||
| const conversationList = ref([] as ChatConversationVO[])  // 对话列表 | ||||
| const conversationMap = ref<any>({})  // 对话分组 (置顶、今天、三天前、一星期前、一个月前) | ||||
| const drawer = ref<boolean>(false) // 角色仓库抽屉 | ||||
| const drawer = ref<boolean>(false) // 角色仓库抽屉 TODO @fan:roleDrawer 会不会好点哈 | ||||
| const loading = ref<boolean>(false) // 加载中 | ||||
| const loadingTime = ref<any>() // 加载中定时器 | ||||
| 
 | ||||
|  | @ -154,6 +153,7 @@ const handleConversationClick = async (id: string) => { | |||
|     return item.id === id | ||||
|   }) | ||||
|   // 回调 onConversationClick | ||||
|   // TODO @fan: 这里 idea 会报黄色警告,有办法解下么? | ||||
|   const res = emits('onConversationClick', filterConversation[0]) | ||||
|   // 切换对话 | ||||
|   if (res) { | ||||
|  | @ -166,18 +166,18 @@ const handleConversationClick = async (id: string) => { | |||
|  */ | ||||
| const getChatConversationList = async () => { | ||||
|   try { | ||||
|     // 0、加载中 | ||||
|     // 0. 加载中 | ||||
|     loadingTime.value = setTimeout(() => { | ||||
|       loading.value = true | ||||
|     }, 50) | ||||
|     // 1、获取 对话数据 | ||||
|     // 1. 获取 对话数据 | ||||
|     const res = await ChatConversationApi.getChatConversationMyList() | ||||
|     // 2、排序 | ||||
|     // 2. 排序 | ||||
|     res.sort((a, b) => { | ||||
|       return b.createTime - a.createTime | ||||
|     }) | ||||
|     conversationList.value = res | ||||
|     // 3、默认选中 | ||||
|     // 3. 默认选中 | ||||
|     if (!activeId?.value) { | ||||
|       // await handleConversationClick(res[0].id) | ||||
|     } else { | ||||
|  | @ -189,13 +189,13 @@ const getChatConversationList = async () => { | |||
|       //   await handleConversationClick(res[0].id) | ||||
|       // } | ||||
|     } | ||||
|     // 4、没有 任何对话情况 | ||||
|     // 4. 没有任何对话情况 | ||||
|     if (conversationList.value.length === 0) { | ||||
|       activeConversationId.value = null | ||||
|       conversationMap.value = {} | ||||
|       return | ||||
|     } | ||||
|     // 5、对话根据时间分组(置顶、今天、一天前、三天前、七天前、30天前) | ||||
|     // 5. 对话根据时间分组(置顶、今天、一天前、三天前、七天前、30天前) | ||||
|     conversationMap.value = await conversationTimeGroup(conversationList.value) | ||||
|   } finally { | ||||
|     // 清理定时器 | ||||
|  | @ -253,15 +253,15 @@ const conversationTimeGroup = async (list: ChatConversationVO[]) => { | |||
|  * 对话 - 新建 | ||||
|  */ | ||||
| const createConversation = async () => { | ||||
|   // 1、新建对话 | ||||
|   // 1. 新建对话 | ||||
|   const conversationId = await ChatConversationApi.createChatConversationMy( | ||||
|     {} as unknown as ChatConversationVO | ||||
|   ) | ||||
|   // 2、获取对话内容 | ||||
|   // 2. 获取对话内容 | ||||
|   await getChatConversationList() | ||||
|   // 3、选中对话 | ||||
|   // 3. 选中对话 | ||||
|   await handleConversationClick(conversationId) | ||||
|   // 4、回调 | ||||
|   // 4. 回调 | ||||
|   emits('onConversationCreate') | ||||
| } | ||||
| 
 | ||||
|  | @ -269,21 +269,21 @@ const createConversation = async () => { | |||
|  * 对话 - 更新标题 | ||||
|  */ | ||||
| const updateConversationTitle = async (conversation: ChatConversationVO) => { | ||||
|   // 1、二次确认 | ||||
|   // 1. 二次确认 | ||||
|   const {value} = await ElMessageBox.prompt('修改标题', { | ||||
|     inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 判断非空,且非空格 | ||||
|     inputErrorMessage: '标题不能为空', | ||||
|     inputValue: conversation.title | ||||
|   }) | ||||
|   // 2、发起修改 | ||||
|   // 2. 发起修改 | ||||
|   await ChatConversationApi.updateChatConversationMy({ | ||||
|     id: conversation.id, | ||||
|     title: value | ||||
|   } as ChatConversationVO) | ||||
|   message.success('重命名成功') | ||||
|   // 刷新列表 | ||||
|   // 3. 刷新列表 | ||||
|   await getChatConversationList() | ||||
|   // 过滤当前切换的 | ||||
|   // 4. 过滤当前切换的 | ||||
|   const filterConversationList = conversationList.value.filter(item => { | ||||
|     return item.id === conversation.id | ||||
|   }) | ||||
|  | @ -316,6 +316,7 @@ const deleteChatConversation = async (conversation: ChatConversationVO) => { | |||
| /** | ||||
|  * 对话置顶 | ||||
|  */ | ||||
| // TODO @fan:应该是 handleXXX,handler 是名词哈 | ||||
| const handlerTop = async (conversation: ChatConversationVO) => { | ||||
|   // 更新对话置顶 | ||||
|   conversation.pinned = !conversation.pinned | ||||
|  | @ -324,9 +325,9 @@ const handlerTop = async (conversation: ChatConversationVO) => { | |||
|   await getChatConversationList() | ||||
| } | ||||
| 
 | ||||
| // TODO @fan:类似 ============ 分块的,最后后面也有 ============ 哈 | ||||
| // ============ 角色仓库 | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * 角色仓库抽屉 | ||||
|  */ | ||||
|  | @ -336,11 +337,11 @@ const handleRoleRepository = async () => { | |||
| 
 | ||||
| // ============= 清空对话 | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * 清空对话 | ||||
|  */ | ||||
| const handleClearConversation = async () => { | ||||
|   // TODO @fan:可以使用 await message.confirm( 简化,然后使用 await 改成同步的逻辑,会更简洁 | ||||
|   ElMessageBox.confirm( | ||||
|     '确认后对话会全部清空,置顶的对话除外。', | ||||
|     '确认提示', | ||||
|  |  | |||
|  | @ -57,7 +57,7 @@ | |||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|   <!--  回到底部  --> | ||||
|   <!-- 回到底部 --> | ||||
|   <div v-if="isScrolling" class="to-bottom" @click="handleGoBottom"> | ||||
|     <el-button :icon="ArrowDownBold" circle /> | ||||
|   </div> | ||||
|  | @ -106,6 +106,7 @@ const messageList = computed(() => { | |||
| 
 | ||||
| const scrollToBottom = async (isIgnore?: boolean) => { | ||||
|   await nextTick(() => { | ||||
|     // TODO @fan:中文写作习惯,中英文之间要有空格;另外,nextick 哈,idea 如果有绿色波兰线,可以关注下 | ||||
|     //注意要使用nexttick以免获取不到dom | ||||
|     if (isIgnore || !isScrolling.value) { | ||||
|       messageContainer.value.scrollTop = | ||||
|  | @ -184,6 +185,7 @@ const handlerGoTop = async () => { | |||
| } | ||||
| 
 | ||||
| // 监听 list | ||||
| // TODO @fan:这个木有,是不是删除啦 | ||||
| const { list, conversationId } = toRefs(props) | ||||
| watch(list, async (newValue, oldValue) => { | ||||
|   console.log('watch list', list) | ||||
|  |  | |||
|  | @ -45,6 +45,4 @@ defineProps({ | |||
|     flex-direction: row; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| </style> | ||||
|  |  | |||
|  | @ -10,27 +10,27 @@ | |||
|     /> | ||||
|     <!-- 右侧:对话详情 --> | ||||
|     <el-container class="detail-container"> | ||||
|       <!-- 右顶部 TODO 芋艿:右对齐 --> | ||||
|       <el-header class="header"> | ||||
|         <div class="title"> | ||||
|           {{ activeConversation?.title ? activeConversation?.title : '对话' }} | ||||
|           <span v-if="list.length">({{list.length}})</span> | ||||
|         </div> | ||||
|         <div class="btns" v-if="activeConversation"> | ||||
|           <!-- TODO @fan:样式改下;这里我已经改成点击后,弹出了 --> | ||||
|           <el-button type="primary" bg text="plain" size="small" @click="openChatConversationUpdateForm"> | ||||
|           <el-button type="primary" bg plain size="small" @click="openChatConversationUpdateForm"> | ||||
|             <span v-html="activeConversation?.modelName"></span> | ||||
|             <Icon icon="ep:setting" style="margin-left: 10px"/> | ||||
|           </el-button> | ||||
|           <el-button size="small" class="btn" @click="handlerMessageClear"> | ||||
|             <!-- TODO @fan:style 部分,可以考虑用 unocss 替代 --> | ||||
|             <img src="@/assets/ai/clear.svg" style="height: 14px;" /> | ||||
|           </el-button> | ||||
|           <!-- TODO @fan:下面两个 icon,可以使用类似 <Icon icon="ep:question-filled" /> 替代哈 --> | ||||
|           <el-button size="small" :icon="Download" class="btn"  /> | ||||
|           <el-button size="small" :icon="Top" class="btn"  @click="handlerGoTop" /> | ||||
|         </div> | ||||
|       </el-header> | ||||
| 
 | ||||
|       <!-- main --> | ||||
|       <!-- main:消息列表 --> | ||||
|       <el-main class="main-container" > | ||||
|         <div > | ||||
|           <div class="message-container" > | ||||
|  | @ -87,7 +87,7 @@ | |||
|     </el-container> | ||||
| 
 | ||||
|     <!--  ========= 额外组件 ==========  --> | ||||
|     <!-- 更新对话 form --> | ||||
|     <!-- 更新对话 Form --> | ||||
|     <ChatConversationUpdateForm | ||||
|       ref="chatConversationUpdateFormRef" | ||||
|       @success="handlerTitleSuccess" | ||||
|  | @ -96,6 +96,7 @@ | |||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| // TODO @fan:是不是把 index.vue 相关的,在这里新建一个 index 目录,然后挪进去哈。因为 /ai/chat 还会有其它功能。例如说,现在的 /ai/chat/manager 管理 | ||||
| import Conversation from './Conversation.vue' | ||||
| import Message from './Message.vue' | ||||
| import ChatEmpty from './ChatEmpty.vue' | ||||
|  | @ -120,15 +121,17 @@ const prompt = ref<string>() // prompt | |||
| const userInfo = ref<ProfileVO>() // 用户信息 | ||||
| const enableContext = ref<boolean>(true) // 是否开启上下文 | ||||
| 
 | ||||
| // TODO @fan:这几个变量,可以注释在补下哈;另外,fullText 可以明确是生成中的消息 Text,这样更容易理解哈; | ||||
| const fullText = ref(''); | ||||
| const displayedText = ref(''); | ||||
| const textSpeed = ref<number>(50); // Typing speed in milliseconds | ||||
| const textRoleRunning = ref<boolean>(false); // Typing speed in milliseconds | ||||
| 
 | ||||
| // chat message 列表 | ||||
| // TODO @fan:list、listLoading、listLoadingTime 不能体现出来是消息列表,是不是可以变量再优化下 | ||||
| const list = ref<ChatMessageVO[]>([]) // 列表的数据 | ||||
| const listLoading = ref<boolean>(false) // 是否加载中 | ||||
| const listLoadingTime = ref<any>() // time定时器,如果加载速度很快,就不进入加载中 | ||||
| const listLoadingTime = ref<any>() // time 定时器,如果加载速度很快,就不进入加载中 | ||||
| 
 | ||||
| // 判断 消息列表 滚动的位置(用于判断是否需要滚动到消息最下方) | ||||
| const messageRef = ref() | ||||
|  | @ -140,6 +143,7 @@ const defaultRoleAvatar = 'http://test.yudao.iocoder.cn/eaef5f41acb911dd718429a0 | |||
| 
 | ||||
| // =========== 自提滚动效果 | ||||
| 
 | ||||
| // TODO @fan:这个方法,要不加个方法注释 | ||||
| const textRoll = async () => { | ||||
|   let index = 0; | ||||
|   try { | ||||
|  | @ -162,7 +166,7 @@ const textRoll = async () => { | |||
|       } else { | ||||
|         textSpeed.value = 100 | ||||
|       } | ||||
|       // 对话结束,就按30的速度 | ||||
|       // 对话结束,就按 30 的速度 | ||||
|       if (!conversationInProgress.value) { | ||||
|         textSpeed.value = 10 | ||||
|       } | ||||
|  | @ -176,6 +180,7 @@ const textRoll = async () => { | |||
|         // 更新 message | ||||
|         const lastMessage = list.value[list.value.length - 1] | ||||
|         lastMessage.content = displayedText.value | ||||
|         // TODO @fan:ist.value?,还是 ist.value.length 哈? | ||||
|         list.value[list.value - 1] = lastMessage | ||||
|         // 滚动到住下面 | ||||
|         await scrollToBottom() | ||||
|  | @ -212,6 +217,7 @@ function scrollToBottom(isIgnore?: boolean) { | |||
| 
 | ||||
| // ============= 处理聊天输入回车发送 ============= | ||||
| 
 | ||||
| // TODO @fan:是不是可以通过 @keydown.enter、@keydown.shift.enter 来实现,回车发送、shift+回车换行;主要看看,是不是可以简化 isComposing 相关的逻辑 | ||||
| const onCompositionstart = () => { | ||||
|   isComposing.value = true | ||||
| } | ||||
|  | @ -276,12 +282,14 @@ const onSendBtn = async () => { | |||
| 
 | ||||
| const doSend = async (content: string) => { | ||||
|   if (content.length < 2) { | ||||
|     // TODO @fan:这个 message.error(`上传文件大小不能超过${props.fileSize}MB!`) 可以替代,这种形式 | ||||
|     ElMessage({ | ||||
|       message: '请输入内容!', | ||||
|       type: 'error' | ||||
|     }) | ||||
|     return | ||||
|   } | ||||
|   // TODO @fan:这个 message.error(`上传文件大小不能超过${props.fileSize}MB!`) 可以替代,这种形式 | ||||
|   if (activeConversationId.value == null) { | ||||
|     ElMessage({ | ||||
|       message: '还没创建对话,不能发送!', | ||||
|  | @ -289,9 +297,9 @@ const doSend = async (content: string) => { | |||
|     }) | ||||
|     return | ||||
|   } | ||||
|   // TODO 芋艿:这块交互要在优化;应该是先插入到 UI 界面,里面会有当前的消息,和正在思考中;之后发起请求; | ||||
|   // 清空输入框 | ||||
|   prompt.value = '' | ||||
|   // TODO @fan:idea 这里会报类型错误,是不是可以解决下哈 | ||||
|   const userMessage = { | ||||
|     conversationId: activeConversationId.value, | ||||
|     content: content | ||||
|  | @ -309,6 +317,7 @@ const doSendStream = async (userMessage: ChatMessageVO) => { | |||
|   fullText.value = '' | ||||
|   try { | ||||
|     // 先添加两个假数据,等 stream 返回再替换 | ||||
|     // TODO @fan:idea 这里会报类型错误,是不是可以解决下哈 | ||||
|     list.value.push({ | ||||
|       id: -1, | ||||
|       conversationId: activeConversationId.value, | ||||
|  | @ -326,13 +335,14 @@ const doSendStream = async (userMessage: ChatMessageVO) => { | |||
|       createTime: new Date() | ||||
|     } as ChatMessageVO) | ||||
|     // 滚动到最下面 | ||||
|     // TODO @fan:可以 await nextTick();然后同步调用 scrollToBottom() | ||||
|     nextTick(async () => { | ||||
|       await scrollToBottom() | ||||
|     }) | ||||
|     // 开始滚动 | ||||
|     textRoll() | ||||
|     // 发送 event stream | ||||
|     let isFirstMessage = true | ||||
|     let isFirstMessage = true // TODO @fan:isFirstChunk 会更精准 | ||||
|     ChatMessageApi.sendStream( | ||||
|       userMessage.conversationId, // TODO 芋艿:这里可能要在优化; | ||||
|       userMessage.content, | ||||
|  | @ -367,12 +377,14 @@ const doSendStream = async (userMessage: ChatMessageVO) => { | |||
|       }, | ||||
|       (error) => { | ||||
|         message.alert(`对话异常! ${error}`) | ||||
|         // TODO @fan:是不是可以复用 stopStream 方法 | ||||
|         // 标记对话结束 | ||||
|         conversationInProgress.value = false | ||||
|         // 结束 stream 对话 | ||||
|         conversationInAbortController.value.abort() | ||||
|       }, | ||||
|       () => { | ||||
|         // TODO @fan:是不是可以复用 stopStream 方法 | ||||
|         // 标记对话结束 | ||||
|         conversationInProgress.value = false | ||||
|         // 结束 stream 对话 | ||||
|  | @ -412,6 +424,7 @@ const messageList = computed(() => { | |||
|   return [] | ||||
| }) | ||||
| 
 | ||||
| // TODO @fan:一般情况下,项目方法注释用 /** */,啊哈,主要保持风格统一,= = 少占点行哈, | ||||
| /** | ||||
|  * 获取 - message 列表 | ||||
|  */ | ||||
|  | @ -500,6 +513,7 @@ const handleConversationClick = async (conversation: ChatConversationVO) => { | |||
|  * 对话 - 清理全部对话 | ||||
|  */ | ||||
| const handlerConversationClear = async ()=> { | ||||
|   // TODO @fan:需要加一个 对话进行中,不允许切换 | ||||
|   activeConversationId.value = null | ||||
|   activeConversation.value = null | ||||
|   list.value = [] | ||||
|  | @ -509,7 +523,7 @@ const handlerConversationClear = async ()=> { | |||
|  * 对话 - 删除 | ||||
|  */ | ||||
| const handlerConversationDelete = async (delConversation: ChatConversationVO) => { | ||||
|   // 删除的对话如果是当前选中的,那么久重置 | ||||
|   // 删除的对话如果是当前选中的,那么就重置 | ||||
|   if (activeConversationId.value === delConversation.id) { | ||||
|     await handlerConversationClear() | ||||
|   } | ||||
|  | @ -532,6 +546,7 @@ const getConversation = async (id: string | null) => { | |||
| /** | ||||
|  * 对话 - 新建 | ||||
|  */ | ||||
| // TODO @fan:应该是 handleXXX,handler 是名词哈 | ||||
| const handlerNewChat = async () => { | ||||
|   // 创建对话 | ||||
|   await conversationRef.value.createConversation() | ||||
|  | @ -552,14 +567,14 @@ const handlerMessageDelete = async () => { | |||
| } | ||||
| 
 | ||||
| /** | ||||
|  * 编辑 message | ||||
|  * 编辑 message:设置为 prompt,可以再次编辑 | ||||
|  */ | ||||
| const handlerMessageEdit = async (message: ChatMessageVO) => { | ||||
|   prompt.value = message.content | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * 编辑 message | ||||
|  * 刷新 message:基于指定消息,再次发起对话 | ||||
|  */ | ||||
| const handlerMessageRefresh = async (message: ChatMessageVO) => { | ||||
|   await doSend(message.content) | ||||
|  | @ -579,10 +594,12 @@ const handlerMessageClear = async () => { | |||
|   if (!activeConversationId.value) { | ||||
|     return | ||||
|   } | ||||
|   // TODO @fan:需要 try catch 下,不然点击取消会报异常 | ||||
|   // 确认提示 | ||||
|   await message.delConfirm("确认清空对话消息?") | ||||
|   // 清空对话 | ||||
|   await ChatMessageApi.deleteByConversationId(activeConversationId.value as string) | ||||
|   // TODO @fan:是不是直接置空就好啦; | ||||
|   // 刷新 message 列表 | ||||
|   await getMessageList() | ||||
| } | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ | |||
|                 <el-icon><More /></el-icon> | ||||
|               </el-button> | ||||
|           </span> | ||||
|             <!-- TODO @fan:下面两个 icon,可以使用类似 <Icon icon="ep:question-filled" /> 替代哈 --> | ||||
|             <template #dropdown> | ||||
|               <el-dropdown-menu> | ||||
|                 <el-dropdown-item :command="['edit', role]" > | ||||
|  | @ -31,7 +32,6 @@ | |||
|           <div class="content-container"> | ||||
|             <div class="title">{{ role.name }}</div> | ||||
|             <div class="description">{{ role.description }}</div> | ||||
| 
 | ||||
|           </div> | ||||
|           <div class="btn-container"> | ||||
|             <el-button type="primary" size="small" @click="handleUseClick(role)">使用</el-button> | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| <!-- chat 角色仓库 --> | ||||
| <template> | ||||
|   <el-container class="role-container"> | ||||
|     <ChatRoleForm ref="formRef" @success="handlerAddRoleSuccess"/> | ||||
|     <ChatRoleForm ref="formRef" @success="handlerAddRoleSuccess" /> | ||||
|     <!--  header  --> | ||||
|     <Header title="角色仓库" style="position: relative"/> | ||||
|     <Header title="角色仓库" style="position: relative" /> | ||||
|     <!--  main  --> | ||||
|     <el-main class="role-main"> | ||||
|       <div class="search-container"> | ||||
|  | @ -17,9 +17,15 @@ | |||
|           :suffix-icon="Search" | ||||
|           @change="getActiveTabsRole" | ||||
|         /> | ||||
|         <el-button v-if="activeRole == 'my-role'" type="primary" @click="handlerAddRole" style="margin-left: 20px;"> | ||||
|         <el-button | ||||
|           v-if="activeRole == 'my-role'" | ||||
|           type="primary" | ||||
|           @click="handlerAddRole" | ||||
|           style="margin-left: 20px" | ||||
|         > | ||||
|           <!-- TODO @fan:下面两个 icon,可以使用类似 <Icon icon="ep:question-filled" /> 替代哈 --> | ||||
|           <el-icon> | ||||
|             <User/> | ||||
|             <User /> | ||||
|           </el-icon> | ||||
|           添加角色 | ||||
|         </el-button> | ||||
|  | @ -35,7 +41,8 @@ | |||
|             @on-edit="handlerCardEdit" | ||||
|             @on-use="handlerCardUse" | ||||
|             @on-page="handlerCardPage('my')" | ||||
|             style="margin-top: 20px;"/> | ||||
|             style="margin-top: 20px" | ||||
|           /> | ||||
|         </el-tab-pane> | ||||
|         <el-tab-pane label="公共角色" name="public-role"> | ||||
|           <RoleCategoryList | ||||
|  | @ -50,34 +57,33 @@ | |||
|             @on-edit="handlerCardEdit" | ||||
|             @on-use="handlerCardUse" | ||||
|             @on-page="handlerCardPage('public')" | ||||
|             style="margin-top: 20px;" | ||||
|            loading/> | ||||
|             style="margin-top: 20px" | ||||
|             loading | ||||
|           /> | ||||
|         </el-tab-pane> | ||||
|       </el-tabs> | ||||
|     </el-main> | ||||
|   </el-container> | ||||
| 
 | ||||
| </template> | ||||
| 
 | ||||
| <!--  setup  --> | ||||
| <script setup lang="ts"> | ||||
| import {ref} from "vue"; | ||||
| import { ref } from 'vue' | ||||
| import Header from '@/views/ai/chat/components/Header.vue' | ||||
| import RoleList from './RoleList.vue' | ||||
| import ChatRoleForm from '@/views/ai/model/chatRole/ChatRoleForm.vue' | ||||
| import RoleCategoryList from './RoleCategoryList.vue' | ||||
| import {ChatRoleApi, ChatRolePageReqVO, ChatRoleVO} from '@/api/ai/model/chatRole' | ||||
| import {ChatConversationApi, ChatConversationVO} from '@/api/ai/chat/conversation' | ||||
| import {TabsPaneContext} from "element-plus"; | ||||
| import {Search, User} from "@element-plus/icons-vue"; | ||||
| import { ChatRoleApi, ChatRolePageReqVO, ChatRoleVO } from '@/api/ai/model/chatRole' | ||||
| import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation' | ||||
| import { TabsPaneContext } from 'element-plus' | ||||
| import { Search, User } from '@element-plus/icons-vue' | ||||
| 
 | ||||
| // 获取路由 | ||||
| const router = useRouter() | ||||
| const router = useRouter() // 路由对象 | ||||
| 
 | ||||
| // 属性定义 | ||||
| const loading = ref<boolean>(false) // 加载中 | ||||
| const activeRole = ref<string>('my-role') // 选中的角色 | ||||
| const activeRole = ref<string>('my-role') // 选中的角色 TODO @fan:是不是叫 activeTab 会更明确一点哈。选中的角色,会以为是某个角色 | ||||
| const search = ref<string>('') // 加载中 | ||||
| // TODO @fan:要不 myPage、pubPage,搞成类似 const queryParams = reactive({ ,分别搞成两个大的参数哈? | ||||
| const myPageNo = ref<number>(1) // my 分页下标 | ||||
| const myPageSize = ref<number>(50) // my 分页大小 | ||||
| const myRoleList = ref<ChatRoleVO[]>([]) // my 分页大小 | ||||
|  | @ -86,18 +92,16 @@ const publicPageSize = ref<number>(50) // public 分页大小 | |||
| const publicRoleList = ref<ChatRoleVO[]>([]) // public 分页大小 | ||||
| const activeCategory = ref<string>('全部') // 选择中的分类 | ||||
| const categoryList = ref<string[]>([]) // 角色分类类别 | ||||
| /** 添加/修改操作 */ | ||||
| const formRef = ref() | ||||
| // tabs 点击 | ||||
| 
 | ||||
| /** tabs 点击 */ | ||||
| const handleTabsClick = async (tab: TabsPaneContext) => { | ||||
|   // 设置切换状态 | ||||
|   const activeTabs = tab.paneName + '' | ||||
|   activeRole.value = activeTabs; | ||||
|   activeRole.value = tab.paneName + '' | ||||
|   // 切换的时候重新加载数据 | ||||
|   await getActiveTabsRole() | ||||
| } | ||||
| 
 | ||||
| // 获取 my role | ||||
| /** 获取 my role 我的角色 */ | ||||
| const getMyRole = async (append?: boolean) => { | ||||
|   const params: ChatRolePageReqVO = { | ||||
|     pageNo: myPageNo.value, | ||||
|  | @ -105,7 +109,7 @@ const getMyRole = async (append?: boolean) => { | |||
|     name: search.value, | ||||
|     publicStatus: false | ||||
|   } | ||||
|   const {total, list} = await ChatRoleApi.getMyPage(params) | ||||
|   const { total, list } = await ChatRoleApi.getMyPage(params) | ||||
|   if (append) { | ||||
|     myRoleList.value.push.apply(myRoleList.value, list) | ||||
|   } else { | ||||
|  | @ -113,7 +117,7 @@ const getMyRole = async (append?: boolean) => { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| // 获取 public role | ||||
| /** 获取 public role 公共角色 */ | ||||
| const getPublicRole = async (append?: boolean) => { | ||||
|   const params: ChatRolePageReqVO = { | ||||
|     pageNo: publicPageNo.value, | ||||
|  | @ -122,7 +126,7 @@ const getPublicRole = async (append?: boolean) => { | |||
|     name: search.value, | ||||
|     publicStatus: true | ||||
|   } | ||||
|   const {total, list} = await ChatRoleApi.getMyPage(params) | ||||
|   const { total, list } = await ChatRoleApi.getMyPage(params) | ||||
|   if (append) { | ||||
|     publicRoleList.value.push.apply(publicRoleList.value, list) | ||||
|   } else { | ||||
|  | @ -130,7 +134,7 @@ const getPublicRole = async (append?: boolean) => { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| // 获取选中的 tabs 角色 | ||||
| /** 获取选中的 tabs 角色 */ | ||||
| const getActiveTabsRole = async () => { | ||||
|   if (activeRole.value === 'my-role') { | ||||
|     myPageNo.value = 1 | ||||
|  | @ -141,14 +145,14 @@ const getActiveTabsRole = async () => { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| // 获取角色分类列表 | ||||
| /** 获取角色分类列表 */ | ||||
| const getRoleCategoryList = async () => { | ||||
|   const res = await ChatRoleApi.getCategoryList() | ||||
|   const defaultRole = ['全部'] | ||||
|   categoryList.value = [...defaultRole, ...res] | ||||
| } | ||||
| 
 | ||||
| // 处理分类点击 | ||||
| /** 处理分类点击 */ | ||||
| const handlerCategoryClick = async (category: string) => { | ||||
|   // 切换选择的分类 | ||||
|   activeCategory.value = category | ||||
|  | @ -156,11 +160,18 @@ const handlerCategoryClick = async (category: string) => { | |||
|   await getActiveTabsRole() | ||||
| } | ||||
| 
 | ||||
| // 添加角色 | ||||
| /** 添加/修改操作 */ | ||||
| const formRef = ref() | ||||
| const handlerAddRole = async () => { | ||||
|   formRef.value.open('my-create', null, '添加角色') | ||||
| } | ||||
| 
 | ||||
| /** 添加角色成功 */ | ||||
| const handlerAddRoleSuccess = async (e) => { | ||||
|   // 刷新数据 | ||||
|   await getActiveTabsRole() | ||||
| } | ||||
| 
 | ||||
| // card 删除 | ||||
| const handlerCardDelete = async (role) => { | ||||
|   await ChatRoleApi.deleteMy(role.id) | ||||
|  | @ -173,7 +184,7 @@ const handlerCardEdit = async (role) => { | |||
|   formRef.value.open('my-update', role.id, '编辑角色') | ||||
| } | ||||
| 
 | ||||
| // card 分页 | ||||
| /** card 分页:获取下一页 */ | ||||
| const handlerCardPage = async (type) => { | ||||
|   console.log('handlerCardPage', type) | ||||
|   try { | ||||
|  | @ -190,39 +201,33 @@ const handlerCardPage = async (type) => { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| // card 使用 | ||||
| /** 选择 card 角色:新建聊天对话 */ | ||||
| const handlerCardUse = async (role) => { | ||||
|   // 1. 创建对话 | ||||
|   const data: ChatConversationVO = { | ||||
|     roleId: role.id | ||||
|   } as unknown as ChatConversationVO | ||||
|   // 创建对话 | ||||
|   const conversation = await ChatConversationApi.createChatConversationMy(data) | ||||
|   // 调整页面 | ||||
|   router.push({ | ||||
|   const conversationId = await ChatConversationApi.createChatConversationMy(data) | ||||
|   // 2. 跳转页面 | ||||
|   // TODO @fan:最好用 name,后续可能会改~~~ | ||||
|   await router.push({ | ||||
|     path: `/ai/chat`, | ||||
|     query: { | ||||
|       conversationId: conversation, | ||||
|       conversationId: conversationId | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| // 添加角色成功 | ||||
| const handlerAddRoleSuccess = async (e) => { | ||||
|   console.log(e) | ||||
|   // 刷新数据 | ||||
|   await getActiveTabsRole() | ||||
| } | ||||
| 
 | ||||
| // | ||||
| /** 初始化 **/ | ||||
| onMounted(async () => { | ||||
|   // 获取分类 | ||||
|   await getRoleCategoryList() | ||||
|   // 获取 role 数据 | ||||
|   await getActiveTabsRole() | ||||
| }) | ||||
| // TODO @fan:css 是不是可以融合到 scss 里面呀? | ||||
| </script> | ||||
| <style lang="css"> | ||||
| 
 | ||||
| .el-tabs__content { | ||||
|   position: relative; | ||||
|   height: 100%; | ||||
|  | @ -232,11 +237,9 @@ onMounted(async () => { | |||
| .el-tabs__nav-scroll { | ||||
|   margin: 10px 20px; | ||||
| } | ||||
| 
 | ||||
| </style> | ||||
| <!-- 样式 --> | ||||
| <style scoped lang="scss"> | ||||
| 
 | ||||
| // 跟容器 | ||||
| .role-container { | ||||
|   position: absolute; | ||||
|  | @ -290,6 +293,4 @@ onMounted(async () => { | |||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| </style> | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 YunaiV
						YunaiV