Просмотр исходного кода

AI 助手添加聊天记录分页功能

- 添加聊天记录分页接口 /upms/ai/chat/history
- 前端实现滚动到顶部加载更多历史记录
- 当前会话最多保留10条记录
- 添加加载状态提示

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tanlie 3 недель назад
Родитель
Сommit
fed499d867
3 измененных файлов с 315 добавлено и 36 удалено
  1. 123 0
      src/mock/index.ts
  2. 13 0
      src/utils/api.ts
  3. 179 36
      src/views/ai-chat/index.vue

+ 123 - 0
src/mock/index.ts

@@ -1,5 +1,100 @@
 import type { MockMethod } from "vite-plugin-mock";
 
+// 模拟聊天记录存储(内存中)
+interface ChatMessage {
+  id: string;
+  role: 'user' | 'assistant';
+  content: string;
+  time: string;
+}
+
+// 生成模拟历史聊天记录
+const generateMockChatHistory = (): ChatMessage[] => {
+  const history: ChatMessage[] = [
+    {
+      id: '1',
+      role: 'assistant',
+      content: '您好!我是您的 AI 智能助手,有什么可以帮助您的吗?',
+      time: new Date(Date.now() - 86400000 * 2).toLocaleString()
+    },
+    {
+      id: '2',
+      role: 'user',
+      content: '如何管理系统用户?',
+      time: new Date(Date.now() - 86400000 * 2 + 60000).toLocaleString()
+    },
+    {
+      id: '3',
+      role: 'assistant',
+      content: '系统用户管理在「用户管理」菜单中,您可以进行以下操作:\n1. 查看用户列表\n2. 添加新用户\n3. 编辑用户信息\n4. 禁用/启用用户账号',
+      time: new Date(Date.now() - 86400000 * 2 + 120000).toLocaleString()
+    },
+    {
+      id: '4',
+      role: 'user',
+      content: '系统有哪些功能模块?',
+      time: new Date(Date.now() - 86400000).toLocaleString()
+    },
+    {
+      id: '5',
+      role: 'assistant',
+      content: '本系统包含以下功能模块:\n- 仪表盘:数据概览\n- 用户管理:系统用户管理\n- 系统管理:菜单、角色管理\n- 数据统计:报表分析\n- AI 助手:智能问答',
+      time: new Date(Date.now() - 86400000 + 60000).toLocaleString()
+    },
+    {
+      id: '6',
+      role: 'user',
+      content: '如何导出数据报表?',
+      time: new Date(Date.now() - 43200000).toLocaleString()
+    },
+    {
+      id: '7',
+      role: 'assistant',
+      content: '导出数据报表步骤:\n1. 进入「数据统计」页面\n2. 选择查询条件\n3. 点击「导出」按钮\n4. 选择导出格式(Excel/PDF)\n5. 下载生成的报表文件',
+      time: new Date(Date.now() - 43200000 + 60000).toLocaleString()
+    },
+    {
+      id: '8',
+      role: 'user',
+      content: '如何配置菜单权限?',
+      time: new Date(Date.now() - 3600000).toLocaleString()
+    },
+    {
+      id: '9',
+      role: 'assistant',
+      content: '菜单权限配置在「系统管理 > 菜单管理」中:\n1. 创建菜单项\n2. 设置菜单图标和路径\n3. 为角色分配菜单权限\n4. 保存后权限即时生效',
+      time: new Date(Date.now() - 3600000 + 60000).toLocaleString()
+    },
+    {
+      id: '10',
+      role: 'user',
+      content: '如何修改个人资料?',
+      time: new Date(Date.now() - 1800000).toLocaleString()
+    },
+    {
+      id: '11',
+      role: 'assistant',
+      content: '修改个人资料:\n1. 点击右上角用户名\n2. 选择「个人中心」\n3. 编辑个人信息\n4. 点击「保存」按钮',
+      time: new Date(Date.now() - 1800000 + 60000).toLocaleString()
+    },
+    {
+      id: '12',
+      role: 'user',
+      content: '帮助',
+      time: new Date(Date.now() - 900000).toLocaleString()
+    },
+    {
+      id: '13',
+      role: 'assistant',
+      content: '我可以帮您:\n- 解答系统使用问题\n- 提供操作指导\n- 解释功能说明\n请直接输入您的问题!',
+      time: new Date(Date.now() - 900000 + 60000).toLocaleString()
+    }
+  ];
+  return history;
+};
+
+const chatHistory: ChatMessage[] = generateMockChatHistory();
+
 export default [
   // 登录接口
   {
@@ -153,6 +248,34 @@ export default [
     },
   },
 
+  // 获取聊天记录列表(分页)
+  {
+    url: "/api/upms/ai/chat/history",
+    method: "get",
+    response: ({ query }) => {
+      const page = parseInt(query.page as string) || 1;
+      const size = parseInt(query.size as string) || 10;
+
+      // 计算分页
+      const total = chatHistory.length;
+      const start = (page - 1) * size;
+      const end = start + size;
+      const list = chatHistory.slice(start, end);
+
+      return {
+        code: 200,
+        message: "success",
+        data: {
+          list,
+          total,
+          page,
+          size,
+          hasMore: end < total
+        }
+      };
+    },
+  },
+
   // AI 对话接口 (POST - 返回 JSON,适用于代理模式)
   {
     url: "/api/upms/ai/chat",

+ 13 - 0
src/utils/api.ts

@@ -70,6 +70,19 @@ export const aiApi = {
   // 发送对话消息
   chat(question: string) {
     return http.post<{ reply: string }>('/upms/ai/chat', { question })
+  },
+  // 获取聊天记录(分页)
+  getHistory(params: { page?: number; size?: number }) {
+    return http.get<{
+      list: Array<{
+        id: string
+        role: 'user' | 'assistant'
+        content: string
+        time: string
+      }>
+      total: number
+      hasMore: boolean
+    }>('/upms/ai/chat/history', params)
   }
 }
 

+ 179 - 36
src/views/ai-chat/index.vue

@@ -9,10 +9,19 @@
       </template>
 
       <!-- 聊天消息区域 -->
-      <div class="chat-messages" ref="messagesRef">
+      <div class="chat-messages" ref="messagesRef" @scroll="handleScroll">
+        <!-- 加载更多提示 -->
+        <div v-if="isLoadingHistory" class="loading-more">
+          <el-icon class="loading-icon"><Loading /></el-icon>
+          <span>加载历史记录中...</span>
+        </div>
+        <div v-else-if="!hasMoreHistory" class="no-more-history">
+          <span>—— 没有更多记录了 ——</span>
+        </div>
+
         <div
-          v-for="(msg, index) in messages"
-          :key="index"
+          v-for="(msg, index) in displayMessages"
+          :key="msg.id || index"
           :class="['message', msg.role === 'user' ? 'user-message' : 'ai-message']"
         >
           <div class="message-avatar">
@@ -100,13 +109,14 @@
 </template>
 
 <script setup lang="ts">
-import { ref, nextTick, onMounted } from 'vue'
+import { ref, nextTick, onMounted, computed } from 'vue'
 import { ElMessage } from 'element-plus'
-import { ChatDotRound, Delete, Download, Promotion } from '@element-plus/icons-vue'
+import { ChatDotRound, Delete, Download, Promotion, Loading } from '@element-plus/icons-vue'
 import { useUserStore } from '@/store/user'
 import { http } from '@/utils/request'
 
 interface Message {
+  id?: string
   role: 'user' | 'assistant'
   content: string
   time: string
@@ -122,8 +132,15 @@ const isTyping = ref(false)
 const userAvatar = 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'
 const aiAvatar = 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
 
-// 消息列表
-const messages = ref<Message[]>([
+// 历史记录分页
+const historyPage = ref(1)
+const historySize = 10
+const isLoadingHistory = ref(false)
+const hasMoreHistory = ref(true)
+const historyMessages = ref<Message[]>([])
+
+// 当前会话消息(最多保留10条)
+const currentMessages = ref<Message[]>([
   {
     role: 'assistant',
     content: '您好!我是您的 AI 智能助手,有什么可以帮助您的吗?',
@@ -131,6 +148,11 @@ const messages = ref<Message[]>([
   }
 ])
 
+// 合并显示的消息(历史 + 当前)
+const displayMessages = computed(() => {
+  return [...historyMessages.value, ...currentMessages.value]
+})
+
 // 快捷问题
 const quickQuestions = [
   '如何管理系统用户?',
@@ -154,18 +176,82 @@ const scrollToBottom = () => {
   })
 }
 
+// 记录滚动位置
+let scrollHeightBeforeLoad = 0
+
+// 加载历史聊天记录
+const loadHistory = async () => {
+  if (isLoadingHistory.value || !hasMoreHistory.value) return
+
+  isLoadingHistory.value = true
+
+  // 记录当前滚动高度
+  if (messagesRef.value) {
+    scrollHeightBeforeLoad = messagesRef.value.scrollHeight
+  }
+
+  try {
+    const res = await http.get<{
+      list: Message[]
+      total: number
+      hasMore: boolean
+    }>('/upms/ai/chat/history', {
+      page: historyPage.value,
+      size: historySize
+    })
+
+    if (res.list && res.list.length > 0) {
+      // 将新加载的历史记录添加到前面
+      historyMessages.value = [...res.list, ...historyMessages.value]
+      historyPage.value++
+      hasMoreHistory.value = res.hasMore
+
+      // 保持滚动位置
+      nextTick(() => {
+        if (messagesRef.value) {
+          const newScrollHeight = messagesRef.value.scrollHeight
+          messagesRef.value.scrollTop = newScrollHeight - scrollHeightBeforeLoad
+        }
+      })
+    } else {
+      hasMoreHistory.value = false
+    }
+  } catch (error) {
+    console.error('加载历史记录失败:', error)
+  } finally {
+    isLoadingHistory.value = false
+  }
+}
+
+// 处理滚动事件
+const handleScroll = () => {
+  if (!messagesRef.value) return
+
+  const { scrollTop } = messagesRef.value
+
+  // 当滚动到顶部时,加载更多历史记录
+  if (scrollTop <= 10 && !isLoadingHistory.value && hasMoreHistory.value) {
+    loadHistory()
+  }
+}
+
 // 发送消息
 const sendMessage = async () => {
   const content = inputMessage.value.trim()
   if (!content || isSending.value) return
 
-  // 添加用户消息
-  messages.value.push({
+  // 添加用户消息到当前会话
+  currentMessages.value.push({
     role: 'user',
     content,
     time: new Date().toLocaleString()
   })
 
+  // 只保留最近10条当前会话记录
+  if (currentMessages.value.length > 10) {
+    currentMessages.value = currentMessages.value.slice(-10)
+  }
+
   inputMessage.value = ''
   isSending.value = true
   isTyping.value = true
@@ -184,26 +270,32 @@ const sendMessage = async () => {
 }
 
 // 打字机效果显示文本
-const typeWriterEffect = async (index: number, text: string, delay: number = 50) => {
+const typeWriterEffect = async (text: string, delay: number = 30) => {
+  // 添加一个空的 AI 消息
+  currentMessages.value.push({
+    role: 'assistant',
+    content: '',
+    time: new Date().toLocaleString()
+  })
+
+  const msgIndex = currentMessages.value.length - 1
+
   for (let i = 0; i < text.length; i++) {
-    if (messages.value[index]) {
-      messages.value[index].content += text[i]
+    if (currentMessages.value[msgIndex]) {
+      currentMessages.value[msgIndex].content += text[i]
       scrollToBottom()
     }
     await new Promise(resolve => setTimeout(resolve, delay))
   }
+
+  // 只保留最近10条当前会话记录
+  if (currentMessages.value.length > 10) {
+    currentMessages.value = currentMessages.value.slice(-10)
+  }
 }
 
 // AI 对话接口 (同时支持 SSE 和普通 JSON)
 const sendChatMessage = async (question: string): Promise<void> => {
-  // 先添加一个空的 AI 消息
-  const aiMessageIndex = messages.value.length
-  messages.value.push({
-    role: 'assistant',
-    content: '',
-    time: new Date().toLocaleString()
-  })
-
   try {
     // 先尝试普通 POST 请求(适用于代理模式,后端返回 JSON)
     const response = await fetch(`/api/upms/ai/chat`, {
@@ -219,33 +311,44 @@ const sendChatMessage = async (question: string): Promise<void> => {
 
     if (contentType.includes('text/event-stream')) {
       // SSE 流式响应(Mock 模式)
-      await handleSSEResponse(response, aiMessageIndex)
+      await handleSSEResponse(response)
     } else {
       // JSON 响应(代理模式)
       const data = await response.json()
       const reply = data.data?.reply || data.reply || '暂无回复'
       // 使用打字机效果显示
-      await typeWriterEffect(aiMessageIndex, reply, 30)
+      await typeWriterEffect(reply, 30)
     }
   } catch (error) {
     console.error('AI 请求失败:', error)
     // 尝试 SSE 方式(兼容旧版 Mock)
     try {
-      await handleSSEFallback(question, aiMessageIndex)
+      await handleSSEFallback(question)
     } catch (sseError) {
       console.error('SSE 也失败了:', sseError)
-      if (messages.value[aiMessageIndex]) {
-        messages.value[aiMessageIndex].content = '抱歉,AI 服务暂时不可用,请稍后再试。'
-      }
+      // 添加错误消息
+      currentMessages.value.push({
+        role: 'assistant',
+        content: '抱歉,AI 服务暂时不可用,请稍后再试。',
+        time: new Date().toLocaleString()
+      })
     }
   }
 }
 
 // 处理 SSE 流响应
-const handleSSEResponse = async (response: Response, index: number): Promise<void> => {
+const handleSSEResponse = async (response: Response): Promise<void> => {
   const reader = response.body?.getReader()
   if (!reader) throw new Error('无法读取响应流')
 
+  // 添加一个空的 AI 消息
+  currentMessages.value.push({
+    role: 'assistant',
+    content: '',
+    time: new Date().toLocaleString()
+  })
+
+  const msgIndex = currentMessages.value.length - 1
   const decoder = new TextDecoder()
   let buffer = ''
 
@@ -261,8 +364,8 @@ const handleSSEResponse = async (response: Response, index: number): Promise<voi
       if (line.startsWith('data: ')) {
         try {
           const data = JSON.parse(line.slice(6))
-          if (messages.value[index]) {
-            messages.value[index].content += data.content
+          if (currentMessages.value[msgIndex]) {
+            currentMessages.value[msgIndex].content += data.content
             scrollToBottom()
           }
         } catch (e) {
@@ -271,22 +374,39 @@ const handleSSEResponse = async (response: Response, index: number): Promise<voi
       }
     }
   }
+
+  // 只保留最近10条当前会话记录
+  if (currentMessages.value.length > 10) {
+    currentMessages.value = currentMessages.value.slice(-10)
+  }
 }
 
 // SSE 降级方案(使用 EventSource)
-const handleSSEFallback = (question: string, index: number): Promise<void> => {
+const handleSSEFallback = (question: string): Promise<void> => {
   return new Promise((resolve, reject) => {
+    // 添加一个空的 AI 消息
+    currentMessages.value.push({
+      role: 'assistant',
+      content: '',
+      time: new Date().toLocaleString()
+    })
+
+    const msgIndex = currentMessages.value.length - 1
     const eventSource = new EventSource(`/api/upms/ai/chat?question=${encodeURIComponent(question)}`)
 
     eventSource.onmessage = (event) => {
       try {
         const data = JSON.parse(event.data)
-        if (messages.value[index]) {
-          messages.value[index].content += data.content
+        if (currentMessages.value[msgIndex]) {
+          currentMessages.value[msgIndex].content += data.content
           scrollToBottom()
         }
         if (data.done) {
           eventSource.close()
+          // 只保留最近10条当前会话记录
+          if (currentMessages.value.length > 10) {
+            currentMessages.value = currentMessages.value.slice(-10)
+          }
           resolve()
         }
       } catch (error) {
@@ -296,7 +416,11 @@ const handleSSEFallback = (question: string, index: number): Promise<void> => {
 
     eventSource.onerror = (error) => {
       eventSource.close()
-      if (messages.value[index]?.content) {
+      if (currentMessages.value[msgIndex]?.content) {
+        // 只保留最近10条当前会话记录
+        if (currentMessages.value.length > 10) {
+          currentMessages.value = currentMessages.value.slice(-10)
+        }
         resolve()
       } else {
         reject(error)
@@ -320,7 +444,7 @@ const handleEnter = (e: KeyboardEvent) => {
 
 // 清空对话
 const clearChat = () => {
-  messages.value = [{
+  currentMessages.value = [{
     role: 'assistant',
     content: '对话已清空,我是您的 AI 智能助手,有什么可以帮助您的吗?',
     time: new Date().toLocaleString()
@@ -330,7 +454,8 @@ const clearChat = () => {
 
 // 导出聊天记录
 const exportChat = () => {
-  const chatText = messages.value.map(m => {
+  const allMessages = [...historyMessages.value, ...currentMessages.value]
+  const chatText = allMessages.map(m => {
     return `[${m.time}] ${m.role === 'user' ? '我' : 'AI'}: ${m.content}`
   }).join('\n\n')
 
@@ -348,7 +473,8 @@ const exportChat = () => {
 }
 
 onMounted(() => {
-  scrollToBottom()
+  // 初始加载历史记录
+  loadHistory()
 })
 </script>
 
@@ -396,6 +522,23 @@ onMounted(() => {
   background-color: #f5f7fa;
 }
 
+.loading-more, .no-more-history {
+  text-align: center;
+  padding: 15px 0;
+  color: #909399;
+  font-size: 13px;
+}
+
+.loading-icon {
+  animation: rotating 1s linear infinite;
+  margin-right: 8px;
+}
+
+@keyframes rotating {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+
 .message {
   display: flex;
   gap: 12px;