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