ws聊天记录

This commit is contained in:
Leo_Ding 2025-08-07 12:55:39 +08:00
parent a411e94890
commit 859d6f2e79
6 changed files with 738 additions and 421 deletions

View File

@ -0,0 +1,353 @@
<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' : '']">
<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) {
//
this.messages = this.messages.filter(msg => !msg.isTyping);
//
if (data.startsWith('<think>')) {
//
this.addMessage('bot', '', true);
} else if (data.startsWith('<answer>')) {
//
const content = data.replace(/<answer>/g, '').replace(/<\/answer>/g, '');
this.addMessage('bot', content);
} else if (data.includes('</think>')) {
//
} else {
//
this.addMessage('bot', data);
}
},
//
addMessage(sender, content, isTyping = false) {
const message = {
sender: sender === 'bot' ? 'other' : 'me',
content,
isTyping,
timestamp: Date.now()
};
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-bottom: 15px;
}
.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: 16px;
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);
}
}
</style>

View File

@ -1,370 +1,330 @@
<template> <template>
<view class="chat-container"> <view class="chat-container">
<!-- 聊天消息区域 --> <!-- 聊天消息区域 -->
<scroll-view scroll-y :scroll-top="scrollTop" class="chat-messages" :scroll-with-animation="true" <scroll-view scroll-y :scroll-top="scrollTop" class="chat-messages" :scroll-with-animation="true"
@scroll="onScroll"> @scroll="onScroll">
<view class="message-time" v-if="showDate">今天 {{ getTime() }}</view>
<!-- 消息列表 -->
<view v-for="(msg, index) in messages" :key="index" <view v-for="(msg, index) in messages" :key="index"
:class="['message', msg.sender === 'me' ? 'sent' : 'received']"> :class="['message', msg.sender === 'me' ? 'sent' : 'received']">
<image v-if="msg.sender !== 'me'" :src="currentChat.avatar" class="avatar"></image> <image v-if="msg.sender !== 'me'" :src="botAvatar" class="avatar"></image>
<view :class="['message-bubble', msg.sender === 'me' ? 'me' : '']"> <view :class="['message-bubble', msg.sender === 'me' ? 'me' : '', msg.type]">
<text class="message-text" v-if="msg.content">{{ msg.content }}</text> <!-- <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="typing-indicator" v-if="msg.isTyping">
<view class="dot"></view> <view class="dot"></view>
<view class="dot"></view> <view class="dot"></view>
<view class="dot"></view> <view class="dot"></view>
<text class="typing-text">对方正在输入...</text> <text class="typing-text">对方正在输入...</text>
</view> </view>
<!-- <text class="message-status">{{ msg.time }}</text> -->
</view> </view>
<image v-if="msg.sender === 'me'" :src="myAvatar" class="avatar"></image>
<image v-if="msg.sender === 'me'" :src="userAvatar" class="avatar"></image>
</view> </view>
</scroll-view> </scroll-view>
<!-- 输入区域 --> <!-- 输入区域 -->
<view class="chat-input"> <view class="chat-input">
<view class="input-container"> <input class="input-field" placeholder="输入消息..." v-model="inputMsg" @confirm="sendMessage"
<!-- <uni-icons type="plus" size="28" color="#666" class="action-icon"></uni-icons> --> confirm-type="send" />
<input class="input-field" placeholder="输入消息..." v-model="inputMsg" @confirm="sendMessage" <button class="send-btn" :disabled="!inputMsg.trim()" @click="sendMessage">
@input="onInput" :focus="isFocus" /> 发送
<!-- <uni-icons type="mic" size="28" color="#666" class="action-icon"></uni-icons>
<uni-icons type="image" size="28" color="#666" class="action-icon"></uni-icons> -->
</view>
<button class="send-btn" :class="{active: inputMsg.trim() !== ''}" @click="sendMessage">
<uni-icons type="paperplane" size="24" color="#fff"></uni-icons>
</button> </button>
</view> </view>
</view> </view>
</template> </template>
<script> <script>
import uniIcons from '@dcloudio/uni-ui/lib/uni-icons/uni-icons.vue' import {
import {IMAGE_BASE_URL,BASE_URL} from '@/utils/config'; BASE_URL
} from '@/utils/config';
export default { export default {
components: {
uniIcons
},
data() { data() {
return { return {
inputMsg: '', inputMsg: '',
scrollTop: 0, scrollTop: 0,
isTyping: false, userAvatar: require('../../static/imgs/index/nav.png'),
isFocus: false, botAvatar: require('../../static/imgs/ai/chuandaguwen.png'),
showDate: true,
myAvatar: require('../../static/imgs/index/nav.png'),
currentChat: {
name: '张婷婷',
avatar: require('../../static/imgs/ai/chuandaguwen.png'),
status: '在线'
},
messages: [], messages: [],
currentResponse: null, // socketTask: null, // WebSocket
currentResponseId: null, // isConnected: false,
abortController: null, // reconnectAttempts: 0,
isStreaming: false, // maxReconnectAttempts: 5,
reconnectDelay: 3000,
userId: null, userId: null,
serviceUrl:'', userName: '默认用户',
apiKey:'', apiKey: '',
aiId:null, serviceUrl: ''
aiName:''
} }
}, },
onLoad(options) { onLoad(options) {
this.serviceUrl=options.serviceUrl this.serviceUrl = options.serviceUrl || '';
this.apiKey=options.apiKey this.apiKey = options.apiKey || '';
this.aiId=options.id this.botAvatar = options.icon || this.botAvatar;
this.currentChat.avatar=options.icon
uni.setNavigationBarTitle({ const userInfo = wx.getStorageSync('userInfo') || {};
title: options.name 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();
}, },
mounted() {
try { onUnload() {
const userInfo = wx.getStorageSync('userInfo') this.closeWebSocket();
if (userInfo === undefined || userInfo === null||userInfo=='') {
const defaultValue=this.generateMiniProgramUUID()
wx.setStorageSync('userId', defaultValue)
this.userId=defaultValue
// this.myAvatar=require('../../static/imgs/index/nav.png')
}else{
this.userId=userInfo.id
this.myAvatar=userInfo.avatar||require('../../static/imgs/index/nav.png')
}
} catch (e) {
const defaultValue=this.generateMiniProgramUUID()
wx.setStorageSync('userId', defaultValue)
this.userId=defaultValue
}
}, },
methods: { methods: {
generateMiniProgramUUID() { // WebSocket
const buffer = new Uint8Array(16); initWebSocket() {
wx.getRandomValues(buffer); if (this.isConnected) return;
buffer[6] = (buffer[6] & 0x0f) | 0x40; // Version 4
buffer[8] = (buffer[8] & 0x3f) | 0x80; // Variant 10
return Array.from(buffer)
.map(b => b.toString(16).padStart(2, '0'))
.join('')
.replace(/(\w{8})(\w{4})(\w{4})(\w{4})(\w{12})/, '$1-$2-$3-$4-$5');
},
async sendMessage() {
if (!this.inputMsg.trim() || this.isStreaming) return;
// // 使 wx.connectSocket
const userMsg = { this.socketTask = wx.connectSocket({
sender: 'me', url: `ws://10.10.1.6:8071/api/v1/ws/ai?apiUrl=${encodeURIComponent(this.serviceUrl)}&apiToken=${this.apiKey}&userName=${this.userName}`,
content: this.inputMsg, success: () => {
time: this.getTime() console.log('WebSocket连接创建成功');
}; },
this.messages.push(userMsg); fail: (err) => {
const userInput = this.inputMsg; console.error('WebSocket连接创建失败', err);
this.inputMsg = ''; this.handleReconnect();
this.scrollToBottom();
// AI
const aiMsg = {
sender: 'other',
content: '',
time: this.getTime(),
isTyping: true,
id: Date.now() //
};
this.messages.push(aiMsg);
this.isTyping = true;
this.isStreaming = true;
try {
// AbortController便
// this.abortController = new AbortController();
const response = await this.streamResponse(userInput, aiMsg.id);
console.log('Stream completed:', response);
} catch (error) {
console.error('Stream error:', error);
if (error.name !== 'AbortError') {
this.updateAiMessage(aiMsg.id, '\n\n抱歉请求过程中出现错误: ' + error.message);
} }
} finally { });
this.isTyping = false;
this.isStreaming = false;
// this.abortController = null;
}
},
async streamResponse(inputParams, messageId) { // WebSocket
return new Promise((resolve, reject) => { this.socketTask.onOpen(() => {
// 使uni.request console.log('WebSocket连接已打开');
uni.request({ this.isConnected = true;
url: this.serviceUrl, this.reconnectAttempts = 0;
method: 'POST', this.addMessage('bot', '连接已建立,请问有什么可以帮您?');
data: { });
'query':inputParams,
"inputs": { this.socketTask.onMessage((res) => {
"content": inputParams // console.log('WebSocket:', res.data);
}, this.processBotMessage(res.data);
"response_mode": "blocking", //blockingstreaming });
"user":this.userId
}, this.socketTask.onError((err) => {
header: { console.error('WebSocket发生错误:', err);
'Authorization': `Bearer ${this.apiKey}`, this.isConnected = false;
'Content-Type': 'application/json' this.addMessage('bot', '连接出错,请稍后再试');
}, this.handleReconnect();
// signal: this.abortController?.signal, // });
// enableChunked: true, //
success: (res) => { this.socketTask.onClose(() => {
// console.log(res) console.log('WebSocket连接已关闭');
// this.isConnected = false;
if (res.data) { this.handleReconnect();
console.log(res.data)
console.log(res.data.data)
try {
let result=null
if(this.aiId==3){
const result = res.data.data.outputs.text;
// <think>
const content = result.replace(/<think>[\s\S]*?<\/think>\n?/g, '');
this.updateAiMessage(messageId, content);
this.isStreaming = false
this.isTyping = false;
}else{
const result = res.data.answer;
// <think>
const content = result.replace(/<think>[\s\S]*?<\/think>\n?/g, '');
this.updateAiMessage(messageId, content);
this.isStreaming = false
this.isTyping = false;
}
} catch (e) {
console.error('Parse error:', e);
}
}
},
fail: (err) => {
reject(new Error(err.errMsg));
},
complete: () => {
//
}
});
}); });
}, },
// processBotMessage(data) {
async updateAiMessage(id, newContent) { try {
const msg = this.messages.find(m => m.id === id); //
if (msg) { const messageData = typeof data === 'string' ? JSON.parse(data) : data;
// JSON
if (newContent.includes('{') && newContent.includes('}')) {
try {
const jsonObj = JSON.parse(newContent);
msg.content = await this.formatJsonToReadableText(jsonObj);
msg.isTyping = false
} catch (e) { if (messageData.event === 'text_chunk') {
// JSON //
msg.content = newContent; this.handleTextChunk(messageData);
} } else if (messageData.event === 'workflow_finished') {
} else { //
msg.content = newContent; this.handleNodeFinished(messageData);
msg.isTyping = false
} }
} catch (error) {
this.scrollToBottom(); console.error('处理机器人消息出错:', error);
this.addMessage('bot', '处理消息时出错: ' + error.message, 'error');
} }
}, },
// JSON //
formatJsonToReadableText(jsonObj) { handleTextChunk(chunkData) {
let result = ''; const text = chunkData.data.text;
if (!text || text.trim() === '') return;
// 穿 //
if (jsonObj.outfit) { const isThinking = text.includes('<think>') ||
result += '👕 穿搭建议:\n'; (this.messages[this.messages.length - 1]?.type === 'thinking');
result += `上衣: ${jsonObj.outfit.top.style} (${jsonObj.outfit.top.color})\n`;
result += `下装: ${jsonObj.outfit.bottom.style} (${jsonObj.outfit.bottom.color})\n`;
result += `鞋子: ${jsonObj.outfit.shoes.style} (${jsonObj.outfit.shoes.color})\n`;
if (jsonObj.outfit.accessories && jsonObj.outfit.accessories.length > 0) { if (isThinking) {
result += '配饰:\n'; this.handleThinkingContent(text);
jsonObj.outfit.accessories.forEach(acc => { } else {
result += `- ${acc.type}: ${acc.style} (${acc.color})\n`; this.handleAnswerContent(text);
});
}
} }
//
if (jsonObj.makeup_suggestion) {
result += '\n💄 化妆建议:\n';
result += `风格: ${jsonObj.makeup_suggestion.style}\n`;
result += `描述: ${jsonObj.makeup_suggestion.description}\n`;
if (jsonObj.makeup_suggestion.products && jsonObj.makeup_suggestion.products.length > 0) {
result += '推荐产品:\n';
jsonObj.makeup_suggestion.products.forEach(product => {
result += `- ${product.name}: ${product.detail_description}\n`;
});
}
}
//
if (jsonObj.styling_tips) {
result += '\n💡 造型小贴士:\n';
result += `色彩搭配: ${jsonObj.styling_tips.color_coordination}\n`;
if (jsonObj.styling_tips.detail_enhancements && jsonObj.styling_tips.detail_enhancements.length > 0) {
result += '细节提升:\n';
jsonObj.styling_tips.detail_enhancements.forEach(tip => {
result += `- ${tip}\n`;
});
}
if (jsonObj.styling_tips.occasion_adaptation) {
result += `场合适配: ${jsonObj.styling_tips.occasion_adaptation}\n`;
}
}
return result;
},
//
cancelStream() {
if (this.requestTask) {
this.requestTask.abort();
this.isTyping = false;
this.isStreaming = false;
this.requestTask = null;
}
},
receiveMessage() {
this.isTyping = true;
this.scrollToBottom(); this.scrollToBottom();
setTimeout(() => {
const replies = [
"明白了,谢谢您的解释明白了,谢谢您的解释明白了,谢谢您的解释明白了,谢谢您的解释明白了,谢谢您的解释明白了,谢谢您的解释明白了,谢谢您的解释明白了,谢谢您的解释明白了,谢谢您的解释明白了,谢谢您的解释明白了,谢谢您的解释",
"这个功能听起来很实用这个功能听起来很实用这个功能听起来很实用这个功能听起来很实用这个功能听起来很实用这个功能听起来很实用",
"请问价格方面是怎样的?这个功能听起来很实用这个功能听起来很实用这个功能听起来很实用这个功能听起来很实用这个功能听起来很实用这个功能听起来很实用",
"我稍后会仔细查看这些信息这个功能听起来很实用这个功能听起来很实用这个功能听起来很实用",
"好的,我会考虑购买专业版这个功能听起来很实用这个功能听起来很实用这个功能听起来很实用这个功能听起来很实用"
];
const newMsg = {
sender: 'other',
content: replies[Math.floor(Math.random() * replies.length)],
time: this.getTime()
};
this.messages.push(newMsg);
this.isTyping = false;
this.scrollToBottom();
}, 2000);
}, },
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(() => { setTimeout(() => {
this.scrollTop = Math.floor(Math.random() * 1000) + 10000; this.initWebSocket();
}, 100); }, 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) { onScroll(e) {
// //
},
onInput() {
//
if (this.inputMsg.trim() && !this.isTyping) {
this.isTyping = true;
setTimeout(() => {
this.isTyping = false;
}, 2000);
}
},
getTime() {
const now = new Date();
return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
},
goBack() {
uni.navigateBack();
} }
} }
} }
</script> </script>
<style> <style>
/* 通用样式 */
* {
box-sizing: border-box;
}
.chat-container { .chat-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -372,67 +332,16 @@
background-color: #f0f0f0; background-color: #f0f0f0;
} }
/* 顶部导航栏 */
.chat-header {
background: linear-gradient(135deg, #3498db, #1a5276);
padding: 10px 15px;
color: white;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.header-content {
display: flex;
align-items: center;
}
.user-info {
display: flex;
align-items: center;
margin-left: 15px;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.user-details {
margin-left: 10px;
display: flex;
flex-direction: column;
}
.user-name {
font-size: 18px;
font-weight: bold;
}
.user-status {
font-size: 12px;
opacity: 0.9;
}
/* 消息区域 */
.chat-messages { .chat-messages {
flex: 1; flex: 1;
overflow-y: auto;
/* padding: 15px; */ /* padding: 15px; */
background-color: rgb(229, 221, 213); background-color: #e5ddd5;
/* background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" opacity="0.05"><path d="M20,20 C30,15 40,25 50,20 C60,15 70,25 80,20" stroke="black" fill="none"/></svg>'); */ overflow-y: auto;
}
.message-time {
text-align: center;
color: #7f8c8d;
font-size: 12px;
margin: 10px 0;
} }
.message { .message {
display: flex; display: flex;
margin-bottom: 15px; margin: 10px;
} }
.sent { .sent {
@ -448,90 +357,56 @@
height: 36px; height: 36px;
border-radius: 50%; border-radius: 50%;
margin: 0 8px; margin: 0 8px;
/* align-self: flex-end; */
} }
.message-bubble { .message-bubble {
max-width: 70%; max-width: 70%;
background: white;
border-radius: 18px;
padding: 10px 15px; padding: 10px 15px;
position: relative; border-radius: 18px;
background-color: white;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
} }
.me { .me {
background: #dcf8c6; background-color: #dcf8c6;
border-bottom-right-radius: 4px;
} }
.message-text { .message-text {
font-size: 16px; font-size: 14px;
line-height: 1.4; line-height: 1.4;
} }
.message-status {
font-size: 11px;
color: #7f8c8d;
text-align: right;
margin-top: 3px;
}
/* 输入区域 */
.chat-input { .chat-input {
background: #f0f0f0;
padding: 10px 15px;
display: flex; display: flex;
align-items: center; padding: 10px;
background-color: white;
border-top: 1px solid #ddd; border-top: 1px solid #ddd;
} }
.input-container {
flex: 1;
background: white;
border-radius: 25px;
display: flex;
align-items: center;
padding: 5px 15px;
margin-right: 10px;
}
.input-field { .input-field {
flex: 1; flex: 1;
border: none; padding: 8px 15px;
outline: none; border-radius: 20px;
padding: 8px 0; background-color: #f0f0f0;
font-size: 16px; margin-right: 10px;
background: transparent;
}
.action-icon {
margin: 0 5px;
} }
.send-btn { .send-btn {
width: 40px; padding: 0 20px;
height: 40px; border-radius: 20px;
border-radius: 50%; background-color: #07C160;
background-color: #2196f3; color: white;
display: flex;
justify-content: center;
align-items: center;
border: none;
transition: all 0.2s;
} }
.send-btn.active { .send-btn:disabled {
background-color: #3498db; opacity: 0.5;
transform: scale(1.05);
} }
/* 对方正在输入提示 */ /* 对方正在输入提示 */
.typing-indicator { .typing-indicator {
display: flex; display: flex;
align-items: center; align-items: center;
margin: 10px 0 20px 10px; margin-top: 8px;
opacity: 0.7;
} }
.dot { .dot {
@ -556,7 +431,7 @@
} }
.typing-text { .typing-text {
font-size: 13px; font-size: 12px;
margin-left: 8px; margin-left: 8px;
color: #555; color: #555;
} }
@ -573,4 +448,33 @@
transform: translateY(-5px); 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> </style>

View File

@ -30,6 +30,7 @@
import Footer from '@/components/footer_common.vue'; import Footer from '@/components/footer_common.vue';
import { get, post } from '@/utils/request'; import { get, post } from '@/utils/request';
import { IMAGE_BASE_URL, BASE_URL } from '@/utils/config'; import { IMAGE_BASE_URL, BASE_URL } from '@/utils/config';
import {navigateTo} from '@/utils/router.js'
export default { export default {
name: 'aiHelper', name: 'aiHelper',
components: { Footer }, components: { Footer },
@ -45,7 +46,7 @@
}, },
methods: { methods: {
goAiType(item) { goAiType(item) {
uni.navigateTo({ navigateTo({
url: `/pages/chat/chatPage?serviceUrl=${item.serviceUrl}&apiKey=${item.apiKey}&id=${item.id}&name=${item.name}&icon=${item.icon}` url: `/pages/chat/chatPage?serviceUrl=${item.serviceUrl}&apiKey=${item.apiKey}&id=${item.id}&name=${item.name}&icon=${item.icon}`
}); });
}, },

View File

@ -89,7 +89,7 @@
<view class="date-section"> <view class="date-section">
<view class="section-header"> <view class="section-header">
<u-icon name="pushpin" size="38" color="#2979ff"></u-icon> <u-icon name="pushpin" size="38" color="#2979ff"></u-icon>
<text class="section-title">申请场次及人数</text> <text class="section-title">申请场次及人数(选填)</text>
</view> </view>
<view> <view>
@ -217,16 +217,14 @@
concatPhone: '负责人电话', concatPhone: '负责人电话',
companyName: '公司名称', companyName: '公司名称',
startTime: '开始时间', startTime: '开始时间',
endTime: '结束时间', endTime: '结束时间'
num: "人数",
counter: '场次'
}, },
userName: '', userName: '',
userPhone: "13667879876", userPhone: "",
userCardId: '1234', userCardId: '',
userAddress: '北京二环', userAddress: '',
counter: '10', counter: '',
num: '100', num: '',
// //
applyTheme: 1, applyTheme: 1,
applyArea:1, applyArea:1,
@ -267,8 +265,8 @@
maxDate: `${year + 1}-12-31`, maxDate: `${year + 1}-12-31`,
// //
startTime: `2025-08`, startTime: ``,
endTime: `2025-10`, endTime: ``,
startTimeValue: `${startHours.toString().padStart(2, '0')}:${startMinutes.toString().padStart(2, '0')}`, startTimeValue: `${startHours.toString().padStart(2, '0')}:${startMinutes.toString().padStart(2, '0')}`,
endTimeValue: `${endHours.toString().padStart(2, '0')}:${endMinutes.toString().padStart(2, '0')}`, endTimeValue: `${endHours.toString().padStart(2, '0')}:${endMinutes.toString().padStart(2, '0')}`,

View File

@ -600,14 +600,14 @@ import pickerColor from "./pickerColor.vue"
display: flex; display: flex;
align-content: center; align-content: center;
flex-direction:column; flex-direction:column;
justify-content: center; /* justify-content: center; */
font-size: 28rpx; font-size: 28rpx;
} }
.handWriting { .handWriting {
background: #fff; background: #fff;
width: 95vw; width: 95vw;
height: 90vh; height: 240px;
margin: 0 auto; margin: 0 auto;
} }
@ -618,7 +618,7 @@ import pickerColor from "./pickerColor.vue"
.handCenter { .handCenter {
border: 4rpx dashed #e9e9e9; border: 4rpx dashed #e9e9e9;
flex: 5; height: 240px;
overflow: hidden; overflow: hidden;
width: 95vw; width: 95vw;
box-sizing: border-box; box-sizing: border-box;

61
utils/ws.js Normal file
View File

@ -0,0 +1,61 @@
// 连接WebSocket
const connectWebSocket = () => {
if (isConnected.value) return;
socket.value = new WebSocket(WS_URL);
socket.value.onopen = () => {
isConnected.value = true;
addMessage('ai', '连接已建立,请问有什么可以帮您?', MESSAGE_TYPE.NORMAL);
};
socket.value.onmessage = (event) => {
console.log('收到消息:', event);
const data = event.data.trim();
if (data.startsWith('<think>')) {
aiStatus.value = 2; // 切换到思考状态
isReplying.value = true;
}
if (data.includes('</think>')) {
aiStatus.value = 1; // 切换到思考状态
}
if (data.startsWith('<answer>')) {
addingText.value = '';
isAnswer.value = true;
}
if (isAnswer.value) {
addingText.value += data.replace(/<answer>/g, '').replace(/<\/answer>/g, '');
console.log('添加的文本:', addingText.value);
}
if (!data.includes('你好,我是 AI 小助手,请输入你的问题。') && !data.includes('<videoList>')) {
processThinkingMessage(data);
}
if (data.includes('<videoList>')) {
const strList = data.split('<videoList>')[1].split('</videoList>')[0];
// videoList.value = [{ src: '/videos/1.mp4' }, { src: '/videos/2.mp4' }, { src: '/videos/3.mp4' }];
videoList.value = strList.replace(/\[/g, '').replace(/\]/g, '').split(',').map(src => ({ src: `/videos/${src.trim()}.mp4` }))
}
if (data.includes('<audio>')) {
audioPath.value = data.split('<audio>')[1].split('</audio>')[0];
}
if (data.includes('</answer>')) {
currentThinking.value = null
isReplying.value = false;
isAnswer.value = false;
audioText.value = addingText.value;
messages.value[messages.value.length - 1].loading = false;
// audioText.value='先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。然侍卫之臣不懈于内,忠志之士忘身于外者,盖追先帝之殊遇,欲报之于陛下也。诚宜开张圣听,以光先帝遗德,恢弘志士之气,不宜妄自菲薄,引喻失义,以塞忠谏之路也'
}
};
socket.value.onerror = (error) => {
console.error('WebSocket错误:', error);
addMessage('ai', '连接出错,请稍后再试', MESSAGE_TYPE.NORMAL);
};
socket.value.onclose = () => {
isConnected.value = false;
addMessage('ai', '连接已断开', MESSAGE_TYPE.NORMAL);
};
};