5 changed files with 604 additions and 43 deletions
			
			
		- 
					20package-lock.json
- 
					1package.json
- 
					22src/store/userPermissionCode.js
- 
					473src/views/aaa.vue
- 
					131src/views/chat.vue
| @ -0,0 +1,473 @@ | |||
| <script setup> | |||
| // 移除未使用的导入 | |||
| import { ref, nextTick, watch, onMounted } from 'vue' | |||
| import { useUserInfo } from '../store/userPermissionCode' | |||
| const { getQueryVariable } = useUserInfo() | |||
| 
 | |||
| const isTokenValid = ref(false) | |||
| const fnGetToken = () => { | |||
|     localStorage.setItem('localToken', decodeURIComponent(String(getQueryVariable('token')))) | |||
|     console.log(localStorage.getItem('localToken')); | |||
| } | |||
| setTimeout(() => { | |||
|     fnGetToken() | |||
| }, 800) | |||
| 
 | |||
| // 验证 token | |||
| const validateToken = async () => { | |||
|     const token = localStorage.getItem('localToken') | |||
|     if (!token) { | |||
|         console.error('未找到 token,请重新登录') | |||
|         return false | |||
|     } | |||
|     return true | |||
| } | |||
| 
 | |||
| // 页面加载时验证 token | |||
| validateToken().then((isValid) => { | |||
|     isTokenValid.value = isValid | |||
|     if (!isValid) { | |||
|         // 可以在这里添加跳转到登录页等逻辑 | |||
|         console.error('Token 验证失败,请重新登录') | |||
|     } | |||
| }) | |||
| 
 | |||
| // 定义Props(可配置参数) | |||
| const props = defineProps({ | |||
|     apiUrl: { | |||
|         type: String, | |||
|         default: 'http://localhost:5000/ask' | |||
|     }, | |||
|     initialGreeting: { | |||
|         type: String, | |||
|         default: '您好!请问有什么可以帮助您?' | |||
|     } | |||
| }) | |||
| // 响应式数据 | |||
| const messages = ref([ | |||
|     { | |||
|         content: props.initialGreeting, | |||
|         sender: 'bot', | |||
|         timestamp: new Date() | |||
|     } | |||
| ]) | |||
| const inputMessage = ref('') | |||
| const messageContainer = ref(null) | |||
| const isLoading = ref(false) // 新增:加载状态 | |||
| 
 | |||
| // 自动滚动到底部 | |||
| const scrollToBottom = () => { | |||
|     nextTick(() => { | |||
|         if (messageContainer.value) { | |||
|             messageContainer.value.scrollTop = messageContainer.value.scrollHeight | |||
|         } | |||
|     }) | |||
| } | |||
| 
 | |||
| // 处理流式数据 | |||
| const handleStreamResponse = async (response, onChunk) => { | |||
|     const reader = response.body.getReader(); | |||
|     const decoder = new TextDecoder(); | |||
|     let done = false; | |||
| 
 | |||
|     while (!done) { | |||
|         const { value, done: readerDone } = await reader.read(); | |||
|         done = readerDone; | |||
|         if (value) { | |||
|             const chunk = decoder.decode(value, { stream: true }); | |||
|             const lines = chunk.split('\n'); | |||
|             for (const line of lines) { | |||
|                 if (line.startsWith('data:')) { | |||
|                     const data = line.slice(5).trim(); | |||
|                     if (data && data!== '[DONE]') { | |||
|                         try { | |||
|                             const jsonData = JSON.parse(data); | |||
|                             const answerChunk = jsonData.data.answer.replace(/<think>|<\/think>/g, ''); | |||
|                             onChunk(answerChunk); | |||
|                         } catch (error) { | |||
|                             console.error('解析 JSON 数据时出错:', error); | |||
|                         } | |||
|                     } | |||
|                 } | |||
|             } | |||
|         } | |||
|     } | |||
| }; | |||
| 
 | |||
| // 发送消息 | |||
| const sendMessage = async () => { | |||
| 
 | |||
|     if (!isTokenValid.value) { | |||
|         console.error('Token 验证失败,无法发送消息') | |||
|         return | |||
|     } | |||
| 
 | |||
|     if (isLoading.value) return; | |||
|     const content = inputMessage.value.trim() | |||
|     if (!content) return | |||
|     // 添加用户消息 | |||
|     messages.value.push({ | |||
|         content, | |||
|         sender: 'user', | |||
|         timestamp: new Date() | |||
|     }) | |||
|     // 清空输入框 | |||
|     inputMessage.value = '' | |||
|     scrollToBottom() | |||
| 
 | |||
|     // 显示加载动画 | |||
|     isLoading.value = true | |||
|     const loadingMessage = { | |||
|         content: '我正在思考...', | |||
|         sender: 'bot', | |||
|         timestamp: new Date(), | |||
|         isLoading: true | |||
|     }; | |||
|     messages.value.push(loadingMessage); | |||
|     scrollToBottom() | |||
| 
 | |||
|     try { | |||
|         // 调用API获取流式回复 | |||
|         const response = await fetch("http://192.168.1.140/api/v1/chats/89fcb72aeffc11ef98100242ac120006/completions", { | |||
|             method: 'POST', | |||
|             headers: { 'Content-Type': 'application/json', Authorization: localStorage.getItem('localToken') }, | |||
|             body: JSON.stringify({ question: content, stream: true, session_id: "750a8af4f00611ef9e130242ac120006" }) | |||
|         }); | |||
| 
 | |||
|         if (!response.ok) { | |||
|             throw new Error(`HTTP error! status: ${response.status}`); | |||
|         } | |||
| 
 | |||
|         // 移除加载消息 | |||
|         messages.value = messages.value.filter(msg => !msg.isLoading); | |||
|         const botMessage = { | |||
|             content: '', | |||
|             sender: 'bot', | |||
|             timestamp: new Date() | |||
|         }; | |||
|         messages.value.push(botMessage); | |||
| 
 | |||
|         await handleStreamResponse(response, (chunk) => { | |||
|             botMessage.content += chunk; | |||
|             const grayText = `<span style="color: gray;">${botMessage.content}</span>`; | |||
|             botMessage.content = grayText; | |||
|             scrollToBottom(); | |||
|         }); | |||
| 
 | |||
|     } catch (error) { | |||
|         console.error('API请求失败:', error) | |||
|         // 移除加载消息 | |||
|         messages.value = messages.value.filter(msg => !msg.isLoading); | |||
| 
 | |||
|         messages.value.push({ | |||
|             content: '服务暂时不可用,请稍后再试', | |||
|             sender: 'bot', | |||
|             timestamp: new Date() | |||
|         }) | |||
|         scrollToBottom() | |||
|     } finally { | |||
|         // 隐藏加载动画 | |||
|         isLoading.value = false | |||
|     } | |||
| } | |||
| // 格式化时间显示 | |||
| const formatTime = (date) => { | |||
|     return new Date(date).toLocaleTimeString([], { | |||
|         hour: '2-digit', | |||
|         minute: '2-digit' | |||
|     }) | |||
| } | |||
| // 自适应输入框高度 | |||
| const adjustInputHeight = () => { | |||
|     const textarea = document.querySelector('.message-input') | |||
|     textarea.style.height = 'auto' | |||
|     textarea.style.height = `${textarea.scrollHeight}px` | |||
| } | |||
| // 监听输入内容变化 | |||
| watch(inputMessage, adjustInputHeight) | |||
| 
 | |||
| // 在组件挂载后按顺序执行操作 | |||
| onMounted(async () => { | |||
|     // 先获取 token | |||
|     fnGetToken() | |||
|     // 再验证 token | |||
|     const isValid = await validateToken() | |||
|     isTokenValid.value = isValid | |||
|     if (!isValid) { | |||
|         console.error('Token 验证失败,请重新登录') | |||
|     } | |||
| }) | |||
| 
 | |||
| </script> | |||
| <template> | |||
|     <!-- 聊天容器 --> | |||
|     <div class="chat-container"> | |||
|         <!-- 聊天框头部 --> | |||
|         <div class="chat-header">夺宝奇兵智能客服</div> | |||
|         <!-- 消息展示区域 --> | |||
|         <div class="message-list" ref="messageContainer"> | |||
|             <div v-for="(message, index) in messages" :key="index" class="message-item" :class="[message.sender]"> | |||
|                 <!-- 机器人头像 --> | |||
|                 <div v-if="message.sender === 'bot'" class="bot-avatar"> | |||
|                     <img src="/src/assets/img/avatar/超级云脑按钮.png" alt="Bot Avatar"> | |||
|                 </div> | |||
|                 <div class="message-bubble"> | |||
|                     <div class="message-content"> | |||
|                         <!-- 显示加载动画 --> | |||
|                         <span v-if="message.isLoading"> | |||
|                             {{ message.content }} | |||
|                             <el-icon class="is-loading"> | |||
|                                 <Loading /> | |||
|                             </el-icon> | |||
|                         </span> | |||
|                         <span v-else v-html="message.content"></span> | |||
|                     </div> | |||
|                     <div class="message-time">{{ formatTime(message.timestamp) }}</div> | |||
|                 </div> | |||
|                 <!-- 用户头像 --> | |||
|                 <div v-if="message.sender === 'user'" class="user-avatar"> | |||
|                     <img src="/src/assets/img/avatar/小柒.png" alt="User Avatar"> | |||
|                 </div> | |||
|             </div> | |||
|         </div> | |||
|         <!-- 输入区域 --> | |||
|         <div class="input-area"> | |||
|             <textarea v-model="inputMessage" @keydown.enter.exact.prevent="isLoading ? null : sendMessage()" | |||
|                 placeholder="输入您的问题..." rows="1" class="message-input"></textarea> | |||
|             <el-tooltip content="机器人正在思考" :disabled="!isLoading"> | |||
|                 <template #content> | |||
|                     机器人正在思考 | |||
|                 </template> | |||
|                 <button @click="sendMessage" :disabled="!isTokenValid || isLoading" class="send-button"> | |||
|                     <!-- 使用ElementPlus的发送图标 --> | |||
|                     <span v-if="isLoading"> | |||
|                         <el-icon class="is-loading"> | |||
|                             <Loading /> | |||
|                         </el-icon> | |||
|                     </span> | |||
|                     <span v-else class="send-button-content"> | |||
|                         <el-icon> | |||
|                             <Position /> | |||
|                         </el-icon> | |||
|                         <span> 发送</span> | |||
|                     </span> | |||
|                 </button> | |||
|             </el-tooltip> | |||
|         </div> | |||
|         <!-- 未登录覆盖层 --> | |||
|         <div v-if="!isTokenValid" class="overlay"> | |||
|             <div class="overlay-content">用户未登录</div> | |||
|         </div> | |||
|     </div> | |||
| </template> | |||
| 
 | |||
| <style scoped> | |||
| .chat-container { | |||
|     display: flex; | |||
|     flex-direction: column; | |||
|     height: 90vh; | |||
|     width: 90vw; | |||
|     max-width: 800px; | |||
|     margin: 0; | |||
|     border: 1px solid #e0e0e0; | |||
|     border-radius: 12px; | |||
|     background: #f8f9fa; | |||
|     overflow: hidden; | |||
|     /* 新增样式,实现水平和垂直居中 */ | |||
|     position: absolute; | |||
|     top: 50%; | |||
|     left: 50%; | |||
|     transform: translate(-50%, -50%); | |||
| } | |||
| 
 | |||
| /* 聊天框头部样式 */ | |||
| .chat-header { | |||
|     background-color: #007bff; | |||
|     color: white; | |||
|     padding: 16px; | |||
|     font-size: 1.2rem; | |||
|     text-align: center; | |||
| } | |||
| 
 | |||
| .message-list { | |||
|     flex: 1; | |||
|     padding: 20px; | |||
|     overflow-y: auto; | |||
|     background: white; | |||
| } | |||
| 
 | |||
| .message-item { | |||
|     display: flex; | |||
|     margin-bottom: 16px; | |||
| } | |||
| 
 | |||
| .message-item.user { | |||
|     justify-content: flex-end; | |||
| } | |||
| 
 | |||
| .bot-avatar { | |||
|     margin-right: 10px; | |||
| } | |||
| 
 | |||
| .bot-avatar img { | |||
|     width: 40px; | |||
|     height: 40px; | |||
|     border-radius: 50%; | |||
|     object-fit: cover; | |||
| } | |||
| 
 | |||
| .user-avatar { | |||
|     margin-left: 10px; | |||
| } | |||
| 
 | |||
| .user-avatar img { | |||
|     width: 40px; | |||
|     height: 40px; | |||
|     border-radius: 50%; | |||
|     object-fit: cover; | |||
| } | |||
| 
 | |||
| .message-bubble { | |||
|     max-width: 80%; | |||
|     padding: 12px 16px; | |||
|     border-radius: 15px; | |||
|     position: relative; | |||
| } | |||
| 
 | |||
| .message-content span { | |||
|     display: block; | |||
|     /* 确保元素显示 */ | |||
| } | |||
| 
 | |||
| .message-item.user .message-bubble { | |||
|     background: #007bff; | |||
|     color: white; | |||
|     border-bottom-right-radius: 4px; | |||
| } | |||
| 
 | |||
| .message-item.bot .message-bubble { | |||
|     background: #f1f3f5; | |||
|     color: #212529; | |||
|     border-bottom-left-radius: 4px; | |||
| } | |||
| 
 | |||
| .message-time { | |||
|     font-size: 0.75rem; | |||
|     color: rgba(255, 255, 255, 0.8); | |||
|     margin-top: 4px; | |||
|     text-align: right; | |||
| } | |||
| 
 | |||
| .message-item.bot .message-time { | |||
|     color: rgba(0, 0, 0, 0.6); | |||
| } | |||
| 
 | |||
| .input-area { | |||
|     display: flex; | |||
|     align-items: center; | |||
|     gap: 12px; | |||
|     padding: 16px; | |||
|     border-top: 1px solid #e0e0e0; | |||
|     background: white; | |||
| } | |||
| 
 | |||
| .message-input { | |||
|     flex: 1; | |||
|     padding: 10px 16px; | |||
|     border: 1px solid #e0e0e0; | |||
|     border-radius: 20px; | |||
|     resize: none; | |||
|     max-height: 120px; | |||
|     font-family: inherit; | |||
| } | |||
| 
 | |||
| .send-button { | |||
|     display: flex; | |||
|     align-items: center; | |||
|     justify-content: center; | |||
|     width: 100px; | |||
|     /* 调整宽度以适应文字 */ | |||
|     height: 40px; | |||
|     border: none; | |||
|     border-radius: 20px; | |||
|     /* 调整圆角 */ | |||
|     background: #007bff; | |||
|     color: white; | |||
|     cursor: pointer; | |||
|     transition: all 0.3s ease; | |||
|     /* 添加过渡效果 */ | |||
|     box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); | |||
|     /* 添加阴影 */ | |||
|     font-size: 16px; | |||
|     /* 调整字体大小 */ | |||
|     font-weight: 600; | |||
|     /* 调整字体粗细 */ | |||
| 
 | |||
| } | |||
| 
 | |||
| .send-button:hover { | |||
|     background: #0056b3; | |||
|     transform: translateY(-2px); | |||
|     /* 悬停时向上移动 */ | |||
|     box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); | |||
|     /* 悬停时增加阴影 */ | |||
| } | |||
| 
 | |||
| /* 新增加载状态样式 */ | |||
| .loading-state { | |||
|     background: #ccc; | |||
|     cursor: not-allowed; | |||
| } | |||
| 
 | |||
| .send-button-content { | |||
|     display: flex; | |||
|     align-items: center; | |||
|     justify-content: center; | |||
|     gap: 8px; | |||
|     /* 调整文字和图标间距 */ | |||
| } | |||
| 
 | |||
| /* .send-button { | |||
|     display: flex; | |||
|     align-items: center; | |||
|     justify-content: center; | |||
|     width: 40px; | |||
|     height: 40px; | |||
|     border: none; | |||
|     border-radius: 50%; | |||
|     background: #007bff; | |||
|     color: white; | |||
|     cursor: pointer; | |||
|     transition: background 0.2s; | |||
| } | |||
| 
 | |||
| .send-button:hover { | |||
|     background: #0056b3; | |||
| } */ | |||
| 
 | |||
| .send-button svg { | |||
|     width: 20px; | |||
|     height: 20px; | |||
| } | |||
| 
 | |||
| .overlay { | |||
|     position: absolute; | |||
|     top: 0; | |||
|     left: 0; | |||
|     width: 100%; | |||
|     height: 100%; | |||
|     background-color: rgba(255, 255, 255, 0.8); | |||
|     /* 透明度 50% 的白色背景 */ | |||
|     display: flex; | |||
|     justify-content: center; | |||
|     align-items: center; | |||
|     z-index: 1; | |||
|     /* 确保覆盖层在聊天框上方 */ | |||
| } | |||
| 
 | |||
| .overlay-content { | |||
|     font-size: 36px; | |||
|     font-weight: bold; | |||
|     color: #f60707; | |||
| } | |||
| </style> | |||
						Write
						Preview
					
					
					Loading…
					
					Cancel
						Save
					
		Reference in new issue