refactor(customer-service): 优化客服模块交互,点击直接弹出聊天框

- 修改 header.vue 和 play.vue 客服按钮点击事件,跳过中间选择弹窗
- 重写 CustomerServiceWindow.vue 样式,匹配 PC 端深色主题风格
- 移除图片发送功能,仅保留文本消息
- 新增遮罩层、头像区分、金色渐变按钮等 UI 元素
This commit is contained in:
li 2026-01-29 01:41:01 +08:00
parent b7061d7a0d
commit 8d9154c668
3 changed files with 556 additions and 3 deletions

View File

@ -8,7 +8,7 @@
</div>
<div class="setup">
<el-tooltip v-if="webconfig.isServerCode||false" class="item" effect="dark" :content="Language.tip_service" placement="bottom">
<span @click="isShowPop('updateService')" class="btn service" ></span>
<span @click="openCustomerService()" class="btn service" ></span>
</el-tooltip>
<el-tooltip class="item" effect="dark" :content="Language.tip_cn_tw" placement="bottom">
<span @click="isShowPop('updatelang')" class="btn fontj" ></span>
@ -69,7 +69,10 @@ export default {
}else{
this.$store.dispatch('updatemainPop',{type:type,ishow:true});
}
},
openCustomerService(){
this.$root.$emit('openCustomerService');
},
offPop(){
this.$store.dispatch('updatemainPop',{type:'',ishow:false});

View File

@ -7,7 +7,7 @@
</div>
<div class="setup">
<el-tooltip v-if="webconfig.isServerCode||false" class="item" effect="dark" :content="Language.tip_service" placement="bottom">
<span @click="isShowPop('updateService')" class="btn service" ></span>
<span @click="openCustomerService()" class="btn service" ></span>
</el-tooltip>
<el-tooltip class="item" effect="dark" :content="Language.tip_cn_tw" placement="bottom">
<span @click="isShowPop('updatelang')" class="btn fontj" ></span>
@ -192,6 +192,9 @@ export default {
}
},
openCustomerService(){
this.$root.$emit('openCustomerService');
},
offPop(){
this.$store.dispatch('updatemainPop',{type:'',ishow:false});
},

View File

@ -0,0 +1,547 @@
<template>
<div class="cs-overlay" v-if="visible" @click.self="closeWindow">
<div class="customer-service-window animated bounceInUp">
<!-- 标题栏 -->
<div class="cs-header">
<span class="cs-title">在线客服</span>
<span class="cs-status" :class="{'online': isConnected}">
{{ isConnected ? '已连接' : '连接中...' }}
</span>
<button class="cs-close" @click="closeWindow">×</button>
</div>
<!-- 消息区域 -->
<div class="cs-body">
<div class="cs-messages" ref="messageList">
<!-- 欢迎信息 -->
<div v-if="!sessionId" class="cs-welcome">
<div class="cs-welcome-icon">💬</div>
<p class="cs-welcome-title">欢迎使用在线客服</p>
<p v-if="!isConnected" class="cs-welcome-tip">正在连接...</p>
<p v-else-if="sessionStatus === 0" class="cs-welcome-tip warning">客服繁忙请稍候...</p>
<p v-else class="cs-welcome-tip">请输入您的问题</p>
</div>
<!-- 消息列表 -->
<div v-for="msg in messages" :key="msg.msgId || msg.id"
class="cs-message"
:class="{
'cs-message-own': msg.senderType === 1 || msg.sender_type === 1,
'cs-message-system': msg.senderType === 0
}">
<!-- 系统消息 -->
<div v-if="msg.senderType === 0" class="cs-system-msg">
{{ msg.content }}
</div>
<!-- 普通消息 -->
<div v-else class="cs-message-content">
<div class="cs-message-avatar">
<span v-if="msg.senderType === 1 || msg.sender_type === 1"></span>
<span v-else>客服</span>
</div>
<div class="cs-message-bubble">
<div class="cs-message-text">{{ msg.content }}</div>
<div class="cs-message-time">{{ formatTime(msg.createTime || msg.create_time) }}</div>
</div>
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="cs-input-area">
<textarea
v-model="inputMessage"
@keydown.enter.exact.prevent="sendMessage"
placeholder="输入消息按Enter发送..."
:disabled="!isConnected || !sessionId"
></textarea>
<div class="cs-input-actions">
<button class="cs-btn cs-btn-send" @click="sendMessage" :disabled="!isConnected || !sessionId || !inputMessage.trim()">
发送
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'CustomerServiceWindow',
data() {
return {
visible: false,
isConnected: false,
sessionId: null,
sessionStatus: null,
messages: [],
inputMessage: '',
agentInfo: null,
heartbeatTimer: null
}
},
computed: {
...mapState(['userInfo', 'webconfig']),
socket() {
return this.$store.state.io
}
},
mounted() {
// this
window.__chatConnectedCallback = this.onChatConnected.bind(this)
window.__chatSessionAssignedCallback = this.onChatSessionAssigned.bind(this)
window.__chatMessageNewCallback = this.onChatMessageNew.bind(this)
window.__chatSessionEndedCallback = this.onChatSessionEnded.bind(this)
window.__chatOfflineNoticeCallback = this.onChatOfflineNotice.bind(this)
},
beforeDestroy() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer)
}
//
window.__chatConnectedCallback = null
window.__chatSessionAssignedCallback = null
window.__chatMessageNewCallback = null
window.__chatSessionEndedCallback = null
window.__chatOfflineNoticeCallback = null
},
methods: {
open() {
this.visible = true
this.connectChat()
},
closeWindow() {
this.visible = false
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer)
}
},
connectChat() {
console.log('=== 客服系统复用现有Socket连接 ===')
if (!this.socket) {
console.error('Socket未初始化请先登录游戏')
this.$message.error('请先登录游戏')
return
}
this.isConnected = this.socket.connected
console.log('Socket连接状态:', this.isConnected)
//
console.log('发送chat.connect事件token:', this.userInfo.online_token)
this.socket.emit('chat.connect', {
token: this.userInfo.online_token,
role: 'user',
source: 1 // PC
})
//
this.startHeartbeat()
},
//
onChatConnected(data) {
console.log('[CustomerService] onChatConnected被调用:', data)
console.log('[CustomerService] this.visible:', this.visible)
if (data.success) {
this.sessionId = data.sessionId
this.sessionStatus = data.status
this.agentInfo = data.agentInfo
console.log('[CustomerService] 设置sessionId:', this.sessionId)
this.loadHistory()
}
},
//
onChatSessionAssigned(data) {
this.agentInfo = data.agentInfo
this.sessionStatus = 1
this.addSystemMessage('客服 ' + data.agentInfo.nickname + ' 已接入')
},
//
onChatMessageNew(data) {
console.log('[CustomerService] onChatMessageNew:', data)
// {data: {...}}
const msg = data.data || data
this.messages.push({
msgId: msg.msgId,
senderType: msg.senderType,
msgType: msg.msgType,
content: msg.content,
createTime: msg.createTime
})
this.scrollToBottom()
//
if (msg.msgId) {
this.socket.emit('chat.message.ack', { msgId: msg.msgId })
}
},
//
onChatSessionEnded(data) {
this.addSystemMessage('会话已结束')
this.sessionStatus = 2
},
// 线
onChatOfflineNotice(data) {
this.addSystemMessage(data.message)
},
setupChatListeners() {
console.log('[Debug] setupChatListeners 已废弃,使用全局回调')
},
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()
},
loadHistory() {
if (!this.sessionId) return
// APIPro使API
// WebSocket
console.log('[CustomerService] 跳过历史消息加载(跨域限制)')
// TODO: WebSocketAPI
},
addSystemMessage(text) {
this.messages.push({
msgId: Date.now(),
senderType: 0,
msgType: 1,
content: text,
createTime: Date.now()
})
this.scrollToBottom()
},
scrollToBottom() {
this.$nextTick(() => {
const el = this.$refs.messageList
if (el) {
el.scrollTop = el.scrollHeight
}
})
},
formatTime(timestamp) {
if (!timestamp) return ''
const date = new Date(timestamp * 1000)
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
}
}
}
</script>
<style scoped>
/* 遮罩层 */
.cs-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9998;
display: flex;
justify-content: center;
align-items: center;
}
/* 主窗口 - PC端深色主题 */
.customer-service-window {
width: 420px;
height: 560px;
background: #553e31;
border-radius: 8px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 标题栏 */
.cs-header {
height: 45px;
background: #1f120e;
color: #fff;
padding: 0 15px;
display: flex;
align-items: center;
justify-content: space-between;
}
.cs-title {
font-size: 16px;
font-weight: bold;
color: #d9ac6b;
}
.cs-status {
font-size: 12px;
padding: 3px 10px;
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
color: #999;
}
.cs-status.online {
background: rgba(76, 175, 80, 0.3);
color: #4caf50;
}
.cs-close {
background: none;
border: none;
color: #d9ac6b;
font-size: 24px;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
line-height: 28px;
text-align: center;
opacity: 0.8;
transition: opacity 0.2s;
}
.cs-close:hover {
opacity: 1;
}
/* 消息区域 */
.cs-body {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.cs-messages {
flex: 1;
overflow-y: auto;
padding: 15px;
background: #3d2c22;
}
/* 滚动条样式 */
.cs-messages::-webkit-scrollbar {
width: 6px;
}
.cs-messages::-webkit-scrollbar-track {
background: #2a1f18;
}
.cs-messages::-webkit-scrollbar-thumb {
background: #d9ac6b;
border-radius: 3px;
}
/* 欢迎信息 */
.cs-welcome {
text-align: center;
padding: 60px 20px;
}
.cs-welcome-icon {
font-size: 48px;
margin-bottom: 15px;
}
.cs-welcome-title {
font-size: 18px;
font-weight: bold;
color: #d9ac6b;
margin-bottom: 10px;
}
.cs-welcome-tip {
font-size: 14px;
color: #999;
}
.cs-welcome-tip.warning {
color: #ff9800;
}
/* 消息项 */
.cs-message {
margin-bottom: 15px;
display: flex;
}
.cs-message-own {
justify-content: flex-end;
}
.cs-message-system {
justify-content: center;
}
/* 系统消息 */
.cs-system-msg {
background: rgba(0, 0, 0, 0.3);
color: #999;
font-size: 12px;
padding: 5px 15px;
border-radius: 12px;
}
/* 消息内容 */
.cs-message-content {
display: flex;
align-items: flex-start;
max-width: 80%;
}
.cs-message-own .cs-message-content {
flex-direction: row-reverse;
}
/* 头像 */
.cs-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;
}
.cs-message-own .cs-message-avatar {
background: linear-gradient(135deg, #d9ac6b 0%, #a67c3d 100%);
color: #1f120e;
}
/* 消息气泡 */
.cs-message-bubble {
background: #2a1f18;
border-radius: 8px;
padding: 10px 12px;
position: relative;
}
.cs-message-own .cs-message-bubble {
background: linear-gradient(135deg, #d9ac6b 0%, #a67c3d 100%);
}
.cs-message-text {
color: #e0e0e0;
font-size: 14px;
line-height: 1.5;
word-break: break-word;
}
.cs-message-own .cs-message-text {
color: #1f120e;
}
.cs-message-time {
font-size: 11px;
color: #777;
margin-top: 5px;
text-align: right;
}
.cs-message-own .cs-message-time {
color: rgba(31, 18, 14, 0.6);
}
/* 输入区域 */
.cs-input-area {
border-top: 1px solid #2a1f18;
padding: 12px;
background: #1f120e;
}
.cs-input-area textarea {
width: 100%;
height: 65px;
background: #3d2c22;
border: 1px solid #553e31;
border-radius: 6px;
padding: 10px;
resize: none;
font-size: 14px;
color: #e0e0e0;
box-sizing: border-box;
}
.cs-input-area textarea::placeholder {
color: #777;
}
.cs-input-area textarea:focus {
outline: none;
border-color: #d9ac6b;
}
.cs-input-area textarea:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.cs-input-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 10px;
}
.cs-btn {
padding: 8px 18px;
border: none;
border-radius: 20px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: all 0.2s;
}
.cs-btn-send {
background: linear-gradient(180deg, #f7ee99 0%, #78681c 50%, #c0b141 100%);
color: #1f120e;
min-width: 80px;
}
.cs-btn-send:hover:not(:disabled) {
background: linear-gradient(180deg, #ffed48 0%, #78681c 50%, #ffed48 100%);
}
.cs-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>