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