This commit is contained in:
Leo_Ding 2025-08-08 19:07:37 +08:00
commit cd8f2588e6
4 changed files with 383 additions and 10 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

@ -101,7 +101,8 @@
<script> <script>
import { import {
get, get,
post post,
del
} from '@/utils/request'; } from '@/utils/request';
import { import {
IMAGE_BASE_URL, IMAGE_BASE_URL,
@ -236,16 +237,14 @@
title: '处理中...', title: '处理中...',
mask: true mask: true
}); });
const res = await post('/api/v1/app_auth/metting-room/order/cancel', { const res = await del(`/api/v1/app_auth/metting-room/order/${orderId}`);
id: orderId
});
if (res?.success) { if (res?.success) {
uni.showToast({ uni.showToast({
title: '取消成功', title: '取消成功',
icon: 'success' icon: 'success'
}); });
this.getOrderList(); // this.getOrderList();
} else { } else {
uni.showToast({ uni.showToast({
title: res?.message || '取消失败', title: res?.message || '取消失败',

View File

@ -33,7 +33,8 @@
:class="{'is-reply': comment.toUserName}"> :class="{'is-reply': comment.toUserName}">
<view class="comment-header"> <view class="comment-header">
<image class="avatar" <image class="avatar"
:src="comment.pusherPortrait === '/static/imgs/index/nav.png' ? comment.pusherPortrait : `${IMAGE_BASE_URL}${comment.pusherPortrait}`" mode="aspectFill"></image> :src="getAvatarSrc(comment.pusherPortrait)"
mode="aspectFill"></image>
<view class="comment-user-info"> <view class="comment-user-info">
<text class="username">{{ comment.pusherName }}</text> <text class="username">{{ comment.pusherName }}</text>
<view class="meta-info"> <view class="meta-info">
@ -114,8 +115,27 @@
this.error = '缺少内容ID'; this.error = '缺少内容ID';
this.loading = false; this.loading = false;
} }
},
computed: {
}, },
methods: { methods: {
getAvatarSrc(portrait) {
const defaultAvatar = '/static/imgs/index/nav.png';
if (!portrait) {
return defaultAvatar;
}
const defaultUrls = ['/static/imgs/index/nav.png', 'https://jinshan.nantong.info'];
if (defaultUrls.includes(portrait) || portrait.startsWith('http')) {
return portrait;
}
return `${this.IMAGE_BASE_URL}${portrait}`;
},
// //
updateFlatComments() { updateFlatComments() {
const flattenComments = (comments, parentUser = '') => { const flattenComments = (comments, parentUser = '') => {

View File

@ -1,8 +1,9 @@
export const BASE_URL = 'https://jinshan.nantong.info';
export const IMAGE_BASE_URL = `https://jinshan.nantong.info`
export const WS_BASE_URL = `wss://jinshan.nantong.info`
// export const BASE_URL = 'http://10.10.1.6:8071'; // export const BASE_URL = 'http://10.10.1.6:8071';
// export const IMAGE_BASE_URL = `http://10.10.1.6:8071`; // export const IMAGE_BASE_URL = `http://10.10.1.6:8071`;
export const BASE_URL = 'https://jinshan.nantong.info';
export const IMAGE_BASE_URL = `https://jinshan.nantong.info`;
export const WS_BASE_URL = `wss://jinshan.nantong.info`;
// http://36.212.197.253:8071 // http://36.212.197.253:8071