From a1d7e81859f554f3a53680cc35f0f49bf1f77098 Mon Sep 17 00:00:00 2001
From: wwf <1971391498@qq.com>
Date: 星期四, 14 五月 2026 14:37:02 +0800
Subject: [PATCH] 导入项目

---
 src/views/ai/chat/index/components/conversation/ConversationList.vue |  391 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 391 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..f73fb27
--- /dev/null
+++ b/src/views/ai/chat/index/components/conversation/ConversationList.vue
@@ -0,0 +1,391 @@
+<!--  AI 瀵硅瘽  -->
+<template>
+  <el-aside
+    width="260px"
+    class="h-100% relative flex flex-col justify-between px-2.5 pt-2.5 pb-0 overflow-hidden"
+  >
+    <!-- 宸﹂《閮細瀵硅瘽 -->
+    <div class="h-100%">
+      <el-button class="w-1/1 py-4.5" type="primary" @click="createConversation">
+        <Icon icon="ep:plus" class="mr-5px" />
+        鏂板缓瀵硅瘽
+      </el-button>
+
+      <!-- 宸﹂《閮細鎼滅储瀵硅瘽 -->
+      <el-input
+        v-model="searchName"
+        size="large"
+        class="mt-5"
+        placeholder="鎼滅储鍘嗗彶璁板綍"
+        @keyup="searchConversation"
+      >
+        <template #prefix>
+          <Icon icon="ep:search" />
+        </template>
+      </el-input>
+
+      <!-- 宸︿腑闂达細瀵硅瘽鍒楄〃 -->
+      <div class="overflow-auto h-full">
+        <!-- 鎯呭喌涓�锛氬姞杞戒腑 -->
+        <el-empty v-if="loading" description="." :v-loading="loading" />
+        <!-- 鎯呭喌浜岋細鎸夌収 group 鍒嗙粍锛屽睍绀鸿亰澶╀細璇� list 鍒楄〃 -->
+        <div v-for="conversationKey in Object.keys(conversationMap)" :key="conversationKey">
+          <div class="mt-1.25 pt-2.5" v-if="conversationMap[conversationKey].length">
+            <el-text class="mx-1" size="small" tag="b">
+              {{ conversationKey }}
+            </el-text>
+          </div>
+          <div
+            class="mt-1.25"
+            v-for="conversation in conversationMap[conversationKey]"
+            :key="conversation.id"
+            @click="handleConversationClick(conversation.id)"
+            @mouseover="hoverConversationId = conversation.id"
+            @mouseout="hoverConversationId = ''"
+          >
+            <div
+              class="flex flex-row justify-between flex-1 px-1.25 cursor-pointer rounded-1.25 items-center leading-7.5"
+              :style="
+                conversation.id === activeConversationId
+                  ? 'background-color: var(--el-color-primary-light-9); border: 1px solid var(--el-color-primary-light-7);'
+                  : ''
+              "
+            >
+              <div class="flex flex-row items-center">
+                <img
+                  class="w-6.25 h-6.25 rounded-1.25 flex flex-row justify-center"
+                  :src="conversation.roleAvatar || roleAvatarDefaultImg"
+                />
+                <span
+                  class="py-0.5 px-2.5"
+                  style="
+                    max-width: 220px;
+                    font-size: 14px;
+                    font-weight: 400;
+                    color: var(--el-text-color-regular);
+                    overflow: hidden;
+                    white-space: nowrap;
+                    text-overflow: ellipsis;
+                  "
+                >
+                  {{ conversation.title }}
+                </span>
+              </div>
+              <div
+                class="right-0.5 flex flex-row justify-center"
+                style="color: var(--el-text-color-regular)"
+                v-show="hoverConversationId === conversation.id"
+              >
+                <el-button class="m-0" 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="m-0" link @click.stop="updateConversationTitle(conversation)">
+                  <el-icon title="缂栬緫">
+                    <Icon icon="ep:edit" />
+                  </el-icon>
+                </el-button>
+                <el-button class="m-0" 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="absolute bottom-0 left-0 right-0 px-5 leading-8.75 flex justify-between items-center"
+      style="
+        background-color: var(--el-fill-color-extra-light);
+        box-shadow: 0 0 1px 1px var(--el-border-color-lighter);
+        color: var(--el-text-color);
+      "
+    >
+      <div
+        class="flex items-center p-0 m-0 cursor-pointer"
+        style="color: var(--el-text-color-regular)"
+        @click="handleRoleRepository"
+      >
+        <Icon icon="ep:user" />
+        <el-text class="ml-1.25" size="small">瑙掕壊浠撳簱</el-text>
+      </div>
+      <div
+        class="flex items-center p-0 m-0 cursor-pointer"
+        style="color: var(--el-text-color-regular)"
+        @click="handleClearConversation"
+      >
+        <Icon icon="ep:delete" />
+        <el-text class="ml-1.25" 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 = {
+    缃《: [] as ChatConversationVO[],
+    浠婂ぉ: [] as ChatConversationVO[],
+    涓�澶╁墠: [] as ChatConversationVO[],
+    涓夊ぉ鍓�: [] as ChatConversationVO[],
+    涓冨ぉ鍓�: [] as ChatConversationVO[],
+    涓夊崄澶╁墠: [] as ChatConversationVO[]
+  }
+  // 褰撳墠鏃堕棿鐨勬椂闂存埑
+  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>

--
Gitblit v1.8.0