ws聊天记录
This commit is contained in:
parent
a411e94890
commit
859d6f2e79
353
pages/chat/chatPage - 副本 (2).vue
Normal file
353
pages/chat/chatPage - 副本 (2).vue
Normal 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>
|
||||||
@ -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": {
|
|
||||||
"content": inputParams
|
|
||||||
},
|
|
||||||
"response_mode": "blocking", //blocking,streaming
|
|
||||||
"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) {
|
this.socketTask.onMessage((res) => {
|
||||||
console.error('Parse error:', e);
|
// console.log('收到WebSocket消息:', res.data);
|
||||||
}
|
this.processBotMessage(res.data);
|
||||||
}
|
});
|
||||||
},
|
|
||||||
fail: (err) => {
|
this.socketTask.onError((err) => {
|
||||||
reject(new Error(err.errMsg));
|
console.error('WebSocket发生错误:', err);
|
||||||
},
|
this.isConnected = false;
|
||||||
complete: () => {
|
this.addMessage('bot', '连接出错,请稍后再试');
|
||||||
// 请求完成
|
this.handleReconnect();
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
this.socketTask.onClose(() => {
|
||||||
|
console.log('WebSocket连接已关闭');
|
||||||
|
this.isConnected = false;
|
||||||
|
this.handleReconnect();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// 更新消息内容
|
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>
|
||||||
@ -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}`
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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')}`,
|
||||||
|
|
||||||
|
|||||||
@ -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
61
utils/ws.js
Normal 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);
|
||||||
|
};
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user