|
|
@@ -664,6 +664,9 @@ export default {
|
|
|
// 转换为我们的消息格式
|
|
|
this.messages = messageList.map(msg => this.convertMessage(msg));
|
|
|
|
|
|
+ // 🔥 获取会话对象,用 peerReadTime 更新已读状态
|
|
|
+ await this.updateMessageReadStatusByConversation();
|
|
|
+
|
|
|
// 滚动到底部
|
|
|
this.$nextTick(() => {
|
|
|
this.scrollToBottom();
|
|
|
@@ -701,6 +704,9 @@ export default {
|
|
|
const newMessages = messageList.map(msg => this.convertMessage(msg));
|
|
|
this.messages = [...newMessages, ...this.messages];
|
|
|
|
|
|
+ // 🔥 更新已读状态
|
|
|
+ await this.updateMessageReadStatusByConversation();
|
|
|
+
|
|
|
} catch (error) {
|
|
|
console.error('❌ 加载更多消息失败:', error);
|
|
|
} finally {
|
|
|
@@ -858,6 +864,60 @@ export default {
|
|
|
isPeerRead: isPeerRead // 对方是否已读(只对自己发送的消息有意义)
|
|
|
};
|
|
|
},
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据会话的 peerReadTime 更新消息的已读状态
|
|
|
+ * 这是解决"退出重进后消息变未读"问题的关键方法
|
|
|
+ */
|
|
|
+ async updateMessageReadStatusByConversation() {
|
|
|
+ try {
|
|
|
+ console.log('🔄 开始根据会话 peerReadTime 更新已读状态...');
|
|
|
+
|
|
|
+ // 获取当前会话对象
|
|
|
+ const conversationRes = await timManager.tim.getConversationProfile(this.conversationID);
|
|
|
+
|
|
|
+ if (!conversationRes || !conversationRes.data || !conversationRes.data.conversation) {
|
|
|
+ console.warn('⚠️ 无法获取会话对象');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const conversation = conversationRes.data.conversation;
|
|
|
+ const peerReadTime = conversation.peerReadTime; // 对方最后阅读时间(秒级时间戳)
|
|
|
+
|
|
|
+ console.log(' 会话ID:', this.conversationID);
|
|
|
+ console.log(' 对方最后阅读时间:', peerReadTime, new Date(peerReadTime * 1000).toLocaleString());
|
|
|
+
|
|
|
+ if (!peerReadTime || peerReadTime === 0) {
|
|
|
+ console.log(' 对方尚未阅读任何消息');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新消息列表中的已读状态
|
|
|
+ let updatedCount = 0;
|
|
|
+ this.messages.forEach((msg, index) => {
|
|
|
+ // 只处理自己发送的消息
|
|
|
+ if (msg.fromUserId === this.userId) {
|
|
|
+ // 如果消息的发送时间 <= 对方最后阅读时间,说明对方已读
|
|
|
+ const msgTime = Math.floor(msg.sendTime.getTime() / 1000); // 转换为秒级时间戳
|
|
|
+
|
|
|
+ if (msgTime <= peerReadTime) {
|
|
|
+ // 只更新未标记为已读的消息
|
|
|
+ if (!msg.isPeerRead) {
|
|
|
+ this.$set(this.messages[index], 'isPeerRead', true);
|
|
|
+ updatedCount++;
|
|
|
+ console.log(` ✅ 消息 ${msg.messageId} 标记为已读 (发送时间: ${new Date(msgTime * 1000).toLocaleString()})`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ console.log(`✅ 根据 peerReadTime 更新了 ${updatedCount} 条消息为已读状态`);
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 更新消息已读状态失败:', error);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
/**
|
|
|
* 检查对方是否拉黑自己
|
|
|
*/
|
|
|
@@ -914,35 +974,55 @@ export default {
|
|
|
console.log('✅ TIM 对象已就绪,准备监听 MESSAGE_READ_BY_PEER 事件');
|
|
|
|
|
|
// 监听消息已读回执事件
|
|
|
- const handleMessageReadByPeer = (event) => {
|
|
|
+ const handleMessageReadByPeer = async (event) => {
|
|
|
console.log('=== 📖 收到已读回执事件 ===');
|
|
|
console.log(' - 完整事件对象:', event);
|
|
|
console.log(' - 事件数据:', JSON.stringify(event.data));
|
|
|
|
|
|
// event.data 包含已读的消息列表
|
|
|
if (event.data && Array.isArray(event.data)) {
|
|
|
- event.data.forEach(item => {
|
|
|
+ for (const item of event.data) {
|
|
|
console.log(' - 处理会话:', item.conversationID, '当前会话:', this.conversationID);
|
|
|
|
|
|
// 只处理当前会话的消息
|
|
|
if (item.conversationID === this.conversationID) {
|
|
|
console.log('✅ 对方已阅读当前会话的消息');
|
|
|
|
|
|
- let updatedCount = 0;
|
|
|
-
|
|
|
- // 更新本地消息列表中所有未读消息的状态
|
|
|
- this.messages.forEach((msg, index) => {
|
|
|
- // 只更新自己发送的且未被标记为已读的消息
|
|
|
- if (msg.fromUserId === this.userId && !msg.isPeerRead && msg.sendStatus !== 4) {
|
|
|
- this.$set(this.messages[index], 'isPeerRead', true);
|
|
|
- updatedCount++;
|
|
|
- console.log(` - 消息 ${msg.messageId} 已标记为已读`);
|
|
|
+ // 🔥 关键修复:使用 peerReadTime 精确判断哪些消息被读了
|
|
|
+ try {
|
|
|
+ const conversationRes = await timManager.tim.getConversationProfile(this.conversationID);
|
|
|
+
|
|
|
+ if (conversationRes && conversationRes.data && conversationRes.data.conversation) {
|
|
|
+ const peerReadTime = conversationRes.data.conversation.peerReadTime;
|
|
|
+ console.log(' - 对方最后阅读时间:', peerReadTime, new Date(peerReadTime * 1000).toLocaleString());
|
|
|
+
|
|
|
+ if (peerReadTime && peerReadTime > 0) {
|
|
|
+ let updatedCount = 0;
|
|
|
+
|
|
|
+ // 只更新发送时间 <= peerReadTime 的消息
|
|
|
+ this.messages.forEach((msg, index) => {
|
|
|
+ if (msg.fromUserId === this.userId && !msg.isPeerRead && msg.sendStatus !== 4) {
|
|
|
+ const msgTime = Math.floor(msg.sendTime.getTime() / 1000);
|
|
|
+
|
|
|
+ // 只有消息发送时间 <= 对方阅读时间,才标记为已读
|
|
|
+ if (msgTime <= peerReadTime) {
|
|
|
+ this.$set(this.messages[index], 'isPeerRead', true);
|
|
|
+ updatedCount++;
|
|
|
+ console.log(` - 消息 ${msg.messageId} 已标记为已读 (发送时间: ${new Date(msgTime * 1000).toLocaleString()})`);
|
|
|
+ } else {
|
|
|
+ console.log(` - 消息 ${msg.messageId} 保持未读 (发送时间: ${new Date(msgTime * 1000).toLocaleString()} > 阅读时间)`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ console.log(`✅ 共更新 ${updatedCount} 条消息为已读状态`);
|
|
|
+ }
|
|
|
}
|
|
|
- });
|
|
|
-
|
|
|
- console.log(`✅ 共更新 ${updatedCount} 条消息为已读状态`);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 获取会话 peerReadTime 失败:', error);
|
|
|
+ }
|
|
|
}
|
|
|
- });
|
|
|
+ }
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -1238,6 +1318,17 @@ export default {
|
|
|
return '[视频]';
|
|
|
case 'TIMFileElem':
|
|
|
return '[文件]';
|
|
|
+ case 'TIMCustomElem':
|
|
|
+ // 处理自定义消息(我们的语音消息)
|
|
|
+ try {
|
|
|
+ const customData = JSON.parse(timMessage.payload.data);
|
|
|
+ if (customData.type === 'voice') {
|
|
|
+ return '[语音]';
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('解析自定义消息内容失败:', e);
|
|
|
+ }
|
|
|
+ return '[未知消息]';
|
|
|
default:
|
|
|
return '[未知消息]';
|
|
|
}
|
|
|
@@ -1552,141 +1643,122 @@ export default {
|
|
|
this.voiceTouchStartY = e.touches[0].clientY;
|
|
|
this.voiceCanceling = false;
|
|
|
|
|
|
- // 检查消息发送限制
|
|
|
- if (this.hasMessageLimit && this.remainingCount <= 0) {
|
|
|
+ // 检查是否被拉黑
|
|
|
+ if (this.isBlockedByTarget) {
|
|
|
uni.showToast({
|
|
|
- title: '今日消息发送次数已用完',
|
|
|
+ title: '对方已将你拉黑',
|
|
|
icon: 'none'
|
|
|
});
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 初始化录音管理器
|
|
|
- if (!this.recorderManager) {
|
|
|
- console.log('📱 初始化录音管理器');
|
|
|
- this.recorderManager = uni.getRecorderManager();
|
|
|
-
|
|
|
- // 录音开始
|
|
|
- this.recorderManager.onStart(() => {
|
|
|
- console.log('✅ 录音开始回调触发');
|
|
|
- this.isRecording = true;
|
|
|
- this.showVoiceRecording = true;
|
|
|
- this.voiceStartTime = Date.now();
|
|
|
- this.voiceRecordingTime = 0;
|
|
|
- this.voiceVolume = 0.3;
|
|
|
- console.log(' - isRecording:', this.isRecording);
|
|
|
- console.log(' - showVoiceRecording:', this.showVoiceRecording);
|
|
|
-
|
|
|
- // 启动计时器
|
|
|
- let volumeDirection = 1;
|
|
|
- let currentVolume = 0.3;
|
|
|
- this.voiceRecordingTimer = setInterval(() => {
|
|
|
- this.voiceRecordingTime = Math.floor((Date.now() - this.voiceStartTime) / 1000);
|
|
|
-
|
|
|
- // 检查是否达到60秒,自动停止录音
|
|
|
- if (this.voiceRecordingTime >= 60) {
|
|
|
- console.log('⏰ 录音时长达到60秒,自动停止');
|
|
|
- clearInterval(this.voiceRecordingTimer);
|
|
|
- this.voiceRecordingTimer = null;
|
|
|
- this.voiceCanceling = false; // 确保不是取消状态
|
|
|
- if (this.recorderManager) {
|
|
|
- this.recorderManager.stop();
|
|
|
- }
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 模拟更真实的音量波动
|
|
|
- if (Math.random() > 0.2) {
|
|
|
- currentVolume += volumeDirection * (0.1 + Math.random() * 0.2);
|
|
|
- if (currentVolume > 1.0) {
|
|
|
- currentVolume = 1.0;
|
|
|
- volumeDirection = -1;
|
|
|
- } else if (currentVolume < 0.4) {
|
|
|
- currentVolume = 0.4;
|
|
|
- volumeDirection = 1;
|
|
|
- }
|
|
|
- this.voiceVolume = currentVolume;
|
|
|
- } else {
|
|
|
- this.voiceVolume = Math.max(0.2, this.voiceVolume * 0.8);
|
|
|
- }
|
|
|
- }, 100);
|
|
|
- });
|
|
|
+ // 🔥 每次都重新获取录音管理器(确保状态干净)
|
|
|
+ console.log('📱 获取录音管理器');
|
|
|
+ this.recorderManager = uni.getRecorderManager();
|
|
|
+
|
|
|
+ // 🔥 每次录音前都重新注册回调(确保回调有效)
|
|
|
+ console.log('📝 注册录音回调');
|
|
|
+
|
|
|
+ // 录音开始回调(可能延迟或不触发,所以状态已在 start() 后立即设置)
|
|
|
+ this.recorderManager.onStart(() => {
|
|
|
+ console.log('✅ 录音开始回调触发(延迟触发)');
|
|
|
+ });
|
|
|
+
|
|
|
+ // 录音结束回调
|
|
|
+ this.recorderManager.onStop((res) => {
|
|
|
+ console.log('🎤 录音结束:', res, ', voiceCanceling:', this.voiceCanceling);
|
|
|
+ this.isRecording = false;
|
|
|
+ this.showVoiceRecording = false;
|
|
|
|
|
|
- // 录音结束
|
|
|
- this.recorderManager.onStop((res) => {
|
|
|
- console.log('🎤 录音结束:', res, ', voiceCanceling:', this.voiceCanceling);
|
|
|
- this.isRecording = false;
|
|
|
- this.showVoiceRecording = false;
|
|
|
-
|
|
|
- // 清除计时器
|
|
|
- if (this.voiceRecordingTimer) {
|
|
|
- clearInterval(this.voiceRecordingTimer);
|
|
|
- this.voiceRecordingTimer = null;
|
|
|
- }
|
|
|
-
|
|
|
- // 如果是取消状态,不发送
|
|
|
- if (this.voiceCanceling) {
|
|
|
- console.log('❌ 录音已取消,不发送');
|
|
|
- this.voiceCanceling = false;
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- this.voiceTempPath = res.tempFilePath;
|
|
|
- this.voiceDuration = Math.floor(res.duration / 1000); // 转换为秒
|
|
|
-
|
|
|
- console.log(' - 文件路径:', this.voiceTempPath);
|
|
|
- console.log(' - 时长:', this.voiceDuration, '秒');
|
|
|
-
|
|
|
- // 时长验证
|
|
|
- if (this.voiceDuration < 1) {
|
|
|
- uni.showToast({
|
|
|
- title: '录音时间太短',
|
|
|
- icon: 'none'
|
|
|
- });
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (this.voiceDuration > 60) {
|
|
|
- uni.showToast({
|
|
|
- title: '录音时间不能超过60秒',
|
|
|
- icon: 'none'
|
|
|
- });
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 发送语音消息
|
|
|
- this.sendVoiceMessage();
|
|
|
- });
|
|
|
+ // 清除计时器
|
|
|
+ if (this.voiceRecordingTimer) {
|
|
|
+ clearInterval(this.voiceRecordingTimer);
|
|
|
+ this.voiceRecordingTimer = null;
|
|
|
+ }
|
|
|
|
|
|
- // 录音错误
|
|
|
- this.recorderManager.onError((err) => {
|
|
|
- console.error('❌ 录音错误:', err);
|
|
|
-
|
|
|
- // 立即清理录音状态和界面
|
|
|
- this.isRecording = false;
|
|
|
- this.showVoiceRecording = false;
|
|
|
- this.voiceCanceling = true;
|
|
|
-
|
|
|
- // 清除计时器
|
|
|
- if (this.voiceRecordingTimer) {
|
|
|
- clearInterval(this.voiceRecordingTimer);
|
|
|
- this.voiceRecordingTimer = null;
|
|
|
- }
|
|
|
-
|
|
|
- // 如果是权限错误,不显示任何提示(用户会看到系统权限弹窗)
|
|
|
- // 其他错误才显示提示
|
|
|
- if (err.errCode !== 'authorize' && err.errMsg && !err.errMsg.includes('authorize')) {
|
|
|
- uni.showToast({
|
|
|
- title: '录音失败',
|
|
|
- icon: 'none'
|
|
|
+ // 如果是取消状态,不发送
|
|
|
+ if (this.voiceCanceling) {
|
|
|
+ console.log('❌ 录音已取消,不发送');
|
|
|
+ this.voiceCanceling = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.voiceTempPath = res.tempFilePath;
|
|
|
+ this.voiceDuration = Math.floor(res.duration / 1000); // 转换为秒
|
|
|
+
|
|
|
+ console.log(' - 文件路径:', this.voiceTempPath);
|
|
|
+ console.log(' - 时长:', this.voiceDuration, '秒');
|
|
|
+
|
|
|
+ // 时长验证
|
|
|
+ if (this.voiceDuration < 1) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '录音时间太短',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.voiceDuration > 60) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '录音时间不能超过60秒',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 发送语音消息
|
|
|
+ this.sendVoiceMessage();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 录音错误回调
|
|
|
+ this.recorderManager.onError((err) => {
|
|
|
+ console.error('❌ 录音错误:', err);
|
|
|
+
|
|
|
+ // 立即清理录音状态和界面
|
|
|
+ this.isRecording = false;
|
|
|
+ this.showVoiceRecording = false;
|
|
|
+ this.voiceCanceling = true;
|
|
|
+
|
|
|
+ // 清除计时器
|
|
|
+ if (this.voiceRecordingTimer) {
|
|
|
+ clearInterval(this.voiceRecordingTimer);
|
|
|
+ this.voiceRecordingTimer = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果是权限错误,不显示任何提示(用户会看到系统权限弹窗)
|
|
|
+ // 其他错误才显示提示
|
|
|
+ if (err.errCode !== 'authorize' && err.errMsg && !err.errMsg.includes('authorize')) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '录音失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('✅ 已清理录音状态,隐藏录音界面');
|
|
|
+ });
|
|
|
+
|
|
|
+ // 🔥 先检查录音权限
|
|
|
+ console.log('🔐 检查录音权限...');
|
|
|
+ uni.getSetting({
|
|
|
+ success: (res) => {
|
|
|
+ console.log('📋 权限设置:', res.authSetting);
|
|
|
+ if (res.authSetting['scope.record'] === false) {
|
|
|
+ console.warn('⚠️ 录音权限被拒绝');
|
|
|
+ uni.showModal({
|
|
|
+ title: '需要录音权限',
|
|
|
+ content: '请在设置中开启录音权限',
|
|
|
+ success: (modalRes) => {
|
|
|
+ if (modalRes.confirm) {
|
|
|
+ uni.openSetting();
|
|
|
+ }
|
|
|
+ }
|
|
|
});
|
|
|
+ return;
|
|
|
}
|
|
|
-
|
|
|
- console.log('✅ 已清理录音状态,隐藏录音界面');
|
|
|
- });
|
|
|
- }
|
|
|
+ }
|
|
|
+ });
|
|
|
|
|
|
- // 开始录音(等待 onStart 回调后才显示界面)
|
|
|
+ // 开始录音
|
|
|
console.log('🎙️ 调用 recorderManager.start()');
|
|
|
try {
|
|
|
this.recorderManager.start({
|
|
|
@@ -1696,6 +1768,52 @@ export default {
|
|
|
encodeBitRate: 48000
|
|
|
});
|
|
|
console.log('✅ recorderManager.start() 调用成功');
|
|
|
+
|
|
|
+ // 🔥 立即显示录音界面(不等待 onStart 回调)
|
|
|
+ // 因为微信小程序的 onStart 可能延迟或不触发
|
|
|
+ this.isRecording = true;
|
|
|
+ this.showVoiceRecording = true;
|
|
|
+ this.voiceStartTime = Date.now();
|
|
|
+ this.voiceRecordingTime = 0;
|
|
|
+ this.voiceVolume = 0.3;
|
|
|
+ console.log('🎬 立即显示录音界面');
|
|
|
+ console.log(' - isRecording:', this.isRecording);
|
|
|
+ console.log(' - showVoiceRecording:', this.showVoiceRecording);
|
|
|
+
|
|
|
+ // 启动计时器
|
|
|
+ let volumeDirection = 1;
|
|
|
+ let currentVolume = 0.3;
|
|
|
+ this.voiceRecordingTimer = setInterval(() => {
|
|
|
+ this.voiceRecordingTime = Math.floor((Date.now() - this.voiceStartTime) / 1000);
|
|
|
+
|
|
|
+ // 检查是否达到60秒,自动停止录音
|
|
|
+ if (this.voiceRecordingTime >= 60) {
|
|
|
+ console.log('⏰ 录音时长达到60秒,自动停止');
|
|
|
+ clearInterval(this.voiceRecordingTimer);
|
|
|
+ this.voiceRecordingTimer = null;
|
|
|
+ this.voiceCanceling = false;
|
|
|
+ if (this.recorderManager) {
|
|
|
+ this.recorderManager.stop();
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 模拟音量波动
|
|
|
+ if (Math.random() > 0.2) {
|
|
|
+ currentVolume += volumeDirection * (0.1 + Math.random() * 0.2);
|
|
|
+ if (currentVolume > 1.0) {
|
|
|
+ currentVolume = 1.0;
|
|
|
+ volumeDirection = -1;
|
|
|
+ } else if (currentVolume < 0.4) {
|
|
|
+ currentVolume = 0.4;
|
|
|
+ volumeDirection = 1;
|
|
|
+ }
|
|
|
+ this.voiceVolume = currentVolume;
|
|
|
+ } else {
|
|
|
+ this.voiceVolume = Math.max(0.2, this.voiceVolume * 0.8);
|
|
|
+ }
|
|
|
+ }, 100);
|
|
|
+
|
|
|
} catch (err) {
|
|
|
console.error('❌ recorderManager.start() 调用失败:', err);
|
|
|
this.isRecording = false;
|