2025-11-14 17:49:30 +08:00

594 lines
20 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 class="chat-messages" scroll-y="true"
upper-threshold="50" @scrolltoupper="loadMoreHistory" @scroll="onScroll">
<!-- 加载历史消息提示 -->
<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">{{ message.times }}</view>
<!-- 消息项 -->
<view :id="'msg-' + index" :class="{
'self': message.isSelf,
'other': !message.isSelf,
'loading': message.isLoading
}" class="message-item">
<image :src="message.isSelf ? userAvatar : getAvatarUrl(message)" 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" auto-height class="message-input" cursor-spacing="10"
enable-keyboard-accessory-view="true" hold-keyboard="true" maxlength="500" placeholder="请输入消息..."
@blur="onInputBlur" @confirm="sendMessage" @focus="onInputFocus" @input="handleInput"></textarea>
<button :disabled="inputMessage.trim() === ''" class="send-btn" @tap="sendMessage">
发送
</button>
</view>
</view>
</view>
</template>
<script>
import { menuButtonInfo, picUrl, request } from '@/utils'
import mqttTool from '@/utils/mqtt'
import { apiArr } from '../../../api/customerService'
export default {
data() {
return {
localHeight: '',
top: '',
// 聊天目标信息
chatTarget: {
mchId: '',
bindId: 0,
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,
// 重连失败提示定时器
reconnectFailedTimer: null,
// 分页参数
pageNum: 1,
pageSize: 10,
// 是否还有更多历史消息
hasMoreHistory: true,
// 滚动到底部的标记
scrollToBottomFlag: false
}
},
onLoad(options) {
console.log('客服聊天页面onLoad触发')
const meun = menuButtonInfo()
this.top = meun.top
this.localHeight = meun.height
console.log('导航栏信息top:', this.top, 'height:', this.localHeight)
// 获取聊天对象信息
if (options.item) {
const item = JSON.parse(options.item)
console.log('参数接收:', item)
if (Number(item.type) === 1) {
// 客户选择客服跳转进来
this.chatTarget = item
console.log('客户找客服跳转:', this.chatTarget)
this.chatTarget.title = this.chatTarget.employee_name
this.getMqttConfig().then(() => {
// 获取配置后再初始化聊天
console.log('获取MQTT配置成功开始初始化聊天')
this.initChat()
}).catch(error => {
console.error('获取MQTT配置失败:', error)
})
} else {
// 客服选择聊天列表进来
this.chatTarget = item
console.log('客服找客户进来:', this.chatTarget)
this.chatTarget.title = this.chatTarget.server_name
this.chatTarget.bindId = this.chatTarget.id
if (this.chatTarget.client_id_one === this.selfClientId) {
this.chatTarget.openId = this.chatTarget.client_id_two
} else {
this.chatTarget.openId = this.chatTarget.client_id_one
}
// 初始化MQTT连接
console.log('开始初始化聊天')
this.initChat()
}
} else {
console.log('没有接收到参数item')
}
// 初始化用户头像
this.userAvatar = picUrl + uni.getStorageSync('headPhoto')
console.log('用户头像:', this.userAvatar)
},
onShow() {
},
methods: {
getAvatarUrl(record) {
return this.chatTarget.employee_image ? this.chatTarget.employee_image : 'https://wechat-img-file.oss-cn-beijing.aliyuncs.com/property-img-file/defaultTx.png'
},
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) => {
const msgStr = Buffer.from(message).toString('utf8') // 二进制转UTF-8字符串
const msg = JSON.parse(msgStr) // 后续解析逻辑不变
let jsMsg = msg // 直接使用已解析的对象,无需再次解析
console.log('收到消息', topic, msg)
if (jsMsg.send_client === this.selfClientId || jsMsg.receive_client === this.selfClientId) {
console.log('接收或发送人是我')
if (jsMsg.send_client === this.chatTarget.openId || jsMsg.receive_client === this.chatTarget.openId) {
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()
// 连接成功后立即加载历史消息
console.log('连接成功,开始加载历史消息')
await this.loadHistoryMessages()
console.log('历史消息加载完成,消息数量:', this.messages.length)
} catch (error) {
console.error('初始化聊天失败', error)
this.connectingStatus = '连接失败,请检查网络'
// 失败后尝试重新连接
this.reconnectFailedTimer = setTimeout(() => {
this.initChat()
}, 3000)
}
},
// 获取MQTT连接配置
async getMqttConfig() {
console.log('🚀 ~ onLoad ~ this.chatTarget.open_id:', this.chatTarget.open_id)
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) {
if (res.client_bind.client_id_one === this.selfClientId) {
this.chatTarget.openId = res.client_bind.client_id_two
} else {
this.chatTarget.openId = res.client_bind.client_id_one
}
this.chatTarget.bindId = 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() {
console.log('loadHistoryMessages方法调用')
console.log('加载条件检查hasMoreHistory:', this.hasMoreHistory, 'isLoadingHistory:', this.isLoadingHistory)
if (!this.hasMoreHistory || this.isLoadingHistory) {
console.log('不满足加载条件hasMoreHistory:', this.hasMoreHistory, 'isLoadingHistory:', this.isLoadingHistory)
return
}
try {
this.isLoadingHistory = true
console.log('开始加载历史消息,当前页码:', this.pageNum)
// 确保已经获取了mqttConfig.bindId
if (!this.chatTarget.bindId) {
console.log('没有bindId开始获取MQTT配置')
await this.getMqttConfig()
console.log('获取MQTT配置成功bindId', this.chatTarget.bindId)
}
const params = {
bindId: this.chatTarget.bindId,
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.msg_record) {
const historyMessages = res.msg_record
console.log('原始历史消息数量:', historyMessages.length)
// 如果没有更多历史消息了
if (historyMessages.length === 0) {
console.log('没有更多历史消息了')
this.hasMoreHistory = false
return
}
// 处理历史消息,转换为需要的格式
const formattedMessages = historyMessages.map(msg => ({
content: msg.content,
time: new Date(msg.create_time).getTime(),
times: msg.update_time,
isSelf: msg.send_client === this.selfClientId, // 修正判断条件
isLoading: false
})).reverse(); // 反转消息顺序,确保最早的消息在最前面
console.log('格式化后的历史消息:', formattedMessages)
// 将格式化后的历史消息添加到消息列表开头
const previousMessageCount = this.messages.length;
console.log('添加前消息数量:', previousMessageCount)
this.messages = [...formattedMessages, ...this.messages];
console.log('添加后消息数量:', this.messages.length)
// 增加页码
this.pageNum++;
console.log('下一页页码:', this.pageNum)
// 如果是首次加载,滚动到底部显示最新消息
if (previousMessageCount === 0) {
setTimeout(() => {
console.log('首次加载,滚动到底部,消息数量:', this.messages.length)
// 使用更可靠的滚动方式
uni.pageScrollTo({
scrollTop: 999999,
duration: 300
});
}, 100);
} else {
// 不是首次加载时,保持当前滚动位置,不自动滚动到底部
// 确保新加载的历史消息在顶部可见
console.log('非首次加载,新增消息数量:', formattedMessages.length)
}
} else {
console.log('接口返回数据格式不正确或无消息记录')
// 仅当不是第一页时才设置hasMoreHistory为false
if (this.pageNum > 1) {
this.hasMoreHistory = false
}
}
} catch (error) {
console.error('加载历史消息失败', error)
} finally {
this.isLoadingHistory = false
console.log('加载历史消息结束')
}
},
// 滚动事件监听
onScroll(e) {
console.log('滚动事件触发scrollTop', e.detail.scrollTop)
},
// 加载更多历史消息
loadMoreHistory() {
console.log('滚动到顶部事件触发')
// 滚动到顶部时加载更多历史消息
console.log('当前条件isLoadingHistory:', this.isLoadingHistory, 'hasMoreHistory:', this.hasMoreHistory, 'pageNum:', this.pageNum)
// 重置hasMoreHistory为true确保可以继续加载
if (!this.hasMoreHistory && this.pageNum === 1) {
console.log('重置hasMoreHistory为true')
this.hasMoreHistory = true
}
if (!this.isLoadingHistory && this.hasMoreHistory) {
console.log('开始加载更多历史消息')
this.loadHistoryMessages()
} else {
console.log('不满足加载更多条件isLoadingHistory:', this.isLoadingHistory, 'hasMoreHistory:', this.hasMoreHistory)
}
},
// 发送消息
sendMessage() {
const content = this.inputMessage.trim()
console.log('发送消息', content)
if (!content || !this.client || !this.isConnected) return
// 滚动到底部
this.scrollToBottom()
console.log('需要发送的对象', this.chatTarget)
// 按照用户提供的格式构建发送消息
const msgData = {
bind_id: this.chatTarget.bindId, // 聊天列表的ID
send_client: this.selfClientId, // 消息发送方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(() => {
console.log('手动滚动到底部')
uni.pageScrollTo({
scrollTop: 999999,
duration: 300
});
}, 100)
},
// 返回上一页
goBack() {
uni.navigateBack()
},
// 启动心跳包 - 增强版:添加错误处理和状态检查
startKeepalive() {
// 停止之前的定时器
this.stopKeepalive()
// 每30秒发送一次心跳包
this.keepaliveTimer = setInterval(() => {
if (this.client && this.isConnected) {
const keepaliveData = {
client_id: this.selfClientId // 自己的client_id
}
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.end()
}
// 停止心跳包
this.stopKeepalive()
// 清除重连失败提示定时器
if (this.reconnectFailedTimer) {
clearTimeout(this.reconnectFailedTimer)
this.reconnectFailedTimer = null
}
}
}
</script>
<style>
@import url("./index.css");
</style>
<style scoped>
:root {
--header-height: 80px; /* 头部高度 */
--input-height: 80px; /* 输入区域高度 */
}
</style>