refactor(customer-service): 优化客服模块交互,点击直接弹出聊天框
- 修改 header.vue 和 play.vue 客服按钮点击事件,跳过中间选择弹窗 - 重写 CustomerServiceWindow.vue 样式,匹配 PC 端深色主题风格 - 移除图片发送功能,仅保留文本消息 - 新增遮罩层、头像区分、金色渐变按钮等 UI 元素
This commit is contained in:
parent
b7061d7a0d
commit
8d9154c668
@ -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});
|
||||
|
||||
@ -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});
|
||||
},
|
||||
|
||||
547
src/components/updateService/CustomerServiceWindow.vue
Normal file
547
src/components/updateService/CustomerServiceWindow.vue
Normal 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
|
||||
// 聊天API在Pro后端,使用相对路径或配置的聊天API地址
|
||||
// 由于跨域问题,暂时跳过历史消息加载,消息会通过WebSocket实时接收
|
||||
console.log('[CustomerService] 跳过历史消息加载(跨域限制)')
|
||||
// TODO: 后续可以通过WebSocket请求历史消息,或配置正确的API地址
|
||||
},
|
||||
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>
|
||||
Loading…
Reference in New Issue
Block a user