feat: integrate customer service chat functionality and add socket.io-client dependency

- 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.
This commit is contained in:
li
2026-01-29 03:04:52 +08:00
parent 14c8cbffcc
commit 322cc24208
12 changed files with 1629 additions and 72 deletions
+473 -69
View File
@@ -1,51 +1,81 @@
<template>
<view class="customservice">
<u-navbar
leftIconColor="#eee"
bgColor="#0c0c0d"
leftIconColor="#d9ac6b"
bgColor="#1f120e"
:placeholder="true"
:fixed="true"
height="60"
height="50"
@leftClick="back"
:title="$lang.customerService || '在线客服'"
titleStyle="color: #d9ac6b; font-weight: bold;"
></u-navbar>
<view class="cellBox">
<view class="cell">
<u--image
:showLoading="false"
:src="require('static/images/customservice.png')"
width="100%"
height="auto"
mode="widthFix"
class="icon"
></u--image>
<u-link
target="_blank"
:href="customServiceUrl"
text="Online service"
class="lable"
></u-link>
<!-- 聊天窗口 -->
<view class="chat-container">
<!-- 聊天头部状态 -->
<view class="chat-header">
<view class="chat-status" :class="{'online': sessionId}">
{{ sessionId ? ($lang.csConnected || '已连接') : ($lang.csConnecting || '连接中...') }}
</view>
</view>
<view class="cell">
<u--image
:showLoading="false"
:src="require('static/images/line.png')"
width="100%"
height="auto"
mode="widthFix"
class="icon"
></u--image>
<u-link target="_blank" :href="telegramUrl" text="Telegram" class="lable"></u-link>
</view>
<view class="cell">
<u--image
:showLoading="false"
:src="require('static/images/telegram.png')"
width="100%"
height="auto"
mode="widthFix"
class="icon"
></u--image>
<u-link target="_blank" :href="lineUrl" text="Line" class="lable"></u-link>
<!-- 消息列表 -->
<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>
@@ -53,21 +83,210 @@
<script>
import { mapState } from "vuex";
import io from 'socket.io-client';
export default {
data() {
return {};
return {
socket: null,
isConnected: false,
sessionId: null,
sessionStatus: null,
messages: [],
inputMessage: '',
scrollTop: 0,
heartbeatTimer: null
};
},
computed: {
...mapState({
customServiceUrl: state => state.app.customServiceUrl,
telegramUrl: state => state.app.telegramUrl,
lineUrl: state => state.app.lineUrl
userInfo: state => state.app.userInfo,
sockUrl: state => state.app.sockUrl,
$lang: state => state.app.lang[state.app.language]
})
},
onLoad() {},
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' });
}
}
};
@@ -78,37 +297,222 @@ export default {
background: #0b1520;
width: 100%;
min-height: 100vh;
::v-deep .u-navbar__content__title {
color: #eee;
color: #d9ac6b;
font-weight: 600;
}
.cellBox {
position: absolute;
top: 30%;
left: 50%;
transform: translate(-50%, -50%);
width: 300px;
}
.cell {
/* 聊天容器 */
.chat-container {
position: fixed;
top: 50px;
left: 0;
right: 0;
bottom: 0;
display: flex;
padding: 15px;
flex-direction: column;
background: #3d2c22;
}
/* 聊天头部 */
.chat-header {
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
// background: #151617;
color: #deb366;
.icon {
width: 35px;
margin-right: 15px;
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;
}
.lable {
// border-bottom: 1px solid;
color: #deb366 !important;
font-size: 30px !important;
line-height: 30px !important;
::v-deep span {
border-bottom: 1px solid;
padding-bottom: 4px;
}
.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>