|
|
@@ -124,7 +124,10 @@
|
|
|
@click.stop="toggleVoicePlay(msg)">
|
|
|
<text class="voice-duration">{{ msg.duration }}''</text>
|
|
|
<view class="voice-icon-wrapper" :class="{playing: playingVoiceId === msg.messageId}">
|
|
|
- <image class="voice-icon" src="/static/voice-icon.png" mode="aspectFit"></image>
|
|
|
+ <image
|
|
|
+ class="voice-icon"
|
|
|
+ :src="msg.fromUserId === userId ? 'http://115.190.125.125:9000/static-images/%E6%88%91%E6%96%B9%E8%AF%AD%E9%9F%B3%E6%B6%88%E6%81%AF' : 'http://115.190.125.125:9000/static-images/%E5%AF%B9%E6%96%B9%E8%AF%AD%E9%9F%B3%E6%B6%88%E6%81%AF'"
|
|
|
+ mode="aspectFit"></image>
|
|
|
</view>
|
|
|
<!-- 暂停后的继续播放按钮 -->
|
|
|
<view
|
|
|
@@ -264,6 +267,7 @@
|
|
|
<script>
|
|
|
import timManager from '@/utils/tim-manager.js';
|
|
|
import TIM from 'tim-wx-sdk';
|
|
|
+import presenceManager from '@/utils/presence-manager.js';
|
|
|
|
|
|
export default {
|
|
|
data() {
|
|
|
@@ -318,7 +322,9 @@ export default {
|
|
|
voiceVolume: 0.3, // 录音音量(0-1),控制波形高度
|
|
|
playingVoiceId: null, // 当前播放的语音消息ID
|
|
|
pausedVoiceId: null, // 当前暂停的语音消息ID
|
|
|
- currentAudioContext: null // 当前音频上下文
|
|
|
+ currentAudioContext: null, // 当前音频上下文
|
|
|
+ onlineStatusPollingTimer: null, // 在线状态轮询定时器
|
|
|
+ voiceRecordCancelled: false // 录音是否已被取消(防止权限允许后自动恢复)
|
|
|
};
|
|
|
},
|
|
|
|
|
|
@@ -414,6 +420,12 @@ export default {
|
|
|
|
|
|
// 监听新消息
|
|
|
this.listenMessages();
|
|
|
+
|
|
|
+ // 初始化在线状态监听
|
|
|
+ this.initPresence();
|
|
|
+
|
|
|
+ // 发送已读回执(标记对方发来的消息为已读)
|
|
|
+ this.sendReadReceipt();
|
|
|
},
|
|
|
|
|
|
onUnload() {
|
|
|
@@ -426,6 +438,9 @@ export default {
|
|
|
timManager.tim.off(TIM.EVENT.MESSAGE_STATUS_CHANGED, this.handleStatusChange);
|
|
|
}
|
|
|
*/
|
|
|
+
|
|
|
+ // 清理在线状态监听
|
|
|
+ this.cleanupPresence();
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
@@ -747,6 +762,64 @@ export default {
|
|
|
*/
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 发送已读回执
|
|
|
+ */
|
|
|
+ sendReadReceipt() {
|
|
|
+ try {
|
|
|
+ console.log('📨 发送已读回执给用户:', this.targetUserId);
|
|
|
+
|
|
|
+ // 通过WebSocket发送已读回执
|
|
|
+ const message = {
|
|
|
+ type: 'read', // 已读回执类型
|
|
|
+ fromUserId: parseInt(this.userId),
|
|
|
+ toUserId: parseInt(this.targetUserId),
|
|
|
+ timestamp: Date.now()
|
|
|
+ };
|
|
|
+
|
|
|
+ // 发送WebSocket消息
|
|
|
+ if (timManager.websocket && timManager.websocket.readyState === 1) {
|
|
|
+ timManager.websocket.send(JSON.stringify(message));
|
|
|
+ console.log('✅ 已读回执发送成功');
|
|
|
+ } else {
|
|
|
+ console.warn('⚠️ WebSocket未连接,无法发送已读回执');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 发送已读回执失败:', error);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 监听已读回执
|
|
|
+ */
|
|
|
+ listenReadReceipt() {
|
|
|
+ // 在WebSocket消息处理中添加已读回执的监听
|
|
|
+ // 这个方法会在initPresence中被调用
|
|
|
+ console.log('👂 开始监听已读回执');
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理已读回执
|
|
|
+ */
|
|
|
+ handleReadReceipt(data) {
|
|
|
+ try {
|
|
|
+ console.log('📬 收到已读回执:', data);
|
|
|
+
|
|
|
+ // 更新本地消息状态为已读
|
|
|
+ this.messages.forEach((msg, index) => {
|
|
|
+ // 只更新自己发送给对方的消息
|
|
|
+ if (msg.fromUserId === this.userId &&
|
|
|
+ msg.toUserId === this.targetUserId &&
|
|
|
+ msg.sendStatus === 2) { // 只更新已送达的消息
|
|
|
+ this.$set(this.messages[index], 'sendStatus', 3); // 更新为已读
|
|
|
+ console.log(`✅ 消息 ${msg.messageId} 状态更新为已读`);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 处理已读回执失败:', error);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
/**
|
|
|
* 转换消息格式
|
|
|
*/
|
|
|
@@ -1430,6 +1503,121 @@ export default {
|
|
|
this.inputType = this.inputType === 'text' ? 'voice' : 'text';
|
|
|
},
|
|
|
|
|
|
+ /**
|
|
|
+ * 初始化在线状态监听
|
|
|
+ */
|
|
|
+ async initPresence() {
|
|
|
+ console.log('🔌 初始化在线状态监听');
|
|
|
+ console.log(' - 当前用户ID:', this.userId);
|
|
|
+ console.log(' - 对方用户ID:', this.targetUserId);
|
|
|
+
|
|
|
+ // 1. 首先通过HTTP API查询对方的在线状态(立即显示)
|
|
|
+ try {
|
|
|
+ const isOnline = await presenceManager.queryOnlineStatus(this.targetUserId);
|
|
|
+ this.isTargetOnline = isOnline;
|
|
|
+ console.log(' - HTTP查询初始在线状态:', this.isTargetOnline);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ HTTP查询在线状态失败:', error);
|
|
|
+ this.isTargetOnline = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 连接WebSocket(如果未连接)
|
|
|
+ if (!presenceManager.getConnectionStatus()) {
|
|
|
+ presenceManager.connect(this.userId);
|
|
|
+
|
|
|
+ // 等待WebSocket连接建立
|
|
|
+ await this.waitForWebSocketConnection();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 监听对方用户的在线状态变化
|
|
|
+ this.handleStatusChange = (status) => {
|
|
|
+ console.log(`👤 对方用户 ${this.targetUserId} 状态变更: ${status}`);
|
|
|
+ this.isTargetOnline = (status === 'online');
|
|
|
+ console.log(' - 更新后的在线状态:', this.isTargetOnline);
|
|
|
+ };
|
|
|
+
|
|
|
+ presenceManager.onStatusChange(this.targetUserId, this.handleStatusChange);
|
|
|
+ console.log('✅ 已订阅对方用户状态');
|
|
|
+
|
|
|
+ // 4. 监听已读回执(通过WebSocket)
|
|
|
+ if (presenceManager.websocket) {
|
|
|
+ const originalOnMessage = presenceManager.websocket.onmessage;
|
|
|
+ presenceManager.websocket.onmessage = (event) => {
|
|
|
+ // 先调用原有的消息处理
|
|
|
+ if (originalOnMessage) {
|
|
|
+ originalOnMessage.call(presenceManager.websocket, event);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理已读回执
|
|
|
+ try {
|
|
|
+ const data = JSON.parse(event.data);
|
|
|
+ if (data.type === 'read' && data.fromUserId == this.targetUserId) {
|
|
|
+ this.handleReadReceipt(data);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 解析WebSocket消息失败:', error);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ console.log('✅ 已监听已读回执');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 定期轮询在线状态(作为补充,每30秒查询一次)
|
|
|
+ this.onlineStatusPollingTimer = setInterval(async () => {
|
|
|
+ try {
|
|
|
+ const isOnline = await presenceManager.queryOnlineStatus(this.targetUserId);
|
|
|
+ if (this.isTargetOnline !== isOnline) {
|
|
|
+ console.log(`🔄 轮询检测到状态变化: ${this.isTargetOnline} -> ${isOnline}`);
|
|
|
+ this.isTargetOnline = isOnline;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 轮询查询在线状态失败:', error);
|
|
|
+ }
|
|
|
+ }, 30000); // 30秒轮询一次
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 等待WebSocket连接建立
|
|
|
+ */
|
|
|
+ async waitForWebSocketConnection() {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ const maxWaitTime = 5000; // 最多等待5秒
|
|
|
+ const startTime = Date.now();
|
|
|
+
|
|
|
+ const checkConnection = setInterval(() => {
|
|
|
+ if (presenceManager.getConnectionStatus()) {
|
|
|
+ console.log('✅ WebSocket已连接');
|
|
|
+ clearInterval(checkConnection);
|
|
|
+ resolve();
|
|
|
+ } else if (Date.now() - startTime > maxWaitTime) {
|
|
|
+ console.log('⚠️ WebSocket连接超时,继续使用HTTP轮询');
|
|
|
+ clearInterval(checkConnection);
|
|
|
+ resolve();
|
|
|
+ }
|
|
|
+ }, 100);
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 清理在线状态监听
|
|
|
+ */
|
|
|
+ cleanupPresence() {
|
|
|
+ console.log('🔌 清理在线状态监听');
|
|
|
+
|
|
|
+ // 清除轮询定时器
|
|
|
+ if (this.onlineStatusPollingTimer) {
|
|
|
+ clearInterval(this.onlineStatusPollingTimer);
|
|
|
+ this.onlineStatusPollingTimer = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.handleStatusChange) {
|
|
|
+ presenceManager.offStatusChange(this.targetUserId, this.handleStatusChange);
|
|
|
+ this.handleStatusChange = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 注意:不要断开WebSocket连接,因为其他页面可能还在使用
|
|
|
+ // presenceManager.disconnect();
|
|
|
+ },
|
|
|
+
|
|
|
/**
|
|
|
* 开始录音
|
|
|
*/
|
|
|
@@ -1456,7 +1644,18 @@ export default {
|
|
|
|
|
|
// 录音开始
|
|
|
this.recorderManager.onStart(() => {
|
|
|
- console.log('✅ 录音开始回调触发');
|
|
|
+ console.log('✅ 录音开始回调触发, voiceRecordCancelled:', this.voiceRecordCancelled);
|
|
|
+
|
|
|
+ // 关键修复:如果录音已被取消,不再恢复录音状态
|
|
|
+ if (this.voiceRecordCancelled) {
|
|
|
+ console.log('⚠️ 录音已被取消,忽略onStart回调');
|
|
|
+ // 立即停止录音
|
|
|
+ if (this.recorderManager) {
|
|
|
+ this.recorderManager.stop();
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
this.isRecording = true;
|
|
|
this.showVoiceRecording = true;
|
|
|
this.voiceStartTime = Date.now();
|
|
|
@@ -1519,6 +1718,7 @@ export default {
|
|
|
// 立即设置录音状态(不等待onStart回调)
|
|
|
this.isRecording = true;
|
|
|
this.showVoiceRecording = true;
|
|
|
+ this.voiceRecordCancelled = false; // 重置取消标志
|
|
|
this.voiceStartTime = Date.now();
|
|
|
this.voiceRecordingTime = 0;
|
|
|
this.voiceVolume = 0.3;
|
|
|
@@ -1630,6 +1830,9 @@ export default {
|
|
|
cancelVoiceRecord() {
|
|
|
console.log('❌ 取消录音, voiceCanceling:', this.voiceCanceling);
|
|
|
|
|
|
+ // 关键修复:标记录音已被取消,防止onStart回调恢复录音
|
|
|
+ this.voiceRecordCancelled = true;
|
|
|
+
|
|
|
// 清除计时器
|
|
|
if (this.voiceRecordingTimer) {
|
|
|
clearInterval(this.voiceRecordingTimer);
|
|
|
@@ -1642,15 +1845,22 @@ export default {
|
|
|
this.voiceCanceling = true;
|
|
|
}
|
|
|
|
|
|
- if (this.recorderManager && this.isRecording) {
|
|
|
- this.recorderManager.stop(); // 这会触发onStop回调
|
|
|
- } else {
|
|
|
- // 如果没有在录音,直接重置状态
|
|
|
- this.isRecording = false;
|
|
|
- this.showVoiceRecording = false;
|
|
|
- this.voiceCanceling = false;
|
|
|
+ // 立即隐藏录音UI
|
|
|
+ this.isRecording = false;
|
|
|
+ this.showVoiceRecording = false;
|
|
|
+
|
|
|
+ if (this.recorderManager) {
|
|
|
+ // 尝试停止录音(如果已经开始)
|
|
|
+ try {
|
|
|
+ this.recorderManager.stop();
|
|
|
+ } catch (err) {
|
|
|
+ console.log('⚠️ 停止录音失败(可能还未开始):', err);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
+ // 重置取消状态
|
|
|
+ this.voiceCanceling = false;
|
|
|
+
|
|
|
uni.showToast({
|
|
|
title: '已取消录音',
|
|
|
icon: 'none'
|