瀏覽代碼

增加智能AI问答

jiuling 10 月之前
父節點
當前提交
b4c01a44d7

+ 6 - 1
src/App.vue

@@ -2,15 +2,20 @@
   <div id="app">
     <router-view />
     <theme-picker />
+    <floating-chat />
   </div>
 </template>
 
 <script>
 import ThemePicker from "@/components/ThemePicker"
+import FloatingChat from "@/components/FloatingChat/index.vue"
 
 export default {
   name: "App",
-  components: { ThemePicker },
+  components: { 
+    ThemePicker,
+    FloatingChat
+  },
   metaInfo() {
     return {
       title: this.$store.state.settings.dynamicTitle && this.$store.state.settings.title,

+ 53 - 0
src/api/ai/chat.js

@@ -0,0 +1,53 @@
+import request from '@/utils/request'
+import { getToken } from '@/utils/auth'
+// 获取AI聊天的SSE连接URL
+// 直接返回SSE连接的URL
+export function getChatStreamUrl() {
+  // 根据实际部署环境调整基础URL
+  const baseUrl = process.env.VUE_APP_BASE_API || '';
+  return `uniapp/dify/chat/stream`;
+}
+
+// 普通的非流式AI聊天请求
+export function postChatMessage(data) {
+  return request({
+    url: 'uniapp/dify/chat',
+    method: 'post',
+    data: data
+  })
+}
+
+// 发送流式AI聊天请求(返回fetch Promise)
+export function postChatMessageData(data) {
+  const baseUrl = process.env.VUE_APP_BASE_API || '';
+  const url = `${baseUrl}/uniapp/dify/chat/stream`;
+  
+  return fetch(url, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+      'Accept': 'text/event-stream',
+      'Authorization': 'Bearer ' + getToken()
+    },
+    body: JSON.stringify(data),
+    credentials: 'include', // 确保发送凭证(cookie等)
+    mode: 'cors'           // 允许跨域请求
+  });
+}
+
+// 终止流式请求
+export function stopChatStream(data) {
+  return request({
+    url: '/uniapp/dify/chat/stop',
+    method: 'post',
+    data: data
+  })
+} 
+// 获取下一轮建议问题列表
+export function chatStreamSuggested(data) {
+  return request({
+    url: '/uniapp/dify/chat/suggested',
+    method: 'get',
+    params: data
+  })
+} 

二進制
src/assets/icons/ai.png


二進制
src/assets/icons/chat.png


二進制
src/assets/icons/chat_off.png


+ 0 - 0
src/assets/icons/user-avatar.png


+ 0 - 0
src/assets/images/chat-bg-pattern.png


+ 298 - 0
src/components/FloatingChat/index.vue

@@ -0,0 +1,298 @@
+<template>
+  <div class="floating-chat-container">
+    <!-- 悬浮按钮 -->
+    <div class="floating-button" @click="toggleChat" v-show="!isOpen || isMobile">
+      <img src="@/assets/icons/ai.png" v-if="!isOpen" alt="聊天" />
+      <img src="@/assets/icons/chat_off.png" v-else alt="关闭聊天" />
+    </div>
+    
+    <!-- 悬浮聊天窗口 -->
+    <div class="floating-chat-window" v-show="isOpen" :class="{ 'mobile-view': isMobile }" :style="chatWindowStyle">
+      <div class="chat-header">
+        <div class="header-title"
+             @mousedown="onDragStart"
+             @touchstart="onDragStart">
+          <!-- <i class="el-icon-s-opportunity"></i> -->
+          <img src="@/assets/icons/ai.png" alt="Custom Icon" class="custom-icon">
+          <span style="padding-left: 5px;">AI 智能助手</span>
+        </div>
+        <div class="header-actions">
+          <i class="el-icon-minus" @click="minimizeChat" v-if="!isMobile"></i>
+          <i class="el-icon-close" @click="closeChat"></i>
+        </div>
+      </div>
+      <div class="chat-body">
+        <ai-chat ref="aiChat" :is-floating="true"></ai-chat>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import AiChat from "@/views/ai-chat/index.vue";
+
+export default {
+  name: "FloatingChat",
+  components: {
+    AiChat
+  },
+  data() {
+    return {
+      isOpen: false,
+      isMobile: false,
+      position: {
+        x: 20, // 初始右侧距离
+        y: 20  // 初始底部距离
+      },
+      isDragging: false,
+      dragOffset: {
+        x: 0,
+        y: 0
+      }
+    };
+  },
+  computed: {
+    chatWindowStyle() {
+      if (this.isMobile) return {};
+      
+      return {
+        right: this.position.x + 'px',
+        bottom: this.position.y + 'px'
+      };
+    }
+  },
+  mounted() {
+    this.checkMobile();
+    window.addEventListener('resize', this.checkMobile);
+    
+    // 添加全局事件监听器用于拖拽
+    document.addEventListener('mousemove', this.onMouseMove);
+    document.addEventListener('mouseup', this.onMouseUp);
+    document.addEventListener('touchmove', this.onTouchMove);
+    document.addEventListener('touchend', this.onTouchEnd);
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.checkMobile);
+    
+    // 移除全局事件监听器
+    document.removeEventListener('mousemove', this.onMouseMove);
+    document.removeEventListener('mouseup', this.onMouseUp);
+    document.removeEventListener('touchmove', this.onTouchMove);
+    document.removeEventListener('touchend', this.onTouchEnd);
+  },
+  methods: {
+    toggleChat() {
+      this.isOpen = !this.isOpen;
+    },
+    closeChat() {
+      this.isOpen = false;
+    },
+    minimizeChat() {
+      this.isOpen = false;
+    },
+    checkMobile() {
+      this.isMobile = window.innerWidth < 768;
+    },
+    
+    // 开始拖拽
+    onDragStart(event) {
+      if (this.isMobile) return;
+      
+      this.isDragging = true;
+      
+      // 计算鼠标位置与窗口边缘的偏移
+      const header = event.target.closest('.chat-header');
+      const rect = header.getBoundingClientRect();
+      
+      if (event.type === 'mousedown') {
+        this.dragOffset.x = event.clientX - rect.left;
+        this.dragOffset.y = event.clientY - rect.top;
+      } else if (event.type === 'touchstart') {
+        const touch = event.touches[0];
+        this.dragOffset.x = touch.clientX - rect.left;
+        this.dragOffset.y = touch.clientY - rect.top;
+      }
+      
+      event.preventDefault();
+    },
+    
+    // 处理鼠标移动
+    onMouseMove(event) {
+      if (!this.isDragging) return;
+      
+      const windowWidth = window.innerWidth;
+      const windowHeight = window.innerHeight;
+      const chatWindow = this.$el.querySelector('.floating-chat-window');
+      const chatWidth = chatWindow.offsetWidth;
+      const chatHeight = chatWindow.offsetHeight;
+      
+      // 计算新的窗口位置
+      const right = windowWidth - (event.clientX - this.dragOffset.x + chatWidth);
+      const bottom = windowHeight - (event.clientY - this.dragOffset.y + chatHeight);
+      
+      // 限制窗口不超出屏幕边界
+      this.position.x = Math.max(0, Math.min(right, windowWidth - 50));
+      this.position.y = Math.max(0, Math.min(bottom, windowHeight - 50));
+    },
+    
+    // 处理触摸移动
+    onTouchMove(event) {
+      if (!this.isDragging) return;
+      
+      const touch = event.touches[0];
+      const windowWidth = window.innerWidth;
+      const windowHeight = window.innerHeight;
+      const chatWindow = this.$el.querySelector('.floating-chat-window');
+      const chatWidth = chatWindow.offsetWidth;
+      const chatHeight = chatWindow.offsetHeight;
+      
+      // 计算新的窗口位置
+      const right = windowWidth - (touch.clientX - this.dragOffset.x + chatWidth);
+      const bottom = windowHeight - (touch.clientY - this.dragOffset.y + chatHeight);
+      
+      // 限制窗口不超出屏幕边界
+      this.position.x = Math.max(0, Math.min(right, windowWidth - 50));
+      this.position.y = Math.max(0, Math.min(bottom, windowHeight - 50));
+      
+      event.preventDefault();
+    },
+    
+    // 结束拖拽
+    onMouseUp() {
+      this.isDragging = false;
+    },
+    
+    onTouchEnd() {
+      this.isDragging = false;
+    }
+  }
+};
+</script>
+
+<style scoped>
+.floating-chat-container {
+  position: fixed;
+  right: 20px;
+  bottom: 20px;
+  z-index: 9999;
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+}
+
+.floating-button {
+  width: 50px;
+  height: 50px;
+  border-radius: 50%;
+  background-color: #67c23a;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  transition: all 0.3s ease;
+  z-index: 10000;
+}
+
+.floating-button:hover {
+  transform: scale(1.05);
+  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
+}
+
+.floating-button img {
+  width: 28px;
+  height: 28px;
+  object-fit: contain;
+}
+
+.floating-chat-window {
+  position: fixed;
+  right: 20px;
+  bottom: 80px;
+  width: 380px;
+  height: 500px;
+  background-color: #fff;
+  border-radius: 8px;
+  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  animation: slideIn 0.3s ease;
+}
+
+.floating-chat-window.mobile-view {
+  width: 100%;
+  height: 100%;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  border-radius: 0;
+}
+
+@keyframes slideIn {
+  from { 
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to { 
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.chat-header {
+  height: 50px;
+  background-image: linear-gradient(to right, #67c23a, #4caf50);
+  color: white;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 15px;
+  cursor: move;
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+}
+
+.header-title {
+  display: flex;
+  align-items: center;
+  font-weight: bold;
+  flex: 1;
+  cursor: move;
+}
+
+.header-title i {
+  margin-right: 8px;
+  font-size: 18px;
+}
+
+.header-actions {
+  display: flex;
+  align-items: center;
+}
+
+.header-actions i {
+  margin-left: 15px;
+  cursor: pointer;
+  font-size: 16px;
+  transition: all 0.3s ease;
+}
+
+.header-actions i:hover {
+  transform: scale(1.1);
+}
+
+.chat-body {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+.custom-icon {
+  width: 34px; /* 设置图标宽度 */
+  height: 34px; /* 设置图标高度 */
+  vertical-align: middle; /* 垂直对齐 */
+}
+</style> 

+ 1034 - 0
src/views/ai-chat/index.vue

@@ -0,0 +1,1034 @@
+<template>
+  <div :class="containerClass">
+    <el-card class="box-card" v-if="!isFloating">
+      <div slot="header" class="clearfix">
+        <span>AI 智能问答</span>
+      </div>
+      <div class="chat-container">
+        <!-- 聊天记录区域 -->
+        <div class="chat-messages" ref="chatMessages">
+          <div v-for="(message, index) in chatHistory" :key="index" :class="['message', message.role]">
+            <div class="message-content">
+              <div class="message-avatar" v-if="message.role === 'user'">
+                <i class="el-icon-user"></i>
+              </div>
+              <div class="message-avatar ai-avatar" v-else-if="message.role === 'assistant'">
+                <i class="el-icon-s-opportunity"></i>
+              </div>
+              <div class="message-text" v-html="message.content">
+              </div>
+              <div class="message-completed" v-if="message.role === 'assistant' && message.completed">
+                <i class="el-icon-check"></i>
+              </div>
+            </div>
+            
+            <!-- 建议问题区域 -->
+            <div class="suggested-questions" v-if="message.role === 'assistant' && message.completed && message.suggestions && message.suggestions.length > 0">
+              <div class="suggestion-title">您可能想问:</div>
+              <div class="suggestion-list">
+                <div v-for="(suggestion, idx) in message.suggestions" :key="idx" 
+                     class="suggestion-item" @click="handleSuggestionClick(suggestion)">
+                  {{ suggestion }}
+                </div>
+              </div>
+            </div>
+          </div>
+          
+          <!-- AI思考中提示 -->
+          <div class="message system thinking-message" v-if="isThinking">
+            <div class="message-content">
+              <div class="thinking-indicator">
+                <i class="el-icon-loading"></i>
+                <span v-html="aiThought"></span>
+              </div>
+            </div>
+          </div>
+          
+          <!-- 打字机效果区域 -->
+          <div class="message ai" v-if="isStreaming">
+            <div class="message-content">
+              <div class="message-avatar ai-avatar">
+                <i class="el-icon-s-opportunity"></i>
+              </div>
+              <div class="message-text">
+                <div v-html="currentStreamingText"></div>
+              </div>
+            </div>
+          </div>
+          
+          <!-- 加载中提示 -->
+          <div class="loading-indicator" v-if="isLoading && !isStreaming">
+            <div class="loading-content">
+              <i class="el-icon-loading"></i>
+              <span>正在思考中...</span>
+            </div>
+          </div>
+        </div>
+        
+        <!-- 输入区域 -->
+        <div class="chat-input">
+          <el-input
+            type="textarea"
+            :rows="3"
+            placeholder="请输入您的问题..."
+            v-model="userInput"
+            :disabled="isLoading || isStreaming"
+            @keyup.ctrl.enter.native="sendMessage"
+          ></el-input>
+          <div class="button-container">
+            <el-button type="primary" :disabled="isLoading || isStreaming || !userInput.trim()" @click="sendMessage">发送</el-button>
+            <el-button v-if="isStreaming" @click="stopStreaming">停止</el-button>
+            <el-button @click="clearChat">清空对话</el-button>
+          </div>
+        </div>
+      </div>
+    </el-card>
+
+    <!-- 悬浮模式 -->
+    <div class="chat-container floating-chat-container" v-else>
+      <!-- 聊天记录区域 -->
+      <div class="chat-messages floating-chat-messages" ref="chatMessages">
+        <div v-for="(message, index) in chatHistory" :key="index" :class="['message', message.role]">
+          <div class="message-content">
+            <div class="message-avatar" v-if="message.role === 'user'">
+              <i class="el-icon-user"></i>
+            </div>
+            <div class="message-avatar ai-avatar" v-else-if="message.role === 'assistant'">
+              <!-- <i class="el-icon-s-opportunity"></i> -->
+              <img src="@/assets/icons/ai.png" alt="Custom Icon" class="custom-icon">
+            </div>
+            <div class="message-text" v-html="message.content">
+            </div>
+            <div class="message-completed" v-if="message.role === 'assistant' && message.completed">
+              <i class="el-icon-check"></i>
+            </div>
+          </div>
+          
+          <!-- 建议问题区域 -->
+          <div class="suggested-questions" v-if="message.role === 'assistant' && message.completed && message.suggestions && message.suggestions.length > 0">
+            <div class="suggestion-title">您可能想问:</div>
+            <div class="suggestion-list">
+              <div v-for="(suggestion, idx) in message.suggestions" :key="idx" 
+                   class="suggestion-item" @click="handleSuggestionClick(suggestion)">
+                {{ suggestion }}
+              </div>
+            </div>
+          </div>
+        </div>
+        
+        <!-- AI思考中提示 -->
+        <!-- <div class="message system thinking-message" v-if="isThinking">
+          <div class="message-content">
+            <div class="thinking-indicator">
+              <i class="el-icon-loading"></i>
+              <span v-html="aiThought"></span>
+            </div>
+          </div>
+        </div> -->
+        
+        <!-- 打字机效果区域 -->
+        <div class="message ai" v-if="isStreaming">
+          <div class="message-content">
+            <div class="message-avatar ai-avatar">
+              <img src="@/assets/icons/ai.png" alt="Custom Icon" class="custom-icon">
+            </div>
+            <div class="message-text">
+              <div v-html="currentStreamingText"></div>
+            </div>
+          </div>
+        </div>
+        
+        <!-- 加载中提示 -->
+        <div class="loading-indicator" v-if="isStreaming">
+          <div class="loading-content">
+            <i class="el-icon-loading"></i>
+            <span>正在思考中...</span>
+          </div>
+        </div>
+      </div>
+      
+      <!-- 输入区域 -->
+      <div class="chat-input floating-chat-input">
+        <el-input
+          type="textarea"
+          :rows="2"
+          placeholder="请输入您的问题..."
+          v-model="userInput"
+          :disabled="isLoading || isStreaming"
+          @keyup.ctrl.enter.native="sendMessage"
+        ></el-input>
+        <div class="button-container">
+          <el-button type="primary" :disabled="isLoading || isStreaming || !userInput.trim()" @click="sendMessage">发送</el-button>
+          <el-button v-if="isStreaming && currentStreamingText" @click="stopStreaming">停止</el-button>
+          <el-button @click="clearChat">清空</el-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { chatStreamSuggested, postChatMessageData, stopChatStream } from "@/api/ai/chat";
+
+export default {
+  name: "AiChat",
+  props: {
+    isFloating: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      userInput: "",
+      chatHistory: [],
+      isLoading: false,
+      isStreaming: false,
+      currentStreamingText: "",
+      eventSource: null,
+      messageQueue: [],
+      sessionId: null,
+      isThinking: false, // 新增思考中状态
+      aiThought: "", // 新增思考内容
+      taskId: null, // 任务ID,用于停止响应
+      messageId: null, // 消息ID,用于标识当前消息
+      isProcessingQueue: false, // 标记是否正在处理消息队列
+    };
+  },
+  computed: {
+    containerClass() {
+      return {
+        'app-container': !this.isFloating,
+        'floating-container': this.isFloating
+      };
+    }
+  },
+  mounted() {
+    this.scrollToBottom();
+  },
+  updated() {
+    this.scrollToBottom();
+  },
+  methods: {
+    sendMessage() {
+      if (!this.userInput.trim()) return;
+      
+      // 添加用户消息到聊天记录
+      const userMessage = {
+        role: "user",
+        content: this.userInput.replace(/\n/g, "<br>")
+      };
+      this.chatHistory.push(userMessage);
+      
+      // 清空输入框
+      const question = this.userInput;
+      this.userInput = "";
+      
+      // 设置加载状态
+      this.isLoading = true;
+      this.isThinking = false; // 确保思考状态为false
+      this.aiThought = ""; // 清空思考内容
+      
+      // 显示正在思考的提示
+      // this.$message({
+      //   message: '正在思考您的问题...',
+      //   type: 'info',
+      //   duration: 2000
+      // });
+      
+      // 开始接收流式响应
+      this.startStreaming(question);
+    },
+    
+    startStreaming(question) {
+      this.isLoading = false;
+      this.isStreaming = true;
+      this.currentStreamingText = "";
+      this.isThinking = false; // 确保思考状态为false
+      this.aiThought = ""; // 清空思考内容
+      
+      // 关闭之前的连接(如果有)
+      if (this.eventSource) {
+        this.eventSource.close();
+      }
+      
+      // 生成一个随机的会话ID,用于停止流
+      this.sessionId = Date.now().toString();
+      
+      // 创建消息对象
+      const messageData = {
+        query: question,
+        user: this.sessionId
+      };
+      console.log("this.sessionId:开始",this.sessionId);
+      // 使用fetch发送POST请求并处理流式响应
+      postChatMessageData(messageData).then(response => {
+        if (!response.ok) {
+          throw new Error(`HTTP错误 ${response.status} ${response.statusText}`);
+        }
+        
+        // 获取响应流的读取器
+        const reader = response.body.getReader();
+        const decoder = new TextDecoder('utf-8');
+        let buffer = '';
+        
+        // 读取流数据
+        const readStream = () => {
+          if (!this.isStreaming) {
+            console.log("流已停止");
+            return;
+          }
+          
+          reader.read().then(({ done, value }) => {
+            if (done) {
+              console.log("流读取完成");
+              this.finishStreaming();
+              return;
+            }
+            
+            try {
+              // 解码接收到的数据块
+              buffer += decoder.decode(value, { stream: true });
+              
+              // 处理SSE格式的数据(按行拆分)
+              const lines = buffer.split('\n');
+              buffer = lines.pop() || '';  // 最后一行可能不完整,保存到buffer
+              
+              // 变量来跟踪当前事件类型和数据
+              let currentEvent = 'agent_message';  // 默认为agent_message事件类型
+              let currentData = '';
+              
+              for (let i = 0; i < lines.length; i++) {
+                const line = lines[i].trim();
+                
+                // 空行表示一个事件的结束
+                if (line === '') {
+                  if (currentData) {
+                    // 如果有数据,处理当前事件
+                    this.handleEvent(currentEvent, currentData);
+                    currentData = '';
+                  }
+                  continue;
+                }
+                
+                // 事件类型行
+                if (line.startsWith('event:')) {
+                  currentEvent = line.slice(6).trim();
+                  continue;
+                }
+                
+                // 数据行
+                if (line.startsWith('data:')) {
+                  const dataContent = line.slice(5).trim();
+                  if (dataContent) {
+                    // 初始化或追加数据
+                    if (currentData) {
+                      currentData += '\n' + dataContent;
+                    } else {
+                      currentData = dataContent;
+                    }
+                  }
+                  continue;
+                }
+              }
+              
+              // 处理缓冲区中可能剩余的完整事件
+              if (currentData) {
+                this.handleEvent(currentEvent, currentData);
+              }
+              
+              // 继续读取流
+              readStream();
+            } catch (error) {
+              console.error("数据解析错误:", error);
+              this.handleError("数据解析错误: " + (error.message || "未知错误"));
+              this.finishStreaming();
+            }
+          }).catch(error => {
+            console.error("Stream reading error:", error);
+            this.handleError("读取数据流错误: " + (error.message || "未知错误"));
+            this.finishStreaming();
+          });
+        };
+        
+        // 开始读取流
+        readStream();
+        
+      }).catch(error => {
+        console.error("Fetch error:", error);
+        this.handleError("请求错误: " + (error.message || "未知错误"));
+        this.finishStreaming();
+      });
+    },
+    
+    processMessageQueue() {
+      this.isProcessingQueue = true;
+      
+      const processNextChunk = () => {
+        if (this.messageQueue.length > 0) {
+          // 每次只处理一小部分文本,以实现打字机效果
+          const chunk = this.messageQueue.shift();
+          
+          // 处理内容 - 不需要类型检查,直接添加内容到当前流式文本
+          this.currentStreamingText += chunk;
+          
+          // 滚动到底部
+          this.scrollToBottom();
+          
+          // 使用setTimeout延迟处理下一个块,创造打字机效果
+          setTimeout(() => {
+            processNextChunk();
+          }, 10); // 调整延迟时间可以控制打字速度
+        } else {
+          this.isProcessingQueue = false;
+          setTimeout(() => {
+            if (this.messageQueue.length > 0) {
+              this.processMessageQueue();
+            }
+          }, 20);
+        }
+      };
+      
+      processNextChunk();
+    },
+    
+    finishStreaming() {
+      // 设置流结束标志,流读取器会检查这个标志
+      this.isStreaming = false;
+      this.isThinking = false;
+      
+      // 等待所有消息处理完毕
+      setTimeout(() => {
+        if (this.currentStreamingText) {
+          // 创建消息对象
+          const assistantMessage = {
+            role: "assistant",
+            content: this.currentStreamingText,
+            completed: true, // 添加完成标记
+            suggestions: [] // 初始化建议问题数组
+          };
+          
+          // 添加到聊天记录
+          this.chatHistory.push(assistantMessage);
+          
+          // 获取建议问题
+          this.fetchSuggestedQuestions(assistantMessage);
+        }
+        
+        // 重置状态
+        this.currentStreamingText = "";
+        this.isLoading = false;
+        this.messageQueue = [];
+        this.aiThought = "";
+        console.log("this.sessionId:结束",this.sessionId);
+        
+        // 最终滚动到底部确保内容可见
+        this.scrollToBottom();
+      }, 200); // 短暂延迟确保所有内容已处理完毕
+    },
+    
+    // 获取建议问题
+    fetchSuggestedQuestions(assistantMessage) {
+      // 调用API获取建议问题
+      chatStreamSuggested({user: this.sessionId,messageId:this.messageId}).then(response => {
+        if (response && response.data) {
+          // 检查API响应格式,可能直接是数组或者包装在data属性中
+          let suggestions = Array.isArray(response.data) ? response.data : 
+                           (Array.isArray(response.data.suggestions) ? response.data.suggestions : []);
+          
+          // 最多显示5个建议问题
+          assistantMessage.suggestions = suggestions.slice(0, 5);
+          
+          // 强制更新视图
+          this.$forceUpdate();
+          // 确保滚动到底部显示建议问题
+          this.scrollToBottom();
+        }
+      }).catch(error => {
+        console.error("获取建议问题失败:", error);
+      });
+    },
+    
+    stopStreaming() {
+      // 设置标志位停止流读取
+      this.isStreaming = false;
+      this.isThinking = false;
+      
+      // 调用API终止流
+      if (this.sessionId) {
+        stopChatStream({user:this.sessionId,taskId:this.taskId}).catch(error => {
+          console.error("Error stopping stream:", error);
+        });
+      }
+      
+      // 完成当前的流式响应
+      if (this.currentStreamingText) {
+        // 创建中断消息对象
+        const interruptedMessage = {
+          role: "assistant",
+          content: this.currentStreamingText + " [用户中断]",
+          completed: true,
+          suggestions: []
+        };
+        
+        // 将流式文本添加到聊天记录,标记为用户中断
+        this.chatHistory.push(interruptedMessage);
+        
+        // 尝试获取建议问题
+        this.fetchSuggestedQuestions(interruptedMessage);
+      }
+      
+      // 重置状态
+      this.currentStreamingText = "";
+      this.isLoading = false;
+      this.messageQueue = [];
+      this.aiThought = "";
+    },
+    
+    clearChat() {
+      this.chatHistory = [];
+      this.stopStreaming();
+      this.isThinking = false;
+      this.aiThought = "";
+    },
+    
+    scrollToBottom() {
+      this.$nextTick(() => {
+        if (this.$refs.chatMessages) {
+          const container = this.$refs.chatMessages;
+          container.scrollTop = container.scrollHeight;
+        }
+      });
+    },
+    
+    // 处理SSE事件
+    handleEvent(eventName, eventData) {
+      // console.log(`收到事件 ${eventName}:`, eventData);
+      
+      // 处理ping事件,不尝试解析为JSON
+      if (eventName === 'ping') {
+        console.log("收到ping事件");
+        return;
+      }
+      
+      try {
+        // 尝试解析JSON数据
+        const data = JSON.parse(eventData);
+        
+        switch (eventName) {
+          case 'agent_message':
+            // 处理agent_message事件
+            if (data && data.answer !== undefined) {
+              // 添加回答内容到消息队列
+              this.messageId = data.id; // 保存消息ID
+              this.taskId = data.task_id; // 保存任务ID用于停止响应
+              const answer = data.answer.toString();
+              if (answer.trim()) {
+                this.isThinking = false; // 收到回答,关闭思考状态
+                this.messageQueue.push(answer);
+                
+                if (!this.isProcessingQueue) {
+                  this.processMessageQueue();
+                }
+              }
+            }
+            break;
+            
+          case 'message':
+            // 处理message事件
+            if (data && data.answer !== undefined) {
+              const answer = data.answer.toString();
+              if (answer.trim()) {
+                this.isThinking = false; // 收到回答,关闭思考状态
+                this.messageQueue.push(answer);
+                
+                if (!this.isProcessingQueue) {
+                  this.processMessageQueue();
+                }
+              }
+            } else if (data && data.eventType === 'MESSAGE_END') {
+              console.log("Message end event received");
+              this.finishStreaming();
+            }
+            break;
+            
+          case 'message_end':
+            // 消息结束事件,完成流式处理
+            // console.log("Message end event received:", data);
+            this.finishStreaming();
+            break;
+            
+          case 'agent_thought':
+            // 处理思考过程
+            if (data && data.thought && data.thought.trim() !== '') {
+              // console.log("AI思考:", data.thought);
+              
+              // 格式化思考内容,添加前缀
+              // const formattedThought = `<strong>AI思考:</strong><br>${data.thought.replace(/\n/g, '<br>')}`;
+              
+              // 更新思考内容并显示思考状态
+              // this.aiThought = formattedThought;
+              this.isThinking = true;
+              
+              // 确保滚动到可见区域
+              // this.$nextTick(() => {
+              //   const thinkingElement = document.querySelector('.thinking-message');
+              //   if (thinkingElement) {
+              //     thinkingElement.scrollIntoView({ behavior: 'smooth', block: 'end' });
+              //   }
+              // });
+            }
+            break;
+            
+          case 'error':
+            // 处理错误事件
+            console.error("Error event:", data);
+            this.isThinking = false;
+            if (data && data.message) {
+              this.$message.error(`错误: ${data.message}`);
+            } else {
+              this.$message.error("发生未知错误");
+            }
+            this.finishStreaming();
+            break;
+            
+          default:
+            // 其他事件类型,尝试提取answer字段
+            if (data && data.answer !== undefined) {
+              const answer = data.answer.toString();
+              if (answer.trim()) {
+                this.isThinking = false; // 收到回答,关闭思考状态
+                this.messageQueue.push(answer);
+                
+                if (!this.isProcessingQueue) {
+                  this.processMessageQueue();
+                }
+              }
+            }
+        }
+      } catch (error) {
+        // 如果解析JSON失败,尝试直接使用原始数据
+        console.error("Error parsing event data:", error);
+        if (eventName === 'message_end') {
+          this.finishStreaming();
+        } else if (eventName === 'agent_thought') {
+          // 尝试处理可能的非JSON格式的思考内容
+          if (typeof eventData === 'string' && eventData.trim()) {
+            this.aiThought = eventData;
+            this.isThinking = true;
+          }
+        } else if (eventName !== 'error') {
+          // 非控制类事件,直接添加到消息队列
+          if (typeof eventData === 'string' && eventData.trim()) {
+            this.isThinking = false; // 收到回答,关闭思考状态
+            // 将原始文本添加到队列
+            this.messageQueue.push(eventData);
+            
+            if (!this.isProcessingQueue) {
+              this.processMessageQueue();
+            }
+          }
+        }
+      }
+    },
+
+    // 统一处理错误
+    handleError(errorMsg) {
+      this.$message.error(errorMsg);
+      
+      // 将错误信息显示在聊天窗口
+      if (this.currentStreamingText) {
+        this.chatHistory.push({
+          role: "assistant",
+          content: this.currentStreamingText
+        });
+      }
+      
+      // 使用系统消息添加错误信息
+      this.addSystemMessage(`<span style="color: red;"><i class="el-icon-warning"></i> ${errorMsg}</span>`);
+    },
+
+    // 添加系统消息到聊天记录
+    addSystemMessage(content) {
+      this.chatHistory.push({
+        role: "system",
+        content: content
+      });
+      this.scrollToBottom();
+    },
+
+    // 处理建议问题点击
+    handleSuggestionClick(suggestion) {
+      this.userInput = suggestion;
+      // this.sendMessage();
+    }
+  }
+};
+</script>
+
+<style scoped>
+.chat-container {
+  display: flex;
+  flex-direction: column;
+  height: 70vh;
+}
+
+.chat-messages {
+  flex: 1;
+  overflow-y: auto;
+  padding: 10px;
+  background-color: #f9f9f9;
+  background-image: url('~@/assets/images/chat-bg-pattern.png');
+  background-repeat: repeat;
+  border-radius: 4px;
+  margin-bottom: 10px;
+  box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05);
+}
+
+.message {
+  margin-bottom: 15px;
+  animation: fadeIn 0.5s ease;
+}
+
+@keyframes fadeIn {
+  from { opacity: 0; transform: translateY(10px); }
+  to { opacity: 1; transform: translateY(0); }
+}
+
+.message-content {
+  display: flex;
+}
+
+.message-avatar {
+  width: 36px;
+  height: 36px;
+  border-radius: 50%;
+  background-color: #409eff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: white;
+  margin-right: 10px;
+  flex-shrink: 0;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.ai-avatar {
+  background-color: #67c23a;
+}
+
+.message-text {
+  padding: 10px;
+  border-radius: 4px;
+  max-width: calc(100% - 60px);
+  word-break: break-word;
+  white-space: pre-wrap; /* 保留换行和空格 */
+  font-size: 14px;
+  line-height: 1.5;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.user .message-text {
+  background-color: #ecf5ff;
+  border: 1px solid #d9ecff;
+}
+
+.ai .message-text {
+  background-color: #f0f9eb;
+  border: 1px solid #e1f3d8;
+  background-image: linear-gradient(to bottom right, #f0f9eb, #e8f5e9);
+}
+
+.message-completed {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-left: 5px;
+  color: #67c23a;
+  font-size: 14px;
+  opacity: 0;
+  animation: fadeIn 0.5s ease forwards 0.5s;
+}
+
+.message-completed i {
+  background-color: #f0f9eb;
+  border-radius: 50%;
+  padding: 3px;
+  border: 1px solid #e1f3d8;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+}
+
+.system .message-content {
+  justify-content: center;
+}
+
+.system .message-text {
+  background-color: #fef0f0;
+  border: 1px solid #fde2e2;
+  text-align: center;
+  max-width: 80%;
+}
+
+.thought {
+  font-style: italic;
+  color: #606266;
+  background-color: #f4f4f5;
+  border: 1px solid #e9e9eb;
+  border-radius: 4px;
+  padding: 5px 10px;
+  display: block;
+  margin: 5px 0;
+}
+
+@keyframes blink {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0; }
+}
+
+.chat-input {
+  margin-top: 10px;
+  background-color: #fff;
+  padding: 10px;
+  border-radius: 4px;
+  box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.05);
+}
+
+.button-container {
+  margin-top: 10px;
+  display: flex;
+  gap: 10px;
+  justify-content: flex-end;
+}
+
+.loading-indicator {
+  text-align: center;
+  padding: 10px;
+  color: #909399;
+  margin: 10px 0;
+}
+
+.loading-content {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  background-color: #f4f4f5;
+  padding: 8px 16px;
+  border-radius: 16px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
+}
+
+.loading-content i {
+  color: #409eff;
+  animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+
+/* 思考中提示样式 */
+.thinking-message {
+  margin-top: 10px;
+  margin-bottom: 10px;
+  width: 100%;
+}
+
+.thinking-message .message-content {
+  justify-content: center;
+}
+
+.thinking-indicator {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  color: #67c23a;
+  background-color: #f0f9eb;
+  border: 1px solid #e1f3d8;
+  padding: 8px 15px;
+  border-radius: 16px;
+  font-size: 14px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+  animation: pulseThinking 2s infinite;
+  max-width: 80%;
+}
+
+@keyframes pulseThinking {
+  0% { box-shadow: 0 0 0 0 rgba(103, 194, 58, 0.4); }
+  70% { box-shadow: 0 0 0 10px rgba(103, 194, 58, 0); }
+  100% { box-shadow: 0 0 0 0 rgba(103, 194, 58, 0); }
+}
+
+.thinking-indicator i {
+  color: #67c23a;
+  font-size: 16px;
+  animation: spin 1s linear infinite;
+}
+
+/* 添加对思考内容的样式增强 */
+.thinking-indicator span {
+  flex: 1;
+  word-break: break-word;
+  white-space: pre-wrap;
+  line-height: 1.4;
+  max-height: 300px;
+  overflow-y: auto;
+  text-align: left;
+  padding: 5px;
+}
+
+/* 悬浮窗样式 */
+.floating-container {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.floating-chat-container {
+  height: 100%;
+  padding: 0;
+  display: flex;
+  flex-direction: column;
+  background-color: transparent;
+  box-shadow: none;
+}
+
+.floating-chat-messages {
+  flex: 1;
+  overflow-y: auto;
+  padding: 10px;
+  background-color: #f9f9f9;
+  background-image: url('~@/assets/images/chat-bg-pattern.png');
+  background-repeat: repeat;
+  border-radius: 0;
+  margin-bottom: 0;
+  box-shadow: none;
+}
+
+.floating-chat-input {
+  padding: 8px;
+  background-color: #fff;
+  border-top: 1px solid #e6e6e6;
+  margin-top: 0;
+  box-shadow: none;
+  border-radius: 0;
+}
+
+.floating-chat-input .el-textarea__inner {
+  min-height: 60px !important;
+  max-height: 80px !important;
+}
+
+/* 悬浮模式下建议问题样式调整 */
+.floating-chat-messages .suggested-questions {
+  margin-top: 8px;
+  padding: 8px;
+}
+
+.floating-chat-messages .suggestion-list {
+  gap: 6px;
+  padding-left: 5px;
+}
+
+.floating-chat-messages .suggestion-item {
+  padding: 4px 8px;
+  font-size: 12px;
+}
+
+@media (max-width: 768px) {
+  .floating-chat-container {
+    height: calc(100% - 10px);
+  }
+  
+  .floating-chat-messages {
+    max-height: calc(100vh - 170px);
+  }
+  
+  .floating-chat-input {
+    padding: 5px;
+  }
+  
+  .floating-chat-input .button-container {
+    padding: 5px 0;
+  }
+  
+  .floating-chat-input .el-button {
+    padding: 7px 12px;
+    font-size: 12px;
+  }
+  
+  /* 移动端下的建议问题样式 */
+  .suggested-questions {
+    padding: 5px;
+  }
+  
+  .suggestion-title {
+    margin-bottom: 5px;
+    font-size: 13px;
+  }
+  
+  .suggestion-list {
+    gap: 5px;
+  }
+  
+  .suggestion-item {
+    padding: 4px 8px;
+    font-size: 12px;
+  }
+}
+
+/* 建议问题样式 */
+.suggested-questions {
+  margin-top: 10px;
+  padding: 10px;
+  background-color: #e6f7ff;
+  border: 1px solid #91d5ff;
+  border-radius: 4px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+}
+
+.suggestion-title {
+  font-size: 14px;
+  font-weight: bold;
+  color: #1890ff;
+  margin-bottom: 8px;
+  padding-left: 5px;
+}
+
+.suggestion-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  padding-left: 10px;
+}
+
+.suggestion-item {
+  background-color: #fff;
+  border: 1px solid #d9d9d9;
+  border-radius: 4px;
+  padding: 5px 10px;
+  font-size: 13px;
+  color: #333;
+  cursor: pointer;
+  transition: background-color 0.2s ease;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.suggestion-item:hover {
+  background-color: #e6f7ff;
+  border-color: #91d5ff;
+}
+
+.suggestion-item:active {
+  background-color: #bae7ff;
+}
+
+.custom-icon {
+  width: 34px; /* 设置图标宽度 */
+  height: 34px; /* 设置图标高度 */
+  vertical-align: middle; /* 垂直对齐 */
+}
+</style> 

+ 1 - 1
src/views/login.vue

@@ -74,7 +74,7 @@ export default {
       codeUrl: "",
       loginForm: {
         username: "admin",
-        password: "admin123",
+        password: "gbd2025",
         rememberMe: false,
         code: "",
         uuid: ""