2025-09-24 17:24:22 +08:00

488 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="chat-container">
<!-- 聊天头部 -->
<view :style="{ paddingTop: top + 'px', height: localHeight + 'px' }" class="chat-header">
<view class="back-btn" @tap="goBack">
<uni-icons color="#333" size="28" type="left"></uni-icons>
</view>
<view class="chat-title">{{ chatTarget.title || '客服' }}</view>
<view class="change-service-btn" @tap="goToChangeService">
<uni-icons color="#333" size="22" type="switch"></uni-icons>
</view>
</view>
<!-- 连接状态提示 -->
<view v-if="connectingStatus" class="connecting-status">{{ connectingStatus }}</view>
<!-- 聊天消息区域 -->
<scroll-view :scroll-into-view="scrollToView" class="chat-messages" scroll-y="true"
@scrolltoupper="loadMoreHistory" @scrolltolower="loadMoreHistory" lower-threshold="100" upper-threshold="100">
<!-- 加载历史消息提示 -->
<view v-if="isLoadingHistory" class="message-time">加载历史消息...</view>
<!-- 消息列表 -->
<block v-for="(message, index) in messages" :key="index">
<!-- 时间分割线 -->
<view v-if="needShowTime(index)" class="message-time">{{ formatTime(message.time) }}</view>
<!-- 消息项 -->
<view :id="'msg-' + index" :class="{
'self': message.isSelf,
'other': !message.isSelf,
'loading': message.isLoading
}" class="message-item">
<image :src="message.isSelf ? userAvatar : (chatTarget.employee_image)" class="message-avatar"
mode="aspectFill"></image>
<view class="message-content">
{{ message.content }}
</view>
</view>
</block>
</scroll-view>
<!-- 输入区域 -->
<view class="chat-input-area">
<view class="input-container">
<textarea v-model="inputMessage" :adjust-position="true" class="message-input" placeholder="请输入消息..."
@confirm="sendMessage" @input="handleInput" auto-height hold-keyboard="true"
enable-keyboard-accessory-view="true" cursor-spacing="10" maxlength="500"
@focus="onInputFocus" @blur="onInputBlur"></textarea>
<button :disabled="!canSend || !client || !isConnected" class="send-btn" @tap="sendMessage">
发送
</button>
</view>
</view>
</view>
</template>
<script>
import { picUrl, menuButtonInfo, request, NavgateTo } from "../../../utils";
import { apiArr } from '@/api/customerService'
import mqttTool from '@/utils/mqtt'
export default {
data(){
return {
localHeight: '',
top: '',
// 聊天目标信息
chatTarget: {
mchId: '',
title: '',
avatar: '',
openId: '' // 接收方的open_id
},
// 用户头像
userAvatar: '',
// 消息列表
messages: [],
// 输入的消息
inputMessage: '',
// 是否可以发送消息
canSend: false,
// 连接状态
isConnected: false,
// 连接状态文本
connectingStatus: '',
// 滚动到指定视图
scrollToView: '',
// 是否加载历史消息
isLoadingHistory: false,
// 心跳包定时器
keepaliveTimer: null,
selfClientId: uni.getStorageSync('openId'),
// MQTT工具实例
client: null,
mqttConfig: {},
// 重连失败提示定时器
reconnectFailedTimer: null,
// 分页参数
pageNum: 1,
pageSize: 10,
// 是否还有更多历史消息
hasMoreHistory: true,
// 滚动到底部的标记
scrollToBottomFlag: false
}
},
onLoad(options){
const meun = menuButtonInfo()
this.top = meun.top
this.localHeight = meun.height
// 获取聊天对象信息
if (options.item) {
this.chatTarget = JSON.parse(options.item)
this.chatTarget.title = `客服${ this.chatTarget.employee_name }`
}
// 初始化MQTT连接
this.initChat()
this.getMqttConfig()
// 初始化用户头像
this.userAvatar = picUrl + uni.getStorageSync('headPhoto')
},
onShow(){
},
methods: {
async connect(){
this.client = null
const options = {
clientId: this.selfClientId
}
// 添加连接状态回调
const callbacks = {
onConnect: () => {
console.log('客服连接成功')
this.isConnected = true
this.connectingStatus = ''
},
onDisconnect: this.onDisconnect.bind(this),
onError: (error) => {
console.error('客服连接错误:', error)
this.isConnected = false
this.connectingStatus = '连接错误,请重试'
},
onReconnect: () => {
console.log('客服正在重连...')
this.isConnected = false
this.connectingStatus = '连接已断开,正在重连...'
}
}
this.client = mqttTool.connect(options, callbacks)
this.isConnected = !!this.client
await this.subscribe()
this.client.on('message', (topic, message) => {
let de = new TextDecoder('utf-8')
let msg = de.decode(message)
let jsMsg = JSON.parse(msg)
console.log('收到消息', topic, msg)
if (jsMsg.send_client === this.selfClientId || jsMsg.receive_client === this.selfClientId) {
console.log('接收或发送人是我')
if (jsMsg.send_client === this.mqttConfig.clientId || jsMsg.receive_client === this.mqttConfig.clientId) {
console.log('接收或发送人是我的聊天对象')
this.messages.push({
content: jsMsg.content,
time: Date.now(),
isSelf: jsMsg.send_client === this.selfClientId,
isLoading: false
})
console.log('收到我的消息', this.messages)
this.scrollToView = 'msg-' + (this.messages.length - 1)
}
}
})
},
async subscribe(){
if (this.isConnected && this.client) {
this.client.subscribe('contact/message/receive_msg', { qos: 0 }, (err) => {
if (!err) {
console.log('订阅成功', 'contact/message/receive_msg', { qos: 0 })
this.connectingStatus = ''
} else {
console.log('订阅失败:', err)
this.connectingStatus = '订阅失败,请重试'
}
})
} else {
console.log('连接失败', this.isConnected, this.client)
this.connectingStatus = '连接失败,请重试'
}
},
// 初始化聊天
async initChat(){
try {
// 显示连接状态
this.connectingStatus = '正在连接客服...'
await this.connect()
// 连接成功后启动心跳包
this.startKeepalive()
// 连接成功后立即加载历史消息
this.loadHistoryMessages()
} catch (error) {
console.error('初始化聊天失败', error)
this.connectingStatus = '连接失败,请检查网络'
// 失败后尝试重新连接
this.reconnectFailedTimer = setTimeout(() => {
this.initChat()
}, 3000)
}
},
// 获取MQTT连接配置
async getMqttConfig(){
try {
// 如果没有已创建的实例或clientId则通过API获取
return new Promise((resolve, reject) => {
const params = {
worker_id: this.chatTarget.id || '',
open_id: this.selfClientId || ''
}
request(apiArr.csGetToClientId, 'POST', params).then((res) => {
console.log('聊天列表:', res)
// 检查响应数据格式是否正确
if (res && res.client_bind && res.client_bind.client_id_one && res.client_bind.client_id_two) {
this.mqttConfig.clientId = res.client_bind.client_id_one // 登录人的open_id
this.chatTarget.openId = res.client_bind.client_id_two // 接收方的open_id
this.mqttConfig.bind_id = res.client_bind.id
resolve()
} else {
console.error('MQTT配置响应格式不正确:', res)
reject(new Error('未获取到有效的MQTT配置'))
}
}).catch(error => {
console.error('获取MQTT配置失败', error)
reject(error)
})
})
} catch (error) {
console.error('获取MQTT配置失败', error)
throw error
}
},
// MQTT断开连接回调
onDisconnect(packet){
console.log('MQTT连接断开', packet)
this.isConnected = false
this.client = null
// 根据断开原因设置不同的连接状态文本
if (packet && packet.error) {
// 连接失败的情况
this.connectingStatus = '连接失败,请检查网络'
} else {
// 其他断开连接的情况
this.connectingStatus = '连接已断开,正在重连...'
}
// 停止心跳包
this.stopKeepalive()
},
// 加载历史消息
async loadHistoryMessages(){
if (!this.hasMoreHistory || this.isLoadingHistory) {
return
}
try {
this.isLoadingHistory = true
// 确保已经获取了mqttConfig.bind_id
if (!this.mqttConfig.bind_id) {
await this.getMqttConfig()
}
const params = {
bind_id: this.mqttConfig.bind_id,
order: 'desc', // 按时间降序排列
page_num: this.pageNum,
page_size: this.pageSize
}
console.log('请求历史消息参数:', params)
const res = await request(apiArr.csGetMsgRecord, 'POST', params)
console.log('历史消息返回结果:', res)
if (res && res.code === 1 && res.data && res.data.msg_record) {
const historyMessages = res.data.msg_record
// 如果没有更多历史消息了
if (historyMessages.length === 0) {
this.hasMoreHistory = false
return
}
// 将获取到的历史消息添加到消息列表开头
// 这里需要根据消息的发送方判断是否是自己发送的消息
const formattedMessages = historyMessages.map(msg => ({
content: msg.content,
time: new Date(msg.create_time).getTime(),
isSelf: msg.send_client === this.mqttConfig.clientId,
isLoading: false
})).reverse() // 因为是按时间降序获取的,所以需要反转
// 添加到消息列表开头
this.messages = [...formattedMessages, ...this.messages]
// 增加页码
this.pageNum++
}
} catch (error) {
console.error('加载历史消息失败', error)
} finally {
this.isLoadingHistory = false
}
},
// 加载更多历史消息
loadMoreHistory(){
if (!this.isLoadingHistory && this.hasMoreHistory) {
this.loadHistoryMessages()
}
},
// 发送消息
sendMessage(){
const content = this.inputMessage.trim()
console.log('发送消息', content)
if (!content || !this.client || !this.isConnected) return
// 滚动到底部
this.scrollToBottom()
console.log('需要发送的对象', this.mqttConfig)
// 按照用户提供的格式构建发送消息
const msgData = {
bind_id: this.mqttConfig.bind_id, // 聊天列表的ID这里暂时固定为1
send_client: this.mqttConfig.clientId, // 消息发送方open_id
receive_client: this.chatTarget.openId, // 消息接收方open_id
type: 1, // 消息类型1表示文字消息
content: content, // 消息内容
receive_read_status: 2 // 接收方阅读状态
}
console.log('发送消息', msgData)
this.client.publish(
'contact/message/send_msg', // 使用指定的发送消息主题
JSON.stringify(msgData),
{ Qos: 0 },
(err) => {
if (err) {
console.error('发送消息失败', err)
// 可以在这里添加消息发送失败的处理逻辑
} else {
console.log('发送消息成功')
}
}
)
// 清空输入框
this.inputMessage = ''
},
// 处理输入事件
handleInput(){
this.canSend = this.inputMessage.trim().length > 0
},
onInputFocus() {
// 输入框获取焦点时,设置滚动到底部的标记
this.scrollToBottomFlag = true;
setTimeout(() => {
if (this.scrollToBottomFlag) {
this.scrollToBottom();
}
}, 300);
},
onInputBlur() {
// 输入框失去焦点时,重置滚动标记
this.scrollToBottomFlag = false;
},
// 是否需要显示时间分割线
needShowTime(index){
if (index === 0) return true
const currentMsg = this.messages[index]
const prevMsg = this.messages[index - 1]
// 如果两条消息间隔超过5分钟则显示时间分割线
return (currentMsg.time - prevMsg.time) > 5 * 60 * 1000
},
// 格式化时间
formatTime(time){
const date = new Date(time)
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
return `${ hours }:${ minutes }`
},
// 滚动到底部
scrollToBottom(){
setTimeout(() => {
this.scrollToView = 'msg-' + (this.messages.length - 1)
}, 100)
},
// 返回上一页
goBack(){
uni.navigateBack()
},
// 启动心跳包 - 增强版:添加错误处理和状态检查
startKeepalive(){
// 停止之前的定时器
this.stopKeepalive()
// 每30秒发送一次心跳包
this.keepaliveTimer = setInterval(() => {
if (this.client && this.isConnected) {
const keepaliveData = {
client_id: this.mqttConfig.clientId
}
this.client.publish(
'contact/message/keep_time',
JSON.stringify(keepaliveData),
{},
(err) => {
if (err) {
console.error('发送心跳包失败', err)
// 心跳包发送失败可能表示连接有问题,可以考虑触发重连
if (!this.isConnected) {
return
}
console.log('心跳包发送失败,尝试检查连接状态')
// 这里可以添加额外的连接检查逻辑
}
}
)
} else {
console.warn('MQTT未连接停止心跳包')
this.stopKeepalive()
}
}, 30000)
},
// 停止心跳包
stopKeepalive(){
if (this.keepaliveTimer) {
clearInterval(this.keepaliveTimer)
this.keepaliveTimer = null
}
},
// 跳转到切换客服页面
goToChangeService(){
uni.navigateTo({
url: '/packages/customerService/changeService/index?currentMchId=' + this.chatTarget.mchId
})
}
},
// 页面卸载时停止心跳包
onUnload(){
// 断开MQTT连接
if (this.client) {
this.client.disconnect()
}
// 停止心跳包
this.stopKeepalive()
// 清除重连失败提示定时器
if (this.reconnectFailedTimer) {
clearTimeout(this.reconnectFailedTimer)
this.reconnectFailedTimer = null
}
}
}
</script>
<style>
@import url("./index.css");
</style>