JinShan_uniapp/pages/chat/chatPage.vue
2025-08-07 12:55:39 +08:00

480 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="chat-container">
<!-- 聊天消息区域 -->
<scroll-view scroll-y :scroll-top="scrollTop" class="chat-messages" :scroll-with-animation="true"
@scroll="onScroll">
<view v-for="(msg, index) in messages" :key="index"
:class="['message', msg.sender === 'me' ? 'sent' : 'received']">
<image v-if="msg.sender !== 'me'" :src="botAvatar" class="avatar"></image>
<view :class="['message-bubble', msg.sender === 'me' ? 'me' : '', msg.type]">
<!-- <text class="message-type-tag" v-if="msg.type !== 'normal'">
{{ msg.type === 'thinking' ? '思考中' : msg.type === 'answer' ? '回答' : '' }}
</text> -->
<text class="message-text">{{ msg.content }}</text>
<view class="typing-indicator" v-if="msg.isTyping">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
<text class="typing-text">对方正在输入...</text>
</view>
</view>
<image v-if="msg.sender === 'me'" :src="userAvatar" class="avatar"></image>
</view>
</scroll-view>
<!-- 输入区域 -->
<view class="chat-input">
<input class="input-field" placeholder="输入消息..." v-model="inputMsg" @confirm="sendMessage"
confirm-type="send" />
<button class="send-btn" :disabled="!inputMsg.trim()" @click="sendMessage">
发送
</button>
</view>
</view>
</template>
<script>
import {
BASE_URL
} from '@/utils/config';
export default {
data() {
return {
inputMsg: '',
scrollTop: 0,
userAvatar: require('../../static/imgs/index/nav.png'),
botAvatar: require('../../static/imgs/ai/chuandaguwen.png'),
messages: [],
socketTask: null, // 微信小程序的 WebSocket 任务对象
isConnected: false,
reconnectAttempts: 0,
maxReconnectAttempts: 5,
reconnectDelay: 3000,
userId: null,
userName: '默认用户',
apiKey: '',
serviceUrl: ''
}
},
onLoad(options) {
this.serviceUrl = options.serviceUrl || '';
this.apiKey = options.apiKey || '';
this.botAvatar = options.icon || this.botAvatar;
const userInfo = wx.getStorageSync('userInfo') || {};
this.userId = userInfo.id || Date.now().toString();
this.userName = userInfo.name || '默认用户';
this.userAvatar = userInfo.avatar || this.userAvatar;
uni.setNavigationBarTitle({
title: options.name || 'AI助手'
});
this.initWebSocket();
},
onUnload() {
this.closeWebSocket();
},
methods: {
// 初始化WebSocket连接
initWebSocket() {
if (this.isConnected) return;
// 微信小程序中使用 wx.connectSocket
this.socketTask = wx.connectSocket({
url: `ws://10.10.1.6:8071/api/v1/ws/ai?apiUrl=${encodeURIComponent(this.serviceUrl)}&apiToken=${this.apiKey}&userName=${this.userName}`,
success: () => {
console.log('WebSocket连接创建成功');
},
fail: (err) => {
console.error('WebSocket连接创建失败', err);
this.handleReconnect();
}
});
// 监听 WebSocket 事件
this.socketTask.onOpen(() => {
console.log('WebSocket连接已打开');
this.isConnected = true;
this.reconnectAttempts = 0;
this.addMessage('bot', '连接已建立,请问有什么可以帮您?');
});
this.socketTask.onMessage((res) => {
// console.log('收到WebSocket消息:', res.data);
this.processBotMessage(res.data);
});
this.socketTask.onError((err) => {
console.error('WebSocket发生错误:', err);
this.isConnected = false;
this.addMessage('bot', '连接出错,请稍后再试');
this.handleReconnect();
});
this.socketTask.onClose(() => {
console.log('WebSocket连接已关闭');
this.isConnected = false;
this.handleReconnect();
});
},
processBotMessage(data) {
try {
// 解析数据
const messageData = typeof data === 'string' ? JSON.parse(data) : data;
if (messageData.event === 'text_chunk') {
// 处理分块文本
this.handleTextChunk(messageData);
} else if (messageData.event === 'workflow_finished') {
// 处理完整节点完成
this.handleNodeFinished(messageData);
}
} catch (error) {
console.error('处理机器人消息出错:', error);
this.addMessage('bot', '处理消息时出错: ' + error.message, 'error');
}
},
// 处理文本分块
handleTextChunk(chunkData) {
const text = chunkData.data.text;
if (!text || text.trim() === '') return;
// 判断是否是思考内容
const isThinking = text.includes('<think>') ||
(this.messages[this.messages.length - 1]?.type === 'thinking');
if (isThinking) {
this.handleThinkingContent(text);
} else {
this.handleAnswerContent(text);
}
this.scrollToBottom();
},
// 处理思考内容
handleThinkingContent(text) {
// 提取思考内容,去除标签
const thinkContent = text.replace(/<\/?think>/g, '').trim();
if (!thinkContent) return;
// 查找最后一条思考消息
const lastThinkMsgIndex = this.messages.findLastIndex(
msg => msg.type === 'thinking'
);
if (lastThinkMsgIndex >= 0) {
// 追加到现有思考消息
this.messages[lastThinkMsgIndex].content += thinkContent;
this.$forceUpdate();
} else {
// 创建新的思考消息
this.addMessage('bot', thinkContent, 'thinking');
}
},
// 处理回答内容
handleAnswerContent(text) {
// 提取回答内容,去除标签
const answerContent = text.replace(/<\/?answer>/g, '').trim();
if (!answerContent) return;
// 查找最后一条回答消息
const lastAnswerMsgIndex = this.messages.findLastIndex(
msg => msg.type === 'answer' || msg.type === 'normal'
);
if (lastAnswerMsgIndex >= 0 && this.messages[lastAnswerMsgIndex].type === 'answer') {
// 追加到现有回答消息
this.messages[lastAnswerMsgIndex].content += answerContent;
this.$forceUpdate();
} else {
// 创建新的回答消息
this.addMessage('bot', answerContent, 'answer');
}
},
// 处理节点完成
handleNodeFinished(nodeData) {
// 标记最后一条消息为完成
if (this.messages.length > 0) {
const lastMsg = this.messages[this.messages.length - 1];
lastMsg.isComplete = true;
// 如果最后一条是思考消息,我们不需要额外处理
if (lastMsg.type === 'thinking') {
return;
}
}
// 只有当没有找到回答内容时才添加完整输出
if (nodeData.data.outputs?.text) {
const fullText = nodeData.data.outputs.text;
const answerContent = fullText.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
// 检查是否已经有相同内容的消息
const hasSameContent = this.messages.some(
msg => msg.content.includes(answerContent)
);
if (answerContent && !hasSameContent) {
this.addMessage('bot', answerContent, 'answer');
}
}
this.scrollToBottom();
},
// 修改后的 addMessage 方法
addMessage(sender, content, type = 'normal', isTyping = false) {
const message = {
sender: sender === 'bot' ? 'other' : 'me',
content,
type, // 'thinking', 'answer', 或 'normal'
isTyping,
isComplete: true, // 默认完整流式消息会设为false
timestamp: Date.now()
};
// 如果是流式消息,标记为未完成
if (type === 'thinking' || type === 'answer') {
message.isComplete = false;
}
this.messages.push(message);
this.scrollToBottom();
},
// 发送消息
sendMessage() {
const content = this.inputMsg.trim();
if (!content || !this.isConnected) return;
this.addMessage('me', content);
// 微信小程序中使用 socketTask.send
this.socketTask.send({
data: content,
success: () => {
console.log('消息发送成功');
// 添加机器人正在输入的提示
this.addMessage('bot', '思考中...', true);
},
fail: (err) => {
console.error('消息发送失败', err);
this.addMessage('bot', '消息发送失败,请重试');
}
});
this.inputMsg = '';
},
// 处理重连逻辑
handleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.log('已达到最大重连次数');
this.addMessage('bot', '连接已断开,请刷新页面重试');
return;
}
this.reconnectAttempts++;
console.log(`尝试重新连接,第${this.reconnectAttempts}`);
setTimeout(() => {
this.initWebSocket();
}, this.reconnectDelay);
},
// 关闭WebSocket连接
closeWebSocket() {
if (this.socketTask) {
this.socketTask.close({
success: () => {
console.log('WebSocket已主动关闭');
},
fail: (err) => {
console.error('关闭WebSocket失败', err);
}
});
this.socketTask = null;
}
this.isConnected = false;
},
// 滚动到底部
scrollToBottom() {
this.$nextTick(() => {
this.scrollTop = 99999; // 足够大的值确保滚动到底部
});
},
onScroll(e) {
// 可以在这里实现加载历史消息
}
}
}
</script>
<style>
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f0f0f0;
}
.chat-messages {
flex: 1;
/* padding: 15px; */
background-color: #e5ddd5;
overflow-y: auto;
}
.message {
display: flex;
margin: 10px;
}
.sent {
justify-content: flex-end;
}
.received {
justify-content: flex-start;
}
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
margin: 0 8px;
}
.message-bubble {
max-width: 70%;
padding: 10px 15px;
border-radius: 18px;
background-color: white;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.me {
background-color: #dcf8c6;
}
.message-text {
font-size: 14px;
line-height: 1.4;
}
.chat-input {
display: flex;
padding: 10px;
background-color: white;
border-top: 1px solid #ddd;
}
.input-field {
flex: 1;
padding: 8px 15px;
border-radius: 20px;
background-color: #f0f0f0;
margin-right: 10px;
}
.send-btn {
padding: 0 20px;
border-radius: 20px;
background-color: #07C160;
color: white;
}
.send-btn:disabled {
opacity: 0.5;
}
/* 对方正在输入提示 */
.typing-indicator {
display: flex;
align-items: center;
margin-top: 8px;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #7f8c8d;
margin: 0 3px;
animation: typing 1.4s infinite ease-in-out;
}
.dot:nth-child(1) {
animation-delay: 0s;
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.4s;
}
.typing-text {
font-size: 12px;
margin-left: 8px;
color: #555;
}
@keyframes typing {
0%,
60%,
100% {
transform: translateY(0);
}
30% {
transform: translateY(-5px);
}
}
/* 思考消息样式 */
.message-bubble.thinking {
background-color: #f0f0f0;
border-left: 4px solid #999;
font-style: italic;
color: #666;
}
/* 回答消息样式 */
.message-bubble.answer {
background-color: #f8f8f8;
border-left: 4px solid #07C160;
}
/* 错误消息样式 */
.message-bubble.error {
background-color: #ffeeee;
border-left: 4px solid #ff4d4f;
color: #ff4d4f;
}
/* 消息类型标签 */
.message-type-tag {
font-size: 12px;
color: #999;
margin-bottom: 4px;
font-weight: bold;
}
</style>