Quellcode durchsuchen

修复代理模式下 AI 展示问题

- 前端同时支持 JSON 和 SSE 两种响应格式
- POST 接口返回 JSON(适用于代理模式)
- GET 接口返回 SSE 流(适用于 Mock 模式)
- 添加打字机效果,提升用户体验

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tanlie vor 3 Wochen
Ursprung
Commit
f292ab363f
2 geänderte Dateien mit 146 neuen und 41 gelöschten Zeilen
  1. 41 1
      src/mock/index.ts
  2. 105 40
      src/views/ai-chat/index.vue

+ 41 - 1
src/mock/index.ts

@@ -153,7 +153,47 @@ export default [
     },
   },
 
-  // AI 对话接口 (SSE 流式输出) - 支持 GET/POST
+  // AI 对话接口 (POST - 返回 JSON,适用于代理模式)
+  {
+    url: "/api/upms/ai/chat",
+    method: "post",
+    response: ({ body }) => {
+      const { question } = body;
+
+      // AI 回复知识库
+      const knowledgeBase: Record<string, string> = {
+        "如何管理系统用户?": "系统用户管理在「用户管理」菜单中,您可以进行以下操作:\n1. 查看用户列表\n2. 添加新用户\n3. 编辑用户信息\n4. 禁用/启用用户账号",
+        "如何配置菜单权限?": "菜单权限配置在「系统管理 > 菜单管理」中:\n1. 创建菜单项\n2. 设置菜单图标和路径\n3. 为角色分配菜单权限\n4. 保存后权限即时生效",
+        "系统有哪些功能模块?": "本系统包含以下功能模块:\n- 仪表盘:数据概览\n- 用户管理:系统用户管理\n- 系统管理:菜单、角色管理\n- 数据统计:报表分析\n- AI 助手:智能问答",
+        "如何导出数据报表?": "导出数据报表步骤:\n1. 进入「数据统计」页面\n2. 选择查询条件\n3. 点击「导出」按钮\n4. 选择导出格式(Excel/PDF)\n5. 下载生成的报表文件",
+        "如何修改个人资料?": "修改个人资料:\n1. 点击右上角用户名\n2. 选择「个人中心」\n3. 编辑个人信息\n4. 点击「保存」按钮",
+        "你好": "您好!很高兴为您服务,请问有什么可以帮助您的?",
+        "帮助": "我可以帮您:\n- 解答系统使用问题\n- 提供操作指导\n- 解释功能说明\n请直接输入您的问题!"
+      };
+
+      // 查找回复
+      let reply = '';
+      for (const key in knowledgeBase) {
+        if (question.includes(key) || key.includes(question)) {
+          reply = knowledgeBase[key];
+          break;
+        }
+      }
+
+      // 默认回复
+      if (!reply) {
+        reply = `感谢您的提问!关于"${question}",我建议您:\n\n1. 查看相关功能模块的说明文档\n2. 联系系统管理员获取帮助\n3. 或者尝试在快捷问题中寻找类似问题\n\n如果您需要更详细的帮助,请联系技术支持。`;
+      }
+
+      return {
+        code: 200,
+        message: "success",
+        data: { reply }
+      };
+    },
+  },
+
+  // AI 对话接口 (GET - SSE 流式输出,适用于 Mock 模式)
   {
     url: "/api/upms/ai/chat",
     method: "get",

+ 105 - 40
src/views/ai-chat/index.vue

@@ -183,59 +183,124 @@ const sendMessage = async () => {
   }
 }
 
-// AI 对话接口 (SSE 流式输出)
+// 打字机效果显示文本
+const typeWriterEffect = async (index: number, text: string, delay: number = 50) => {
+  for (let i = 0; i < text.length; i++) {
+    if (messages.value[index]) {
+      messages.value[index].content += text[i]
+      scrollToBottom()
+    }
+    await new Promise(resolve => setTimeout(resolve, delay))
+  }
+}
+
+// AI 对话接口 (同时支持 SSE 和普通 JSON)
 const sendChatMessage = async (question: string): Promise<void> => {
-  return new Promise((resolve, reject) => {
+  // 先添加一个空的 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`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({ question })
+    })
+
+    // 检查响应类型
+    const contentType = response.headers.get('content-type') || ''
+
+    if (contentType.includes('text/event-stream')) {
+      // SSE 流式响应(Mock 模式)
+      await handleSSEResponse(response, aiMessageIndex)
+    } else {
+      // JSON 响应(代理模式)
+      const data = await response.json()
+      const reply = data.data?.reply || data.reply || '暂无回复'
+      // 使用打字机效果显示
+      await typeWriterEffect(aiMessageIndex, reply, 30)
+    }
+  } catch (error) {
+    console.error('AI 请求失败:', error)
+    // 尝试 SSE 方式(兼容旧版 Mock)
     try {
-      // 先添加一个空的 AI 消息
-      const aiMessageIndex = messages.value.length
-      messages.value.push({
-        role: 'assistant',
-        content: '',
-        time: new Date().toLocaleString()
-      })
-
-      // 创建 SSE 连接
-      const eventSource = new EventSource(`/api/upms/ai/chat?question=${encodeURIComponent(question)}`)
-
-      eventSource.onmessage = (event) => {
-        try {
-          const data = JSON.parse(event.data)
+      await handleSSEFallback(question, aiMessageIndex)
+    } catch (sseError) {
+      console.error('SSE 也失败了:', sseError)
+      if (messages.value[aiMessageIndex]) {
+        messages.value[aiMessageIndex].content = '抱歉,AI 服务暂时不可用,请稍后再试。'
+      }
+    }
+  }
+}
 
-          // 追加内容到 AI 消息
-          if (messages.value[aiMessageIndex]) {
-            messages.value[aiMessageIndex].content += data.content
-          }
+// 处理 SSE 流响应
+const handleSSEResponse = async (response: Response, index: number): Promise<void> => {
+  const reader = response.body?.getReader()
+  if (!reader) throw new Error('无法读取响应流')
 
-          // 滚动到底部
-          scrollToBottom()
+  const decoder = new TextDecoder()
+  let buffer = ''
 
-          // 如果完成了,关闭连接
-          if (data.done) {
-            eventSource.close()
-            resolve()
+  while (true) {
+    const { done, value } = await reader.read()
+    if (done) break
+
+    buffer += decoder.decode(value, { stream: true })
+    const lines = buffer.split('\n')
+    buffer = lines.pop() || ''
+
+    for (const line of lines) {
+      if (line.startsWith('data: ')) {
+        try {
+          const data = JSON.parse(line.slice(6))
+          if (messages.value[index]) {
+            messages.value[index].content += data.content
+            scrollToBottom()
           }
-        } catch (error) {
-          console.error('解析 SSE 数据失败:', error)
+        } catch (e) {
+          // 忽略解析错误
         }
       }
+    }
+  }
+}
 
-      eventSource.onerror = (error) => {
-        console.error('SSE 连接错误:', error)
-        eventSource.close()
+// SSE 降级方案(使用 EventSource)
+const handleSSEFallback = (question: string, index: number): Promise<void> => {
+  return new Promise((resolve, reject) => {
+    const eventSource = new EventSource(`/api/upms/ai/chat?question=${encodeURIComponent(question)}`)
 
-        // 如果有内容,则认为是正常结束
-        if (messages.value[aiMessageIndex]?.content) {
+    eventSource.onmessage = (event) => {
+      try {
+        const data = JSON.parse(event.data)
+        if (messages.value[index]) {
+          messages.value[index].content += data.content
+          scrollToBottom()
+        }
+        if (data.done) {
+          eventSource.close()
           resolve()
-        } else {
-          // 如果没有内容,显示错误信息
-          messages.value[aiMessageIndex].content = '抱歉,AI 服务暂时不可用,请稍后再试。'
-          reject(error)
         }
+      } catch (error) {
+        console.error('解析 SSE 数据失败:', error)
+      }
+    }
+
+    eventSource.onerror = (error) => {
+      eventSource.close()
+      if (messages.value[index]?.content) {
+        resolve()
+      } else {
+        reject(error)
       }
-    } catch (error) {
-      console.error('创建 SSE 连接失败:', error)
-      reject(error)
     }
   })
 }