- Implemented a customer service chat interface in the application. - Added socket.io-client for real-time communication. - Updated language files to include customer service related translations. - Modified navigation to direct users to the customer service page. - Enhanced UI elements for better user experience in chat interactions.
519 lines
13 KiB
Vue
519 lines
13 KiB
Vue
<template>
|
||
<view class="customservice">
|
||
<u-navbar
|
||
leftIconColor="#d9ac6b"
|
||
bgColor="#1f120e"
|
||
:placeholder="true"
|
||
:fixed="true"
|
||
height="50"
|
||
@leftClick="back"
|
||
:title="$lang.customerService || '在线客服'"
|
||
titleStyle="color: #d9ac6b; font-weight: bold;"
|
||
></u-navbar>
|
||
|
||
<!-- 聊天窗口 -->
|
||
<view class="chat-container">
|
||
<!-- 聊天头部状态 -->
|
||
<view class="chat-header">
|
||
<view class="chat-status" :class="{'online': sessionId}">
|
||
{{ sessionId ? ($lang.csConnected || '已连接') : ($lang.csConnecting || '连接中...') }}
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 消息列表 -->
|
||
<scroll-view class="chat-messages" scroll-y :scroll-top="scrollTop" scroll-with-animation>
|
||
<!-- 欢迎信息 -->
|
||
<view v-if="!sessionId" class="chat-welcome">
|
||
<text class="welcome-icon">💬</text>
|
||
<text class="welcome-title">{{ $lang.csWelcome || '欢迎使用在线客服' }}</text>
|
||
<text v-if="!isConnected" class="welcome-tip">{{ $lang.csConnecting || '正在连接...' }}</text>
|
||
<text v-else-if="sessionStatus === 0" class="welcome-tip warning">{{ $lang.csBusy || '客服繁忙,请稍候...' }}</text>
|
||
<text v-else class="welcome-tip">{{ $lang.csInputTip || '请输入您的问题' }}</text>
|
||
</view>
|
||
|
||
<!-- 消息列表 -->
|
||
<view v-for="(msg, index) in messages" :key="index"
|
||
class="chat-message"
|
||
:class="{
|
||
'chat-message-own': msg.senderType === 1,
|
||
'chat-message-system': msg.senderType === 0
|
||
}">
|
||
<!-- 系统消息 -->
|
||
<view v-if="msg.senderType === 0" class="system-msg">
|
||
<text>{{ msg.content }}</text>
|
||
</view>
|
||
<!-- 普通消息 -->
|
||
<view v-else class="message-content">
|
||
<view class="message-avatar">
|
||
<text v-if="msg.senderType === 1">{{ $lang.csMe || '我' }}</text>
|
||
<text v-else>{{ $lang.csAgent || '客服' }}</text>
|
||
</view>
|
||
<view class="message-bubble">
|
||
<text class="message-text">{{ msg.content }}</text>
|
||
<text class="message-time">{{ formatTime(msg.createTime) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部占位 -->
|
||
<view style="height: 20px;"></view>
|
||
</scroll-view>
|
||
|
||
<!-- 输入区域 -->
|
||
<view class="chat-input-area">
|
||
<textarea
|
||
v-model="inputMessage"
|
||
:placeholder="$lang.csInputPlaceholder || '输入消息,点击发送...'"
|
||
:disabled="!isConnected || !sessionId"
|
||
class="chat-input"
|
||
:maxlength="500"
|
||
@confirm="sendMessage"
|
||
></textarea>
|
||
<view class="chat-actions">
|
||
<button
|
||
class="chat-btn-send"
|
||
@click="sendMessage"
|
||
:disabled="!isConnected || !sessionId || !inputMessage.trim()"
|
||
>{{ $lang.csSend || '发送' }}</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { mapState } from "vuex";
|
||
import io from 'socket.io-client';
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
socket: null,
|
||
isConnected: false,
|
||
sessionId: null,
|
||
sessionStatus: null,
|
||
messages: [],
|
||
inputMessage: '',
|
||
scrollTop: 0,
|
||
heartbeatTimer: null
|
||
};
|
||
},
|
||
computed: {
|
||
...mapState({
|
||
userInfo: state => state.app.userInfo,
|
||
sockUrl: state => state.app.sockUrl,
|
||
$lang: state => state.app.lang[state.app.language]
|
||
})
|
||
},
|
||
onLoad() {
|
||
this.connectWebSocket();
|
||
},
|
||
onUnload() {
|
||
this.disconnect();
|
||
},
|
||
methods: {
|
||
back() {
|
||
this.disconnect();
|
||
uni.navigateBack();
|
||
},
|
||
disconnect() {
|
||
if (this.heartbeatTimer) {
|
||
clearInterval(this.heartbeatTimer);
|
||
this.heartbeatTimer = null;
|
||
}
|
||
if (this.socket) {
|
||
this.socket.disconnect();
|
||
this.socket = null;
|
||
}
|
||
},
|
||
connectWebSocket() {
|
||
// 使用 store 中的 sockUrl
|
||
const sockUrl = this.sockUrl || 'wss://wss.g7g7.top';
|
||
const loginToken = this.userInfo.online_token || this.userInfo.login_token || '';
|
||
|
||
console.log('[CustomerService] 连接 WebSocket:', sockUrl);
|
||
console.log('[CustomerService] loginToken:', loginToken);
|
||
console.log('[CustomerService] userInfo:', JSON.stringify(this.userInfo));
|
||
|
||
// 客服连接:不带 connect=user 参数,避免触发游戏连接逻辑
|
||
// 连接成功后通过 chat.connect 事件进行客服认证
|
||
this.socket = io(sockUrl, {
|
||
transports: ['websocket'],
|
||
reconnection: true,
|
||
reconnectionDelay: 1000,
|
||
reconnectionAttempts: 5
|
||
});
|
||
|
||
this.socket.on('connect', () => {
|
||
console.log('[CustomerService] WebSocket 已连接, socket.id:', this.socket.id);
|
||
this.isConnected = true;
|
||
|
||
// 发送客服连接请求,后端通过 chat.connect 事件验证用户
|
||
const chatConnectData = {
|
||
token: loginToken,
|
||
role: 'user',
|
||
source: 3 // Portal端
|
||
};
|
||
console.log('[CustomerService] 发送 chat.connect:', JSON.stringify(chatConnectData));
|
||
this.socket.emit('chat.connect', chatConnectData);
|
||
|
||
// 启动心跳
|
||
this.startHeartbeat();
|
||
});
|
||
|
||
this.socket.on('disconnect', (reason) => {
|
||
console.log('[CustomerService] WebSocket 已断开, 原因:', reason);
|
||
this.isConnected = false;
|
||
});
|
||
|
||
this.socket.on('connect_error', (error) => {
|
||
console.log('[CustomerService] 连接错误:', error.message);
|
||
this.isConnected = false;
|
||
});
|
||
|
||
this.socket.on('error', (error) => {
|
||
console.log('[CustomerService] Socket错误:', error);
|
||
});
|
||
|
||
// 后端 ChatConnect.php 使用下划线格式的事件名
|
||
this.socket.on('chat_connected', (data) => {
|
||
console.log('[CustomerService] chat_connected:', data);
|
||
if (data.success) {
|
||
this.sessionId = data.sessionId;
|
||
this.sessionStatus = data.status;
|
||
if (data.agentInfo) {
|
||
this.addSystemMessage((this.$lang.csAgent || '客服') + ' ' + data.agentInfo.nickname + ' ' + (this.$lang.csAgentServing || '为您服务'));
|
||
}
|
||
} else {
|
||
uni.showToast({ title: (this.$lang.csConnectFailed || '连接失败') + ': ' + (data.error || '未知错误'), icon: 'none' });
|
||
}
|
||
});
|
||
|
||
this.socket.on('chat.session.assigned', (data) => {
|
||
console.log('[CustomerService] chat.session.assigned:', data);
|
||
this.sessionStatus = 1;
|
||
this.addSystemMessage((this.$lang.csAgent || '客服') + ' ' + data.agentInfo.nickname + ' ' + (this.$lang.csAgentJoined || '已接入'));
|
||
});
|
||
|
||
this.socket.on('chat_message_new', (data) => {
|
||
console.log('[CustomerService] chat_message_new:', data);
|
||
const msg = data.data || data;
|
||
this.messages.push({
|
||
msgId: msg.msgId,
|
||
senderType: msg.senderType,
|
||
msgType: msg.msgType,
|
||
content: msg.content,
|
||
createTime: msg.createTime || msg.time
|
||
});
|
||
this.scrollToBottom();
|
||
|
||
// 发送已读回执
|
||
if (msg.msgId) {
|
||
this.socket.emit('chat.message.ack', { msgId: msg.msgId });
|
||
}
|
||
});
|
||
|
||
this.socket.on('chat.session.ended', () => {
|
||
console.log('[CustomerService] chat.session.ended');
|
||
this.addSystemMessage(this.$lang.csSessionEnded || '会话已结束');
|
||
this.sessionStatus = 2;
|
||
});
|
||
|
||
// 后端使用下划线格式
|
||
this.socket.on('chat_offline_notice', (data) => {
|
||
console.log('[CustomerService] chat_offline_notice:', data);
|
||
this.addSystemMessage(data.message);
|
||
});
|
||
|
||
// 后端使用下划线格式
|
||
this.socket.on('chat_pong', () => {
|
||
// 心跳响应
|
||
});
|
||
},
|
||
startHeartbeat() {
|
||
if (this.heartbeatTimer) {
|
||
clearInterval(this.heartbeatTimer);
|
||
}
|
||
this.heartbeatTimer = setInterval(() => {
|
||
if (this.socket && this.socket.connected) {
|
||
this.socket.emit('chat.ping', {});
|
||
}
|
||
}, 25000);
|
||
},
|
||
sendMessage() {
|
||
const content = this.inputMessage.trim();
|
||
if (!content || !this.sessionId) return;
|
||
|
||
const clientMsgId = Date.now();
|
||
this.socket.emit('chat.message.send', {
|
||
sessionId: this.sessionId,
|
||
msgType: 1,
|
||
content: content,
|
||
clientMsgId: clientMsgId
|
||
});
|
||
|
||
// 乐观更新 UI
|
||
this.messages.push({
|
||
msgId: clientMsgId,
|
||
senderType: 1, // 用户发送
|
||
msgType: 1,
|
||
content: content,
|
||
createTime: Date.now()
|
||
});
|
||
|
||
this.inputMessage = '';
|
||
this.scrollToBottom();
|
||
},
|
||
addSystemMessage(text) {
|
||
this.messages.push({
|
||
msgId: Date.now(),
|
||
senderType: 0,
|
||
msgType: 1,
|
||
content: text,
|
||
createTime: Date.now()
|
||
});
|
||
this.scrollToBottom();
|
||
},
|
||
scrollToBottom() {
|
||
// 先重置为0,再设置大值,确保Vue检测到变化
|
||
this.scrollTop = 0;
|
||
this.$nextTick(() => {
|
||
this.scrollTop = 999999;
|
||
});
|
||
},
|
||
formatTime(timestamp) {
|
||
if (!timestamp) return '';
|
||
// 如果是毫秒时间戳
|
||
const ts = timestamp > 9999999999 ? timestamp : timestamp * 1000;
|
||
const date = new Date(ts);
|
||
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.customservice {
|
||
background: #0b1520;
|
||
width: 100%;
|
||
min-height: 100vh;
|
||
|
||
::v-deep .u-navbar__content__title {
|
||
color: #d9ac6b;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 聊天容器 */
|
||
.chat-container {
|
||
position: fixed;
|
||
top: 50px;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: #3d2c22;
|
||
}
|
||
|
||
/* 聊天头部 */
|
||
.chat-header {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
padding: 10px 15px;
|
||
background: #1f120e;
|
||
}
|
||
|
||
.chat-status {
|
||
padding: 4px 16px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 12px;
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
|
||
.chat-status.online {
|
||
background: rgba(76, 175, 80, 0.3);
|
||
color: #4caf50;
|
||
}
|
||
|
||
/* 消息区域 */
|
||
.chat-messages {
|
||
flex: 1;
|
||
padding: 15px;
|
||
background: #3d2c22;
|
||
overflow: hidden;
|
||
height: 0; /* 配合flex:1让scroll-view正常工作 */
|
||
}
|
||
|
||
/* 欢迎信息 */
|
||
.chat-welcome {
|
||
text-align: center;
|
||
padding: 60px 20px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 10px;
|
||
|
||
.welcome-icon {
|
||
font-size: 48px;
|
||
}
|
||
|
||
.welcome-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #d9ac6b;
|
||
}
|
||
|
||
.welcome-tip {
|
||
font-size: 14px;
|
||
color: #999;
|
||
}
|
||
|
||
.welcome-tip.warning {
|
||
color: #ff9800;
|
||
}
|
||
}
|
||
|
||
/* 消息项 */
|
||
.chat-message {
|
||
margin-bottom: 15px;
|
||
display: flex;
|
||
}
|
||
|
||
.chat-message-own {
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.chat-message-system {
|
||
justify-content: center;
|
||
}
|
||
|
||
/* 系统消息 */
|
||
.system-msg {
|
||
background: rgba(0, 0, 0, 0.3);
|
||
color: #999;
|
||
font-size: 12px;
|
||
padding: 5px 15px;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
/* 消息内容 */
|
||
.message-content {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
max-width: 80%;
|
||
}
|
||
|
||
.chat-message-own .message-content {
|
||
flex-direction: row-reverse;
|
||
}
|
||
|
||
/* 头像 */
|
||
.message-avatar {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 50%;
|
||
background: #1f120e;
|
||
color: #d9ac6b;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
font-weight: bold;
|
||
flex-shrink: 0;
|
||
margin: 0 10px;
|
||
}
|
||
|
||
.chat-message-own .message-avatar {
|
||
background: linear-gradient(135deg, #d9ac6b 0%, #a67c3d 100%);
|
||
color: #1f120e;
|
||
}
|
||
|
||
/* 消息气泡 */
|
||
.message-bubble {
|
||
background: #2a1f18;
|
||
border-radius: 8px;
|
||
padding: 10px 12px;
|
||
}
|
||
|
||
.chat-message-own .message-bubble {
|
||
background: linear-gradient(135deg, #d9ac6b 0%, #a67c3d 100%);
|
||
}
|
||
|
||
.message-text {
|
||
color: #e0e0e0;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.chat-message-own .message-text {
|
||
color: #1f120e;
|
||
}
|
||
|
||
.message-time {
|
||
font-size: 11px;
|
||
color: #777;
|
||
margin-top: 5px;
|
||
display: block;
|
||
text-align: right;
|
||
}
|
||
|
||
.chat-message-own .message-time {
|
||
color: rgba(31, 18, 14, 0.6);
|
||
}
|
||
|
||
/* 输入区域 */
|
||
.chat-input-area {
|
||
flex-shrink: 0;
|
||
border-top: 1px solid #2a1f18;
|
||
padding: 12px;
|
||
background: #1f120e;
|
||
padding-bottom: calc(12px + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
.chat-input {
|
||
width: 100%;
|
||
min-height: 60px;
|
||
max-height: 100px;
|
||
background: #3d2c22;
|
||
border: 1px solid #553e31;
|
||
border-radius: 6px;
|
||
padding: 10px;
|
||
font-size: 14px;
|
||
color: #e0e0e0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.chat-input[disabled] {
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.chat-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.chat-btn-send {
|
||
padding: 10px 30px;
|
||
border: none;
|
||
border-radius: 20px;
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
background: linear-gradient(180deg, #f7ee99 0%, #78681c 50%, #c0b141 100%);
|
||
color: #1f120e;
|
||
}
|
||
|
||
.chat-btn-send:active {
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.chat-btn-send[disabled] {
|
||
opacity: 0.5;
|
||
}
|
||
}
|
||
</style>
|