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>
<view class="chat-container">
<!-- 聊天消息区域 -->
<scroll-view scroll-y :scroll-top="scrollTop" class="chat-messages" :scroll-with-animation="true"
@scroll="onScroll">
<view class="message-time" v-if="showDate">今天 {{ getTime() }}</view>
<!-- 消息列表 -->
<view v-for="(msg, index) in messages" :key="index"
: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' : '']">
<text class="message-text" v-if="msg.content">{{ msg.content }}</text>
<!-- 对方正在输入提示 -->
<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>
<!-- <text class="message-status">{{ msg.time }}</text> -->
</view>
<image v-if="msg.sender === 'me'" :src="myAvatar" class="avatar"></image>
<image v-if="msg.sender === 'me'" :src="userAvatar" class="avatar"></image>
</view>
</scroll-view>
<!-- 输入区域 -->
<view class="chat-input">
<view class="input-container">
<!-- <uni-icons type="plus" size="28" color="#666" class="action-icon"></uni-icons> -->
<input class="input-field" placeholder="输入消息..." v-model="inputMsg" @confirm="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>
<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 uniIcons from '@dcloudio/uni-ui/lib/uni-icons/uni-icons.vue'
import {IMAGE_BASE_URL,BASE_URL} from '@/utils/config';
import {
BASE_URL
} from '@/utils/config';
export default {
components: {
uniIcons
},
data() {
return {
inputMsg: '',
scrollTop: 0,
isTyping: false,
isFocus: false,
showDate: true,
myAvatar: require('../../static/imgs/index/nav.png'),
currentChat: {
name: '张婷婷',
avatar: require('../../static/imgs/ai/chuandaguwen.png'),
status: '在线'
},
userAvatar: require('../../static/imgs/index/nav.png'),
botAvatar: require('../../static/imgs/ai/chuandaguwen.png'),
messages: [],
currentResponse: null, //
currentResponseId: null, //
abortController: null, //
isStreaming: false, //
socketTask: null, // WebSocket
isConnected: false,
reconnectAttempts: 0,
maxReconnectAttempts: 5,
reconnectDelay: 3000,
userId: null,
serviceUrl:'',
apiKey:'',
aiId:null,
aiName:''
userName: '默认用户',
apiKey: '',
serviceUrl: ''
}
},
onLoad(options) {
this.serviceUrl=options.serviceUrl
this.apiKey=options.apiKey
this.aiId=options.id
this.currentChat.avatar=options.icon
uni.setNavigationBarTitle({
title: options.name
})
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();
},
mounted() {
try {
const userInfo = wx.getStorageSync('userInfo')
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
}
onUnload() {
this.closeWebSocket();
},
methods: {
generateMiniProgramUUID() {
const buffer = new Uint8Array(16);
wx.getRandomValues(buffer);
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;
// WebSocket
initWebSocket() {
if (this.isConnected) return;
//
const userMsg = {
sender: 'me',
content: this.inputMsg,
time: this.getTime()
};
this.messages.push(userMsg);
const userInput = this.inputMsg;
this.inputMsg = '';
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);
// 使 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();
}
} finally {
this.isTyping = false;
this.isStreaming = false;
// this.abortController = null;
}
},
});
async streamResponse(inputParams, messageId) {
return new Promise((resolve, reject) => {
// 使uni.request
uni.request({
url: this.serviceUrl,
method: 'POST',
data: {
'query':inputParams,
"inputs": {
"content": inputParams
},
"response_mode": "blocking", //blockingstreaming
"user":this.userId
},
header: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
// signal: this.abortController?.signal, //
// enableChunked: true, //
success: (res) => {
// console.log(res)
//
if (res.data) {
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: () => {
//
}
});
// 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();
});
},
//
async updateAiMessage(id, newContent) {
const msg = this.messages.find(m => m.id === id);
if (msg) {
// JSON
if (newContent.includes('{') && newContent.includes('}')) {
try {
const jsonObj = JSON.parse(newContent);
msg.content = await this.formatJsonToReadableText(jsonObj);
msg.isTyping = false
processBotMessage(data) {
try {
//
const messageData = typeof data === 'string' ? JSON.parse(data) : data;
} catch (e) {
// JSON
msg.content = newContent;
}
} else {
msg.content = newContent;
msg.isTyping = false
if (messageData.event === 'text_chunk') {
//
this.handleTextChunk(messageData);
} else if (messageData.event === 'workflow_finished') {
//
this.handleNodeFinished(messageData);
}
this.scrollToBottom();
} catch (error) {
console.error('处理机器人消息出错:', error);
this.addMessage('bot', '处理消息时出错: ' + error.message, 'error');
}
},
// JSON
formatJsonToReadableText(jsonObj) {
let result = '';
//
handleTextChunk(chunkData) {
const text = chunkData.data.text;
if (!text || text.trim() === '') return;
// 穿
if (jsonObj.outfit) {
result += '👕 穿搭建议:\n';
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`;
//
const isThinking = text.includes('<think>') ||
(this.messages[this.messages.length - 1]?.type === 'thinking');
if (jsonObj.outfit.accessories && jsonObj.outfit.accessories.length > 0) {
result += '配饰:\n';
jsonObj.outfit.accessories.forEach(acc => {
result += `- ${acc.type}: ${acc.style} (${acc.color})\n`;
});
}
if (isThinking) {
this.handleThinkingContent(text);
} else {
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();
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(() => {
this.scrollTop = Math.floor(Math.random() * 1000) + 10000;
}, 100);
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) {
//
},
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>
<style>
/* 通用样式 */
* {
box-sizing: border-box;
}
.chat-container {
display: flex;
flex-direction: column;
@ -372,67 +332,16 @@
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 {
flex: 1;
overflow-y: auto;
/* padding: 15px; */
background-color: rgb(229, 221, 213);
/* 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>'); */
}
.message-time {
text-align: center;
color: #7f8c8d;
font-size: 12px;
margin: 10px 0;
background-color: #e5ddd5;
overflow-y: auto;
}
.message {
display: flex;
margin-bottom: 15px;
margin: 10px;
}
.sent {
@ -448,90 +357,56 @@
height: 36px;
border-radius: 50%;
margin: 0 8px;
/* align-self: flex-end; */
}
.message-bubble {
max-width: 70%;
background: white;
border-radius: 18px;
padding: 10px 15px;
position: relative;
border-radius: 18px;
background-color: white;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.me {
background: #dcf8c6;
border-bottom-right-radius: 4px;
background-color: #dcf8c6;
}
.message-text {
font-size: 16px;
font-size: 14px;
line-height: 1.4;
}
.message-status {
font-size: 11px;
color: #7f8c8d;
text-align: right;
margin-top: 3px;
}
/* 输入区域 */
.chat-input {
background: #f0f0f0;
padding: 10px 15px;
display: flex;
align-items: center;
padding: 10px;
background-color: white;
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 {
flex: 1;
border: none;
outline: none;
padding: 8px 0;
font-size: 16px;
background: transparent;
}
.action-icon {
margin: 0 5px;
padding: 8px 15px;
border-radius: 20px;
background-color: #f0f0f0;
margin-right: 10px;
}
.send-btn {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #2196f3;
display: flex;
justify-content: center;
align-items: center;
border: none;
transition: all 0.2s;
padding: 0 20px;
border-radius: 20px;
background-color: #07C160;
color: white;
}
.send-btn.active {
background-color: #3498db;
transform: scale(1.05);
.send-btn:disabled {
opacity: 0.5;
}
/* 对方正在输入提示 */
.typing-indicator {
display: flex;
align-items: center;
margin: 10px 0 20px 10px;
opacity: 0.7;
margin-top: 8px;
}
.dot {
@ -556,7 +431,7 @@
}
.typing-text {
font-size: 13px;
font-size: 12px;
margin-left: 8px;
color: #555;
}
@ -573,4 +448,33 @@
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>

View File

@ -30,6 +30,7 @@
import Footer from '@/components/footer_common.vue';
import { get, post } from '@/utils/request';
import { IMAGE_BASE_URL, BASE_URL } from '@/utils/config';
import {navigateTo} from '@/utils/router.js'
export default {
name: 'aiHelper',
components: { Footer },
@ -45,7 +46,7 @@
},
methods: {
goAiType(item) {
uni.navigateTo({
navigateTo({
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="section-header">
<u-icon name="pushpin" size="38" color="#2979ff"></u-icon>
<text class="section-title">申请场次及人数</text>
<text class="section-title">申请场次及人数(选填)</text>
</view>
<view>
@ -217,16 +217,14 @@
concatPhone: '负责人电话',
companyName: '公司名称',
startTime: '开始时间',
endTime: '结束时间',
num: "人数",
counter: '场次'
endTime: '结束时间'
},
userName: '',
userPhone: "13667879876",
userCardId: '1234',
userAddress: '北京二环',
counter: '10',
num: '100',
userName: '',
userPhone: "",
userCardId: '',
userAddress: '',
counter: '',
num: '',
//
applyTheme: 1,
applyArea:1,
@ -267,8 +265,8 @@
maxDate: `${year + 1}-12-31`,
//
startTime: `2025-08`,
endTime: `2025-10`,
startTime: ``,
endTime: ``,
startTimeValue: `${startHours.toString().padStart(2, '0')}:${startMinutes.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;
align-content: center;
flex-direction:column;
justify-content: center;
/* justify-content: center; */
font-size: 28rpx;
}
.handWriting {
background: #fff;
width: 95vw;
height: 90vh;
height: 240px;
margin: 0 auto;
}
@ -618,7 +618,7 @@ import pickerColor from "./pickerColor.vue"
.handCenter {
border: 4rpx dashed #e9e9e9;
flex: 5;
height: 240px;
overflow: hidden;
width: 95vw;
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);
};
};