From a358c1d7b5b9b9974c9a91f13dbe339dcc48d742 Mon Sep 17 00:00:00 2001 From: dengzedong <dengzedong@email> Date: 星期五, 13 六月 2025 11:05:43 +0800 Subject: [PATCH] 数据分析 影响因素 --- src/views/ai/chat/index/components/conversation/ConversationList.vue | 472 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 472 insertions(+), 0 deletions(-) diff --git a/src/views/ai/chat/index/components/conversation/ConversationList.vue b/src/views/ai/chat/index/components/conversation/ConversationList.vue new file mode 100644 index 0000000..54940f8 --- /dev/null +++ b/src/views/ai/chat/index/components/conversation/ConversationList.vue @@ -0,0 +1,472 @@ +<!-- AI 对话 --> +<template> + <el-aside width="260px" class="conversation-container h-100%"> + <!-- 左顶部:对话 --> + <div class="h-100%"> + <el-button class="w-1/1 btn-new-conversation" type="primary" @click="createConversation"> + <Icon icon="ep:plus" class="mr-5px" /> + 新建对话 + </el-button> + + <!-- 左顶部:搜索对话 --> + <el-input + v-model="searchName" + size="large" + class="mt-10px search-input" + placeholder="搜索历史记录" + @keyup="searchConversation" + > + <template #prefix> + <Icon icon="ep:search" /> + </template> + </el-input> + + <!-- 左中间:对话列表 --> + <div class="conversation-list"> + <!-- 情况一:加载中 --> + <el-empty v-if="loading" description="." :v-loading="loading" /> + <!-- 情况二:按照 group 分组,展示聊天会话 list 列表 --> + <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> + </div> + <div + class="conversation-item" + v-for="conversation in conversationMap[conversationKey]" + :key="conversation.id" + @click="handleConversationClick(conversation.id)" + @mouseover="hoverConversationId = conversation.id" + @mouseout="hoverConversationId = ''" + > + <div + :class=" + conversation.id === activeConversationId ? 'conversation active' : 'conversation' + " + > + <div class="title-wrapper"> + <img class="avatar" :src="conversation.roleAvatar || roleAvatarDefaultImg" /> + <span class="title">{{ conversation.title }}</span> + </div> + <div class="button-wrapper" v-show="hoverConversationId === conversation.id"> + <el-button class="btn" link @click.stop="handleTop(conversation)"> + <el-icon title="置顶" v-if="!conversation.pinned"><Top /></el-icon> + <el-icon title="置顶" v-if="conversation.pinned"><Bottom /></el-icon> + </el-button> + <el-button class="btn" link @click.stop="updateConversationTitle(conversation)"> + <el-icon title="编辑"> + <Icon icon="ep:edit" /> + </el-icon> + </el-button> + <el-button class="btn" link @click.stop="deleteChatConversation(conversation)"> + <el-icon title="删除对话"> + <Icon icon="ep:delete" /> + </el-icon> + </el-button> + </div> + </div> + </div> + </div> + <!-- 底部占位 --> + <div class="h-160px w-100%"></div> + </div> + </div> + + <!-- 左底部:工具栏 --> + <div class="tool-box"> + <div @click="handleRoleRepository"> + <Icon icon="ep:user" /> + <el-text size="small">角色仓库</el-text> + </div> + <div @click="handleClearConversation"> + <Icon icon="ep:delete" /> + <el-text size="small">清空未置顶对话</el-text> + </div> + </div> + + <!-- 角色仓库抽屉 --> + <el-drawer v-model="roleRepositoryOpen" title="角色仓库" size="754px"> + <RoleRepository /> + </el-drawer> + </el-aside> +</template> + +<script setup lang="ts"> +import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation' +import RoleRepository from '../role/RoleRepository.vue' +import { Bottom, Top } from '@element-plus/icons-vue' +import roleAvatarDefaultImg from '@/assets/ai/gpt.svg' + +const message = useMessage() // 消息弹窗 + +// 定义属性 +const searchName = ref<string>('') // 对话搜索 +const activeConversationId = ref<number | null>(null) // 选中的对话,默认为 null +const hoverConversationId = ref<number | null>(null) // 悬浮上去的对话 +const conversationList = ref([] as ChatConversationVO[]) // 对话列表 +const conversationMap = ref<any>({}) // 对话分组 (置顶、今天、三天前、一星期前、一个月前) +const loading = ref<boolean>(false) // 加载中 +const loadingTime = ref<any>() // 加载中定时器 + +// 定义组件 props +const props = defineProps({ + activeId: { + type: String || null, + required: true + } +}) + +// 定义钩子 +const emits = defineEmits([ + 'onConversationCreate', + 'onConversationClick', + 'onConversationClear', + 'onConversationDelete' +]) + +/** 搜索对话 */ +const searchConversation = async (e) => { + // 恢复数据 + if (!searchName.value.trim().length) { + conversationMap.value = await getConversationGroupByCreateTime(conversationList.value) + } else { + // 过滤 + const filterValues = conversationList.value.filter((item) => { + return item.title.includes(searchName.value.trim()) + }) + conversationMap.value = await getConversationGroupByCreateTime(filterValues) + } +} + +/** 点击对话 */ +const handleConversationClick = async (id: number) => { + // 过滤出选中的对话 + const filterConversation = conversationList.value.filter((item) => { + return item.id === id + }) + // 回调 onConversationClick + // noinspection JSVoidFunctionReturnValueUsed + const success = emits('onConversationClick', filterConversation[0]) + // 切换对话 + if (success) { + activeConversationId.value = id + } +} + +/** 获取对话列表 */ +const getChatConversationList = async () => { + try { + // 加载中 + loadingTime.value = setTimeout(() => { + loading.value = true + }, 50) + + // 1.1 获取 对话数据 + conversationList.value = await ChatConversationApi.getChatConversationMyList() + // 1.2 排序 + conversationList.value.sort((a, b) => { + return b.createTime - a.createTime + }) + // 1.3 没有任何对话情况 + if (conversationList.value.length === 0) { + activeConversationId.value = null + conversationMap.value = {} + return + } + + // 2. 对话根据时间分组(置顶、今天、一天前、三天前、七天前、30 天前) + conversationMap.value = await getConversationGroupByCreateTime(conversationList.value) + } finally { + // 清理定时器 + if (loadingTime.value) { + clearTimeout(loadingTime.value) + } + // 加载完成 + loading.value = false + } +} + +/** 按照 creteTime 创建时间,进行分组 */ +const getConversationGroupByCreateTime = async (list: ChatConversationVO[]) => { + // 排序、指定、时间分组(今天、一天前、三天前、七天前、30天前) + // noinspection NonAsciiCharacters + const groupMap = { + 置顶: [], + 今天: [], + 一天前: [], + 三天前: [], + 七天前: [], + 三十天前: [] + } + // 当前时间的时间戳 + const now = Date.now() + // 定义时间间隔常量(单位:毫秒) + const oneDay = 24 * 60 * 60 * 1000 + const threeDays = 3 * oneDay + const sevenDays = 7 * oneDay + const thirtyDays = 30 * oneDay + for (const conversation of list) { + // 置顶 + if (conversation.pinned) { + groupMap['置顶'].push(conversation) + continue + } + // 计算时间差(单位:毫秒) + const diff = now - conversation.createTime + // 根据时间间隔判断 + if (diff < oneDay) { + groupMap['今天'].push(conversation) + } else if (diff < threeDays) { + groupMap['一天前'].push(conversation) + } else if (diff < sevenDays) { + groupMap['三天前'].push(conversation) + } else if (diff < thirtyDays) { + groupMap['七天前'].push(conversation) + } else { + groupMap['三十天前'].push(conversation) + } + } + return groupMap +} + +/** 新建对话 */ +const createConversation = async () => { + // 1. 新建对话 + const conversationId = await ChatConversationApi.createChatConversationMy( + {} as unknown as ChatConversationVO + ) + // 2. 获取对话内容 + await getChatConversationList() + // 3. 选中对话 + await handleConversationClick(conversationId) + // 4. 回调 + emits('onConversationCreate') +} + +/** 修改对话的标题 */ +const updateConversationTitle = async (conversation: ChatConversationVO) => { + // 1. 二次确认 + const { value } = await ElMessageBox.prompt('修改标题', { + inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 判断非空,且非空格 + inputErrorMessage: '标题不能为空', + inputValue: conversation.title + }) + // 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 + }) + if (filterConversationList.length > 0) { + // tip:避免切换对话 + if (activeConversationId.value === filterConversationList[0].id) { + emits('onConversationClick', filterConversationList[0]) + } + } +} + +/** 删除聊天对话 */ +const deleteChatConversation = async (conversation: ChatConversationVO) => { + try { + // 删除的二次确认 + await message.delConfirm(`是否确认删除对话 - ${conversation.title}?`) + // 发起删除 + await ChatConversationApi.deleteChatConversationMy(conversation.id) + message.success('对话已删除') + // 刷新列表 + await getChatConversationList() + // 回调 + emits('onConversationDelete', conversation) + } catch {} +} + +/** 清空对话 */ +const handleClearConversation = async () => { + try { + await message.confirm('确认后对话会全部清空,置顶的对话除外。') + await ChatConversationApi.deleteChatConversationMyByUnpinned() + ElMessage({ + message: '操作成功!', + type: 'success' + }) + // 清空 对话 和 对话内容 + activeConversationId.value = null + // 获取 对话列表 + await getChatConversationList() + // 回调 方法 + emits('onConversationClear') + } catch {} +} + +/** 对话置顶 */ +const handleTop = async (conversation: ChatConversationVO) => { + // 更新对话置顶 + conversation.pinned = !conversation.pinned + await ChatConversationApi.updateChatConversationMy(conversation) + // 刷新对话 + await getChatConversationList() +} + +// ============ 角色仓库 ============ + +/** 角色仓库抽屉 */ +const roleRepositoryOpen = ref<boolean>(false) // 角色仓库是否打开 +const handleRoleRepository = async () => { + roleRepositoryOpen.value = !roleRepositoryOpen.value +} + +/** 监听选中的对话 */ +const { activeId } = toRefs(props) +watch(activeId, async (newValue, oldValue) => { + activeConversationId.value = newValue as string +}) + +// 定义 public 方法 +defineExpose({ createConversation }) + +/** 初始化 */ +onMounted(async () => { + // 获取 对话列表 + await getChatConversationList() + // 默认选中 + if (props.activeId) { + activeConversationId.value = props.activeId + } else { + // 首次默认选中第一个 + if (conversationList.value.length) { + activeConversationId.value = conversationList.value[0].id + // 回调 onConversationClick + await emits('onConversationClick', conversationList.value[0]) + } + } +}) +</script> + +<style scoped lang="scss"> +.conversation-container { + position: relative; + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 10px 10px 0; + overflow: hidden; + + .btn-new-conversation { + padding: 18px 0; + } + + .search-input { + margin-top: 20px; + } + + .conversation-list { + overflow: auto; + height: 100%; + + .classify-title { + padding-top: 10px; + } + + .conversation-item { + margin-top: 5px; + } + + .conversation { + display: flex; + flex-direction: row; + justify-content: space-between; + flex: 1; + padding: 0 5px; + cursor: pointer; + border-radius: 5px; + align-items: center; + line-height: 30px; + + &.active { + background-color: #e6e6e6; + + .button { + display: inline-block; + } + } + + .title-wrapper { + display: flex; + flex-direction: row; + align-items: center; + } + + .title { + padding: 2px 10px; + max-width: 220px; + font-size: 14px; + font-weight: 400; + color: rgba(0, 0, 0, 0.77); + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .avatar { + width: 25px; + height: 25px; + border-radius: 5px; + display: flex; + flex-direction: row; + justify-items: center; + } + + // 对话编辑、删除 + .button-wrapper { + right: 2px; + display: flex; + flex-direction: row; + justify-items: center; + color: #606266; + + .btn { + margin: 0; + } + } + } + } + + // 角色仓库、清空未设置对话 + .tool-box { + position: absolute; + bottom: 0; + left: 0; + right: 0; + //width: 100%; + padding: 0 20px; + background-color: #f4f4f4; + box-shadow: 0 0 1px 1px rgba(228, 228, 228, 0.8); + line-height: 35px; + display: flex; + justify-content: space-between; + align-items: center; + color: var(--el-text-color); + + > div { + display: flex; + align-items: center; + color: #606266; + padding: 0; + margin: 0; + cursor: pointer; + + > span { + margin-left: 5px; + } + } + } +} +</style> -- Gitblit v1.9.3