5 changed files with 604 additions and 43 deletions
-
20package-lock.json
-
1package.json
-
22src/store/userPermissionCode.js
-
473src/views/aaa.vue
-
129src/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