Browse Source

feat:添加左侧历史对话列表

zhangwl 1 month ago
parent
commit
a3c6cd0ec0

+ 11 - 0
chat-ai-ui-main/src/components/Header.vue

@@ -56,6 +56,17 @@ const logout = async () => {
     class="py-4 px-6 bg-gray-800 shadow-md flex justify-between items-center"
     class="py-4 px-6 bg-gray-800 shadow-md flex justify-between items-center"
   >
   >
     <div class="flex items-center space-x-3">
     <div class="flex items-center space-x-3">
+      <!-- 历史对话汉堡按钮 -->
+      <button
+        v-if="userStore.userId"
+        @click="chatStore.sidebarOpen = !chatStore.sidebarOpen"
+        class="text-gray-400 hover:text-white p-1.5 rounded transition-colors hover:bg-gray-700"
+        title="历史对话"
+      >
+        <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
+        </svg>
+      </button>
       <img :src="robotImage" alt="Chat AI" class="w-8 h-8" />
       <img :src="robotImage" alt="Chat AI" class="w-8 h-8" />
       <h1 class="text-lg font-semibold text-white">AI智能问答系统</h1>
       <h1 class="text-lg font-semibold text-white">AI智能问答系统</h1>
     </div>
     </div>

+ 32 - 2
chat-ai-ui-main/src/stores/chat.ts

@@ -10,6 +10,12 @@ interface FormattedMessage {
   searching?: string | null;
   searching?: string | null;
 }
 }
 
 
+interface Session {
+  sessionId: string;
+  createdAt: string;
+  preview: string;
+}
+
 interface Message {
 interface Message {
   role: string;
   role: string;
   content: string;
   content: string;
@@ -34,13 +40,37 @@ const blankMeta = () => ({
 export const useChatStore = defineStore('chat', () => {
 export const useChatStore = defineStore('chat', () => {
   const messages = ref<Message[]>([]);
   const messages = ref<Message[]>([]);
   const isLoading = ref(false);
   const isLoading = ref(false);
-  const sessionId = ref(crypto.randomUUID());
+  const sessionId = ref<string>(crypto.randomUUID());
+  const sessions = ref<Session[]>([]);
+  const sidebarOpen = ref(false);
 
 
   const userStore = useUserStore();
   const userStore = useUserStore();
 
 
+  const loadSessions = async () => {
+    if (!userStore.userId) return;
+    try {
+      const { data } = await axios.get(
+        `${import.meta.env.VITE_API_URL}/chat/sessions`,
+        { headers: { 'Authorization': `Bearer ${userStore.userId}` } }
+      );
+      sessions.value = data;
+    } catch (error) {
+      console.error('Error loading sessions:', error);
+    }
+  };
+
+  const switchSession = async (id: string) => {
+    if (id === sessionId.value) { sidebarOpen.value = false; return; }
+    sessionId.value = id;
+    messages.value = [];
+    sidebarOpen.value = false;
+    await loadChatHistory();
+  };
+
   const newConversation = () => {
   const newConversation = () => {
     sessionId.value = crypto.randomUUID();
     sessionId.value = crypto.randomUUID();
     messages.value = [];
     messages.value = [];
+    sidebarOpen.value = false;
   };
   };
 
 
   const loadChatHistory = async () => {
   const loadChatHistory = async () => {
@@ -219,5 +249,5 @@ export const useChatStore = defineStore('chat', () => {
     }
     }
   };
   };
 
 
-  return { messages, isLoading, sessionId, loadChatHistory, sendMessage, newConversation };
+  return { messages, isLoading, sessionId, sessions, sidebarOpen, loadChatHistory, loadSessions, sendMessage, newConversation, switchSession };
 });
 });

+ 109 - 4
chat-ai-ui-main/src/views/ChatView.vue

@@ -60,8 +60,22 @@ const scrollToBottom = () => {
 // 初始化页面后进行加载历史消息
 // 初始化页面后进行加载历史消息
 onMounted(() => {
 onMounted(() => {
   chatStore.loadChatHistory().then(() => scrollToBottom());
   chatStore.loadChatHistory().then(() => scrollToBottom());
+  chatStore.loadSessions();
 });
 });
 
 
+// 会话日期格式化
+const formatSessionDate = (dateStr: string) => {
+  const date = new Date(dateStr);
+  const now = new Date();
+  const dateDay = new Date(date.getFullYear(), date.getMonth(), date.getDate());
+  const nowDay  = new Date(now.getFullYear(),  now.getMonth(),  now.getDate());
+  const days = Math.round((nowDay.getTime() - dateDay.getTime()) / 86400000);
+  if (days === 0) return '今天';
+  if (days === 1) return '昨天';
+  if (days < 7) return `${days}天前`;
+  return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
+};
+
 // 使用响应式监听器 监听 messages 变化并滚动到底
 // 使用响应式监听器 监听 messages 变化并滚动到底
 watch(
 watch(
   () => chatStore.messages,
   () => chatStore.messages,
@@ -73,12 +87,70 @@ watch(
 </script>
 </script>
 
 
 <template>
 <template>
-  <div class="min-h-screen bg-gray-900 text-white">
+  <div class="min-h-screen bg-gray-900 text-white flex flex-col">
     <Header />
     <Header />
 
 
-    <!-- 具有居中聊天区域的主容器 -->
-    <div class="flex justify-center px-4 py-6">
-      <div class="w-full max-w-4xl flex flex-col h-[calc(100vh-120px)]">
+    <!-- 主内容区(相对定位,作为抽屉定位基准) -->
+    <div class="flex-1 relative overflow-hidden">
+
+      <!-- 遮罩层 -->
+      <transition name="fade">
+        <div
+          v-if="chatStore.sidebarOpen"
+          class="absolute inset-0 bg-black/50 z-10"
+          @click="chatStore.sidebarOpen = false"
+        ></div>
+      </transition>
+
+      <!-- 左侧抽屉 -->
+      <transition name="slide-left">
+        <aside
+          v-if="chatStore.sidebarOpen"
+          class="absolute left-0 top-0 h-full w-64 bg-gray-800 border-r border-gray-700 z-20 flex flex-col"
+        >
+          <!-- 抽屉顶部:新对话 + 关闭 -->
+          <div class="flex items-center justify-between px-4 py-3 border-b border-gray-700">
+            <button
+              @click="chatStore.newConversation()"
+              class="flex items-center gap-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 px-3 py-1.5 rounded-lg transition-colors"
+            >
+              <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
+              </svg>
+              新对话
+            </button>
+            <button
+              @click="chatStore.sidebarOpen = false"
+              class="text-gray-400 hover:text-white p-1 rounded transition-colors"
+            >
+              <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
+              </svg>
+            </button>
+          </div>
+
+          <!-- 会话列表 -->
+          <div class="flex-1 overflow-y-auto py-2">
+            <p v-if="chatStore.sessions.length === 0" class="text-gray-500 text-sm text-center mt-8">暂无历史对话</p>
+            <button
+              v-for="session in chatStore.sessions"
+              :key="session.sessionId"
+              @click="chatStore.switchSession(session.sessionId)"
+              class="w-full text-left px-4 py-3 hover:bg-gray-700 transition-colors border-l-2"
+              :class="session.sessionId === chatStore.sessionId
+                ? 'border-blue-500 bg-gray-700/60 text-white'
+                : 'border-transparent text-gray-300'"
+            >
+              <p class="text-sm truncate">{{ session.preview || '新对话' }}</p>
+              <p class="text-xs text-gray-500 mt-0.5">{{ formatSessionDate(session.createdAt) }}</p>
+            </button>
+          </div>
+        </aside>
+      </transition>
+
+      <!-- 聊天主区域 -->
+      <div class="flex justify-center px-4 py-6 h-full">
+        <div class="w-full max-w-4xl flex flex-col h-full">
         
         
         <!-- 聊天消息容器 -->
         <!-- 聊天消息容器 -->
         <div id="chat-container" class="flex-1 overflow-y-auto space-y-4 mb-4 px-4 py-4 rounded-lg shadow-lg">
         <div id="chat-container" class="flex-1 overflow-y-auto space-y-4 mb-4 px-4 py-4 rounded-lg shadow-lg">
@@ -210,6 +282,7 @@ watch(
       </div>
       </div>
     </div>
     </div>
   </div>
   </div>
+</div>
 </template>
 </template>
 
 
 
 
@@ -390,6 +463,38 @@ watch(
   color: #ef4444;
   color: #ef4444;
 }
 }
 
 
+/* 抽屉滑入动画 */
+.slide-left-enter-active,
+.slide-left-leave-active {
+  transition: transform 0.25s ease;
+}
+.slide-left-enter-from,
+.slide-left-leave-to {
+  transform: translateX(-100%);
+}
+
+/* 遮罩淡入动画 */
+.fade-enter-active,
+.fade-leave-active {
+  transition: opacity 0.25s ease;
+}
+.fade-enter-from,
+.fade-leave-to {
+  opacity: 0;
+}
+
+/* 抽屉会话列表滚动条 */
+aside::-webkit-scrollbar {
+  width: 4px;
+}
+aside::-webkit-scrollbar-track {
+  background: transparent;
+}
+aside::-webkit-scrollbar-thumb {
+  background: #4b5563;
+  border-radius: 2px;
+}
+
 /* 打字光标效果 */
 /* 打字光标效果 */
 .typing-cursor {
 .typing-cursor {
   display: inline-block;
   display: inline-block;