// 聊天页面逻辑 const app = getApp(); const mediaPicker = require('../../../utils/media-picker.js'); const { initPageSystemInfo } = require('../../../utils/system-info-modern.js'); const imageCacheManager = require('../../../utils/image-cache-manager.js'); const nimConversationManager = require('../../../utils/nim-conversation-manager.js'); const nimUserManager = require('../../../utils/nim-user-manager.js'); const friendAPI = require('../../../utils/friend-api.js'); Page({ data: { // 聊天基本信息 conversationId: '', targetId: '', chatName: '', chatType: 0, // 0: 单聊, 1: 群聊 isOnline: false, isNewConversation: false, // 是否为新会话 // 消息数据 messages: [], hasMore: true, lastMessageTime: 0, unreadCount: 0, loadingMessages: false, // 🔥 添加加载状态 // 输入相关 inputType: 'text', // 'text' 或 'voice' inputText: '', inputFocus: false, recording: false, voiceCancel: false, // 🔥 语音录制取消标识 isComposing: false, // 中文输入法状态 // 面板状态 showEmojiPanel: false, showMorePanel: false, showVoiceRecorder: false, // 滚动相关 scrollTop: 0, scrollIntoView: '', showScrollToBottom: false, // 🔥 控制滚动到底部按钮显示 // 常用表情 commonEmojis: [ '😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '🥰', '😍', '🤩', '😘', '😗', '😚', '😙', '😋', '😛', '😜', '🤪', '😝', '🤑', '🤗', '🤭', '🤫', '🤔', '🤐', '🤨', '😐', '😑', '😶', '😏', '😒', '🙄', '😬', '🤥', '😌', '😔', '😪', '🤤', '😴', '😷', '🤒', '🤕', '🤢', '🤮', '🤧', '🥵', '🥶', '🥴', '😵', '🤯', '🤠', '🥳', '🥸', '😎', '🤓', '🧐', '😕', '😟', '🙁', '☹️', '😮', '😯', '😲', '😳', '🥺', '😦', '😧', '😨', '😰', '😥', '😢', '😭', '😱', '😖', '😣', '😞', '😓', '😩', '😫', '🥱', '😤', '😡', '😠', '🤬', '👋', '🤚', '🖐️', '✋', '🖖', '👌', '🤌', '🤏', '✌️', '🤞', '🤟', '🤘', '🤙', '👈', '👉', '👆', '🖕', '👇', '☝️', '👍', '👎', '👊', '✊', '🤛', '🤜', '👏', '🙌', '👐', '🤲', '🤝' ], // 用户信息 userInfo: null, userAvatar: '', targetUserInfo: null, // 对方用户信息 targetAvatar: '', // 对方头像 // 系统适配信息 systemInfo: {}, statusBarHeight: 0, menuButtonHeight: 0, menuButtonTop: 0, navBarHeight: 0, windowHeight: 0, safeAreaBottom: 0, keyboardHeight: 0, // 键盘高度 inputAreaHeight: 0, // 输入框区域高度 emojiPanelHeight: 0, // 表情面板高度 // 加载状态 isLoading: false, }, // 🔍 判断内容是否像图片(URL/Base64/JSON中包含图片URL) _looksLikeImageContent(content) { try { if (!content) return false; const isImgUrl = (u) => { if (typeof u !== 'string') return false; if (/^data:image\//i.test(u)) return true; if (!/^https?:\/\//i.test(u)) return false; return /\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(u); }; if (typeof content === 'string') { const s = content.trim(); if (isImgUrl(s)) return true; if (s.startsWith('{') && s.endsWith('}')) { try { const obj = JSON.parse(s); const u = obj?.file_url || obj?.url; return isImgUrl(u || ''); } catch (_) { return false; } } return false; } if (typeof content === 'object') { const u = content.file_url || content.url; return isImgUrl(u || ''); } return false; } catch (_) { return false; } }, // 🔥 将 NIM SDK V2 的数字类型枚举转换为 WXML 中使用的字符串类型 // V2NIMMessageType: 0=文本, 1=图片, 2=语音, 3=视频, 4=位置, 5=通知, 6=文件, 10=提示, 100=自定义 convertMessageTypeToString(messageType) { const typeMap = { 0: 'text', // 文本 1: 'image', // 图片 2: 'audio', // 语音(WXML中也兼容'voice') 3: 'video', // 视频 4: 'location', // 位置 5: 'notification', // 通知 6: 'file', // 文件 10: 'tip', // 提示 100: 'custom' // 自定义 }; return typeMap[messageType] || 'text'; }, /** * 点击自己头像 -> 跳转个人资料页 */ onSelfAvatarTap(e) { try { const userId = this.data.userInfo?.user?.customId; if (!userId) return; wx.navigateTo({ url: `/subpackages/profile/profile/profile?userId=${userId}` }); } catch (err) { console.error('进入个人资料失败', err); } }, /** * 点击对方头像 -> 跳转好友详情页(单聊)或成员资料(群聊可后续拓展) */ async onPeerAvatarTap(e) { try { const senderId = e.currentTarget.dataset.senderId; if (!senderId) { console.warn('⚠️ senderId 为空,无法跳转'); return; } // // 如果是自己就直接走自己的逻辑 // const selfId = this.data.userInfo?.user?.customId; // if (senderId === selfId) { // this.onSelfAvatarTap(); // return; // } wx.showLoading({ title: '加载中...', mask: true }); // 调用好友详情接口判断关系 let res = await friendAPI.getFriendDetail(senderId) wx.hideLoading(); if (res?.code == 0 || res?.code == 200) { console.log('√ 是好友,跳转好友详情页'); wx.navigateTo({ url: `/subpackages/social/friend-detail/friend-detail?customId=${senderId}` }); } else { console.log('! 非好友,跳转陌生人预览页'); wx.navigateTo({ url: `/subpackages/social/user-preview/user-preview?customId=${senderId}` }); } } catch (err) { console.error('× 查询好友关系失败:', err); wx.hideLoading(); const senderId = e.currentTarget.dataset.senderId; if (senderId) { wx.navigateTo({ url: `/subpackages/social/user-preview/user-preview?customId=${senderId}` }); } } }, async onLoad(options) { // 显示加载状态 this.setData({ isLoading: true }); // 初始化系统信息(同步,立即完成) this.initSystemInfo(); // 检查 NIM 状态 const appInstance = getApp(); this.nim = appInstance?.globalData?.nim || null; this._nimOldestMessage = null; if (!this.nim || !this.nim.V2NIMMessageService || !this.nim.V2NIMConversationService) { console.error('❌ NIM 服务不可用'); wx.showToast({ title: 'NIM服务不可用', icon: 'none' }); return; } console.log('✅ NIM消息服务可用,启用NIM模式加载消息'); // 并行初始化用户与聊天信息 const userInfoPromise = this.initUserInfo(); const chatInfoPromise = this.initChatInfo(options); await userInfoPromise; await chatInfoPromise; // 隐藏加载状态 this.setData({ isLoading: false }); // 注册 NIM 监听器 this.attachNIMListeners(); }, onShow: function () { console.log('聊天页面显示'); // 🔥 检查并重连 NIM(防止从后台恢复后 NIM 断开) this.checkAndReconnectNim(); // 进入聊天页面时自动标记所有消息已读 this.markAllMessagesRead(); }, onReady() { // 页面已就绪,获取输入框区域的实际高度 this.updateInputAreaHeight(); }, // 更新输入框区域高度 updateInputAreaHeight() { wx.createSelectorQuery() .select('.input-area') .boundingClientRect(rect => { if (rect && rect.height) { console.log('📏 输入框区域高度:', rect.height); this.setData({ inputAreaHeight: rect.height }); } }) .exec(); }, onHide: function () { // 页面隐藏时可以保持会话状态 }, onUnload: function () { // 🔥 清理录音相关定时器 if (this.voiceStartTimer) { clearTimeout(this.voiceStartTimer); this.voiceStartTimer = null; } // 页面卸载时清理 NIM 监听器 this.detachNIMListeners(); }, // 注册 NIM 事件监听器 attachNIMListeners() { console.log('📡 注册 NIM 事件监听器'); const nim = getApp().globalData.nim; if (!nim || !nim.V2NIMMessageService) { console.warn('⚠️ NIM SDK 不可用,无法注册监听器'); return; } // 🔥 使用 bind(this) 确保回调中的 this 上下文正确 // 监听新消息 this._nimMessageListener = this.handleNewNIMMessage.bind(this); // 🔥 监听消息撤回通知 this._nimMessageRevokeListener = this.handleMessageRevokeNotifications.bind(this); // 注册监听器(根据NIM SDK文档调整) try { nim.V2NIMMessageService.on('onReceiveMessages', this._nimMessageListener); nim.V2NIMMessageService.on('onMessageRevokeNotifications', this._nimMessageRevokeListener); console.log('✅ NIM 监听器注册成功'); } catch (error) { console.error('❌ 注册 NIM 监听器失败:', error); } }, // 清理 NIM 事件监听器 detachNIMListeners() { console.log('🔌 清理 NIM 事件监听器'); const nim = getApp().globalData.nim; if (!nim || !nim.V2NIMMessageService) { return; } try { if (this._nimMessageListener) { nim.V2NIMMessageService.off('onReceiveMessages', this._nimMessageListener); this._nimMessageListener = null; } if (this._nimMessageRevokeListener) { nim.V2NIMMessageService.off('onMessageRevokeNotifications', this._nimMessageRevokeListener); this._nimMessageRevokeListener = null; } console.log('✅ NIM 监听器清理成功'); } catch (error) { console.error('❌ 清理 NIM 监听器失败:', error); } }, // 🔥 检查并重连 NIM(页面级别) async checkAndReconnectNim() { try { const app = getApp(); // 触发全局 NIM 重连检查 if (app && typeof app.checkAndReconnectNim === 'function') { await app.checkAndReconnectNim(); } // 检查 NIM 实例是否可用 const nim = app.globalData.nim; if (!nim || !nim.V2NIMMessageService) { console.warn('⚠️ NIM 实例不可用'); return; } // 重新注册事件监听器(确保监听器有效) if (!this._nimMessageListener || !this._nimMessageRevokeListener) { console.log('🔄 重新注册 NIM 事件监听器'); this.detachNIMListeners(); // 先清理旧的 this.attachNIMListeners(); // 重新注册 } // 检查会话管理器状态 if (!nimConversationManager.getInitStatus()) { console.log('🔄 重新初始化会话管理器'); nimConversationManager.init(nim); } console.log('✅ 聊天页面 NIM 状态检查完成'); } catch (error) { console.error('❌ 聊天页面 NIM 重连检查失败:', error); } }, // 处理 NIM 新消息 async handleNewNIMMessage(messages) { try { console.log('📨 处理 NIM 新消息,数量:', messages?.length || 0); // 🔥 NIM SDK V2 的 onReceiveMessages 回调参数是消息数组 if (!Array.isArray(messages) || messages.length === 0) { console.warn('⚠️ 收到空消息数组或非数组数据'); return; } // 过滤出属于当前会话的消息 const currentConversationMessages = messages.filter( msg => msg.conversationId === this.data.conversationId ); if (currentConversationMessages.length === 0) { console.log('⚠️ 没有属于当前会话的消息,跳过'); return; } console.log('✅ 收到当前会话的新消息:', currentConversationMessages.length, '条'); // 格式化所有新消息 const formattedMessages = await Promise.all( currentConversationMessages.map(msg => this.formatMessage(msg)) ); // 去重处理 - 防止重复显示已存在的消息 const existingIds = new Set(this.data.messages.map(msg => msg.messageId)); const uniqueNewMessages = formattedMessages.filter( msg => !existingIds.has(msg.messageId) ); if (uniqueNewMessages.length === 0) { console.log('⚠️ 所有新消息均已存在,跳过重复添加'); return; } console.log('✅ 去重后新消息数量:', uniqueNewMessages.length); // 添加到消息列表 const updatedMessages = this.data.messages.concat(uniqueNewMessages); this.setMessagesWithDate(updatedMessages); // 滚动到底部 setTimeout(() => this.scrollToBottom(), 100); // 标记已读 await nimConversationManager.markConversationRead(this.data.conversationId); console.log('✅ 新消息处理完成'); } catch (error) { console.error('❌ 处理新消息失败:', error); } }, // 处理消息撤回通知 handleMessageRevokeNotifications(revokeNotifications) { try { if (!Array.isArray(revokeNotifications) || revokeNotifications.length === 0) { return; } // 过滤出属于当前会话的撤回通知 const currentConversationRevokes = revokeNotifications.filter( revoke => revoke.messageRefer?.conversationId === this.data.conversationId ); if (currentConversationRevokes.length === 0) { return; } // 更新本地消息状态 currentConversationRevokes.forEach(revoke => { const messageRefer = revoke.messageRefer; if (!messageRefer) return; const messageServerId = messageRefer.messageServerId; const messageClientId = messageRefer.messageClientId; // 尝试找到匹配的消息 let foundMessage = null; // 方式1: 通过 messageServerId 查找 if (messageServerId) { foundMessage = this.data.messages.find(m => m.messageId === messageServerId || m._rawMessage?.messageServerId === messageServerId ); } // 方式2: 通过 messageClientId 查找 if (!foundMessage && messageClientId) { foundMessage = this.data.messages.find(m => m.messageId === messageClientId || m._rawMessage?.messageClientId === messageClientId ); } if (foundMessage) { this.updateMessageToRecalled(foundMessage.messageId); } }); } catch (error) { console.error('处理撤回通知失败:', error); } }, // 初始化系统信息 initSystemInfo() { const pageSystemInfo = initPageSystemInfo(); this.setData({ systemInfo: pageSystemInfo.systemInfo, statusBarHeight: pageSystemInfo.statusBarHeight, menuButtonHeight: pageSystemInfo.menuButtonHeight, menuButtonTop: pageSystemInfo.menuButtonTop, navBarHeight: pageSystemInfo.navBarHeight, windowHeight: pageSystemInfo.windowHeight, safeAreaBottom: pageSystemInfo.safeAreaBottom, keyboardHeight: 0 // 初始化键盘高度 }); }, // 🔥 监听键盘高度变化 onKeyboardHeightChange(e) { const { height, duration } = e.detail; const inputAreaHeight = this.data.inputAreaHeight; const totalBottom = inputAreaHeight + (height || 0); console.log('⌨️ 键盘高度变化:', { keyboardHeight: height, inputAreaHeight, totalBottom, duration }); // 直接使用微信提供的键盘高度(单位:px) this.setData({ keyboardHeight: height || 0 }, () => { // 键盘弹出时,滚动到底部,确保最新消息可见 if (height > 0) { this.scrollToBottom(true); } }); }, // 初始化聊天信息 async initChatInfo(options) { const inputConversationId = options.conversationId || ''; const targetId = options.targetId || ''; const chatName = decodeURIComponent(options.name || '聊天'); const chatType = parseInt(options.chatType || 0); const incomingTargetAvatar = options.targetAvatar || ''; const sendMessage = options.sendMessage?.trim() || ''; // 临时设置基本信息 this.setData({ targetId, chatName, chatType, inputText: sendMessage, conversationId: inputConversationId, targetAvatar: incomingTargetAvatar || this.data.targetAvatar || '' }); // 🔥 动态设置导航栏标题为用户昵称 wx.setNavigationBarTitle({ title: chatName }); // 🔥 从 NIM 获取对方用户信息(头像和昵称) // 注意:如果 targetId 是会话ID格式(如 "p2p-xxx" 或 "16950956|1|33981539"),需要提取真实用户ID if (targetId) { let realUserId = targetId; // 🔥 如果是 NIM 会话ID格式(包含 | 分隔符),提取真实用户ID // 格式:senderId|conversationType|receiverId(如 "16950956|1|33981539") if (targetId.includes('|')) { const parts = targetId.split('|'); if (parts.length >= 3) { // 对于单聊(type=1),receiverId 是对方用户ID(parts[2]) realUserId = parts[2]; console.log('🔧 从会话ID提取用户ID:', { conversationId: targetId, userId: realUserId }); } } await this.loadTargetUserInfo(realUserId); } // 🔥 如果没有传入conversationId,先检查会话是否已存在 if (!inputConversationId) { console.log('🔍 未传入会话ID,检查是否已存在该会话'); if (!targetId) { console.error('❌ 缺少 targetId,无法查询会话'); return; } // 使用NIM生成会话ID const conversationId = this.buildNIMConversationId(targetId, chatType); if (!conversationId) { console.error('❌ 无法生成会话ID'); return; } console.log('🔍 生成的会话ID:', conversationId); // 🔥 检查会话是否已存在(从NIM SDK获取会话列表查询) try { const nim = getApp().globalData.nim; if (nim && nim.V2NIMConversationService) { // 🔥 获取会话列表并查找目标会话 console.log('🔍 从NIM SDK查询会话是否存在...'); const result = await nim.V2NIMConversationService.getConversationList(0, 100); const existingConv = result?.conversationList?.find(c => c.conversationId === conversationId); if (existingConv) { console.log('✅ 会话已存在,使用现有会话:', existingConv); this.setData({ conversationId, isNewConversation: false }); // 加载消息 await this.loadMessages(); return; } else { console.log('📝 会话不存在,标记为新会话'); } } } catch (error) { console.warn('⚠️ 查询会话失败,将作为新会话处理:', error); } // 🔥 会话不存在,标记为新会话(不加载历史消息) console.log('✅ 创建新会话ID:', conversationId); this.setData({ conversationId, isNewConversation: true, messages: [] }); return; } // 使用传入的conversationId this.setData({ conversationId: inputConversationId }); // 加载消息 await this.loadMessages(); }, // 🔥 验证并使用传入的会话ID buildNIMConversationId(targetId, chatType) { try { if (!this.nim || !this.nim.V2NIMConversationIdUtil) { return ''; } if (!targetId) { return ''; } if (Number(chatType) === 1) { return this.nim.V2NIMConversationIdUtil.teamConversationId(String(targetId)); } return this.nim.V2NIMConversationIdUtil.p2pConversationId(String(targetId)); } catch (err) { console.warn('构造 NIM 会话ID 失败:', err); return ''; } }, // 🔥 从 NIM SDK 获取对方用户信息(包括头像和昵称) async loadTargetUserInfo(targetId) { try { console.log('📡 从 NIM 获取用户信息:', targetId); // 🔥 使用 NIM SDK 获取用户信息 const userInfo = await nimUserManager.getUserInfo(targetId); if (userInfo) { const targetAvatar = userInfo.avatar || ''; const targetNickname = userInfo.nickname || ''; // 🔥 先设置用户信息,头像缓存异步执行 this.setData({ targetUserInfo: { avatar: targetAvatar, nickname: targetNickname }, targetAvatar: targetAvatar, // 先使用原始URL chatName: targetNickname || this.data.chatName // 更新聊天名称 }); console.log('✅ 用户信息加载成功(从 NIM):', { targetNickname, targetAvatar }); // 🔥 异步缓存头像,不阻塞UI if (targetAvatar) { imageCacheManager.cacheAvatar(targetAvatar).then(cachedAvatar => { this.setData({ targetAvatar: cachedAvatar }); }).catch(error => { console.error('❌ 对方头像缓存失败:', error); }); } } else { console.warn('⚠️ 从 NIM 未获取到用户信息'); } } catch (error) { console.error('❌ 从 NIM 获取用户信息异常:', error); } }, // 初始化用户信息 async initUserInfo() { // 先尝试从全局获取 let userInfo = app.globalData.userInfo; // 如果全局没有,尝试从本地存储获取 if (!userInfo || !userInfo.token) { try { const storedUserInfo = wx.getStorageSync('userInfo'); if (storedUserInfo && storedUserInfo.token) { userInfo = storedUserInfo; // 更新全局数据 app.globalData.userInfo = userInfo; app.globalData.isLoggedIn = true; } } catch (error) { console.error('从本地存储获取用户信息失败:', error); } } // 确保有用户信息 if (userInfo && userInfo.token) { // 正确获取用户头像 const userAvatar = userInfo.user?.avatar || userInfo.avatar || ''; // 🔥 先设置用户信息,头像缓存异步执行 this.setData({ userInfo: userInfo, userAvatar: userAvatar // 先使用原始URL }); // 🔥 异步缓存头像,不阻塞UI imageCacheManager.cacheAvatar(userAvatar).then(cachedAvatar => { this.setData({ userAvatar: cachedAvatar }); }).catch(error => { console.error('❌ 用户头像缓存失败:', error); }); } else { console.error('用户信息初始化失败,跳转到登录页'); wx.reLaunch({ url: '/pages/login/login' }); } }, // 加载消息 - 使用 NIM SDK async loadMessages(isLoadMore = false) { try { console.log('📨 加载聊天消息 (NIM SDK):', { isLoadMore, conversationId: this.data.conversationId }); // 检查 conversationId if (!this.data.conversationId) { console.log('⚠️ 缺少 conversationId,无法加载消息'); return; } // 避免重复请求 if (this.data.loadingMessages) { console.log('⚠️ 消息正在加载中,跳过重复请求'); return; } // 设置加载状态 this.setData({ loadingMessages: true }); // 获取 NIM 实例 const nim = getApp().globalData.nim; if (!nim || !nim.V2NIMMessageService) { throw new Error('NIM SDK 未初始化'); } // 使用 NIM SDK 获取历史消息 const limit = 20; const option = { conversationId: this.data.conversationId, endTime: isLoadMore && this.data.lastMessageTime ? this.data.lastMessageTime : Date.now(), limit: limit, direction: 0 // 1 表示查询比 endTime 更早的消息 }; console.log('📡 查询参数:', option); const result = await nim.V2NIMMessageService.getMessageList(option); if (!result) { console.log('⚠️ NIM SDK 返回空结果'); this.setData({ loadingMessages: false, hasMore: false }); return; } const newMessages = result || []; console.log('✅ NIM 消息回调:', newMessages); console.log('✅ 从 NIM 获取到消息:', newMessages.length, '条'); // 格式化消息 const formattedMessages = await Promise.all( newMessages.map(msg => this.formatMessage(msg)) ); // 判断是否是首次加载 const isFirstLoad = this.data.messages.length === 0; // 去重处理 const existingIds = new Set(this.data.messages.map(msg => msg.messageId)); const uniqueNewMessages = formattedMessages.filter( msg => !existingIds.has(msg.messageId) ); // 合并并排序消息 let sortedMessages; if (isFirstLoad) { sortedMessages = uniqueNewMessages.sort((a, b) => a.timestamp - b.timestamp); } else { const allMessages = uniqueNewMessages.concat(this.data.messages); sortedMessages = allMessages.sort((a, b) => a.timestamp - b.timestamp); } // 更新 lastMessageTime(最早消息的时间戳,用于分页) const earliestMessage = newMessages.length > 0 ? newMessages[newMessages.length - 1] : null; const newLastMessageTime = earliestMessage?.createTime || earliestMessage?.timestamp || 0; // 添加日期分隔符 const annotated = this.recomputeDateDividers(this._filterDeleted(sortedMessages)); // 更新数据 this.setData({ messages: annotated, hasMore: newMessages.length === limit, lastMessageTime: newLastMessageTime, loadingMessages: false }); console.log('✅ 消息加载完成:', { totalMessages: sortedMessages.length, hasMore: newMessages.length === limit, lastMessageTime: newLastMessageTime }); // 首次加载时滚动到底部 if (isFirstLoad && this.data.messages.length > 0) { setTimeout(() => this.scrollToBottom(), 100); } } catch (error) { console.error('❌ 加载消息失败:', error); this.setData({ hasMore: false, loadingMessages: false }); wx.showToast({ title: '加载消息失败', icon: 'none' }); } }, // 格式化消息 - 适配接口文档的数据结构,特殊处理图片消息 async formatMessage(msgData) { // 获取当前用户的NIM账号ID(用于判断消息是否为自己发送) let currentNimAccountId = this.data.userInfo?.neteaseIMAccid || this.data.userInfo?.user?.neteaseIMAccid || ''; // 如果从data中获取不到,尝试从全局获取 if (!currentNimAccountId) { const app = getApp(); const globalUserInfo = app.globalData.userInfo; currentNimAccountId = globalUserInfo?.neteaseIMAccid || globalUserInfo?.user?.neteaseIMAccid || ''; } // 如果还是获取不到,尝试从本地存储获取 if (!currentNimAccountId) { try { const storedUserInfo = wx.getStorageSync('userInfo'); currentNimAccountId = storedUserInfo?.neteaseIMAccid || storedUserInfo?.user?.neteaseIMAccid || ''; } catch (error) { console.error('从本地存储获取NIM账号ID失败:', error); } } // 适配 NIM SDK V2 官方字段名 const senderId = msgData.senderId || msgData.senderAccountId; const messageId = msgData.messageClientId || msgData.messageServerId || msgData.id || msgData.messageId; const isSelf = senderId === currentNimAccountId; // 🔥 直接使用 NIM SDK V2 的 messageType 枚举值(无需转换) // V2NIMMessageType: 0=文本, 1=图片, 2=语音, 3=视频, 4=位置, 5=通知, 6=文件, 10=提示, 100=自定义 let messageType = msgData.messageType !== undefined ? msgData.messageType : msgData.msgType; // 🔥 获取消息内容 - NIM SDK V2 使用 text(文本) 或 attachment(附件) let _rawContent; if (msgData.text !== undefined && msgData.text !== null) { _rawContent = msgData.text; } else if (msgData.attachment !== undefined && msgData.attachment !== null) { _rawContent = msgData.attachment; } else if (msgData.content !== undefined && msgData.content !== null) { _rawContent = msgData.content; } else { _rawContent = ''; } // 标记为图片消息(1)但内容不是可用图片 -> 降级为文本(0) if (messageType === 1) { let hasValidImage = false; try { if (typeof _rawContent === 'string') { const s = _rawContent.trim(); if (/^https?:\/\//i.test(s) || /^data:image\//i.test(s)) { hasValidImage = true; } else if (s.startsWith('{') && s.endsWith('}')) { const obj = JSON.parse(s); const u = obj?.file_url || obj?.url; hasValidImage = !!u; } } else if (typeof _rawContent === 'object' && _rawContent !== null) { const u = _rawContent.file_url || _rawContent.url; hasValidImage = !!u; } } catch (_) { hasValidImage = false; } if (!hasValidImage) { console.log('🔧 检测到图片类型但无有效URL,降级为文本'); messageType = 0; // 降级为文本 } } // 标记为文本(0)但内容像图片 -> 升级为图片(1) if (messageType === 0 && this._looksLikeImageContent(_rawContent)) { console.log('🖼️ 文本消息内容识别为图片,自动升级为图片类型'); messageType = 1; } // 🔥 撤回态识别(历史记录/服务端数据) // 注意:撤回状态应该通过 msgData.isRecalled 字段判断,不应该依赖 sendingState // sendingState 枚举值: 0=未知, 1=成功, 2=失败, 3=发送中 const recalledFromServer = !!msgData.isRecalled; // 只通过 isRecalled 字段判断是否撤回 // 🔥 特殊处理媒体消息内容 let processedContent = _rawContent; if (messageType === 1) { // 图片 processedContent = await this.parseImageContent(_rawContent); console.log('🖼️ 图片消息处理结果:', processedContent); } else if (messageType === 2) { // 语音 processedContent = this.parseVoiceContent(_rawContent); console.log('🎤 语音消息处理结果:', processedContent); } else if (messageType === 6) { // 文件 processedContent = this.parseFileContent(_rawContent); console.log('📄 文件消息处理结果:', processedContent); } else if (messageType === 4) { // 位置 processedContent = this.parseLocationContent(_rawContent); console.log('📍 位置消息处理结果:', processedContent); } else if (messageType === 3) { // 视频 processedContent = this.parseVideoContent(_rawContent); console.log('🎬 视频消息处理结果:', processedContent); } else if (messageType === 0) { // 文本 processedContent = msgData.text || _rawContent; } // 🔥 统一时间戳字段 - NIM SDK V2 使用 createTime let timestamp = 0; try { // 优先使用 createTime,兼容其他时间字段 if (typeof msgData.createTime === 'number') { timestamp = msgData.createTime; } else if (typeof msgData.sendTime === 'number') { timestamp = msgData.sendTime; } else if (typeof msgData.sendTime === 'string') { const t = Date.parse(msgData.sendTime); timestamp = isNaN(t) ? Date.now() : t; } else { timestamp = Number(msgData.timestamp || Date.now()); } } catch (_) { timestamp = Date.now(); } // 🔥 获取发送者头像(已通过 NIM getUserInfo 获取并缓存) const senderAvatar = isSelf ? (this.data.userAvatar || '') : (this.data.targetAvatar || ''); // 🔥 处理消息发送状态 let deliveryStatus; if (isSelf) { // 根据官方文档的正确枚举值处理 if (msgData.sendingState === 3) { deliveryStatus = 3; // 发送中 (V2NIM_MESSAGE_SENDING_STATE_SENDING) } else if (msgData.sendingState === 2) { deliveryStatus = 2; // 发送失败 (V2NIM_MESSAGE_SENDING_STATE_FAILED) } else { // 对于历史消息,默认为成功状态 // 包括: sendingState为0(未知)、1(成功)、undefined、null或其他值的情况 deliveryStatus = 1; // V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED console.log('📜 历史消息状态设为成功:', { messageId: msgData.messageClientId || msgData.messageServerId, sendingState: msgData.sendingState, status: msgData.status, finalStatus: deliveryStatus }); } } else { // 对方消息:成功状态 deliveryStatus = 1; } const baseMessage = { messageId: messageId, senderId: senderId, targetId: msgData.receiverId || msgData.targetId, content: processedContent, msgType: this.convertMessageTypeToString(messageType), // 🔥 转换为字符串类型 (text/image/audio/video/location/file) sendTime: timestamp, timestamp: timestamp, isSelf: isSelf, senderName: msgData.senderName || (isSelf ? '我' : '对方'), senderAvatar: senderAvatar, deliveryStatus: deliveryStatus, bubbleTime: this.formatBubbleTime(timestamp), _rawMessage: msgData // 🔥 保存 NIM SDK 原始消息对象,用于撤回等操作 }; // 🔥 调试:查看消息状态转换结果 console.log('🔍 消息状态转换:', { isSelf: isSelf, sendingState: msgData.sendingState, status: msgData.status, deliveryStatus: baseMessage.deliveryStatus, messageId: baseMessage.messageId, messageType: 'formatMessage调用 - 历史消息处理' }); // NIM SDK V2 撤回的消息会通过 onMessageRevokeNotifications 事件通知 if (recalledFromServer) { return { ...baseMessage, isRecalled: true, msgType: 'system', // 🔥 系统消息类型使用字符串 content: isSelf ? '你撤回了一条消息' : (msgData.senderName ? `${msgData.senderName}撤回了一条消息` : '对方撤回了一条消息'), originalContent: processedContent }; } // 文件消息补充文件展示字段,便于WXML直接使用 if (messageType === 6) { // 文件 return { ...baseMessage, fileName: processedContent.fileName || msgData.fileName || '', fileSize: this.formatFileSize(processedContent.fileSize || msgData.fileSize || 0), fileType: processedContent.fileType || msgData.fileType || '' }; } // 🔥 位置消息:为兼容旧渲染逻辑,将名称/地址/经纬度同步到根级字段,避免显示默认“位置” if (messageType === 4) { // 位置 return { ...baseMessage, locationName: processedContent?.locationName || msgData.locationName || '位置', locationAddress: processedContent?.address || msgData.locationAddress || '', latitude: processedContent?.latitude, longitude: processedContent?.longitude }; } // 🔥 视频消息:将URL和缩略图同步到根级字段,兼容WXML渲染 if (messageType === 3) { // 视频 return { ...baseMessage, content: processedContent.url || processedContent, // WXML 中使用 item.content 作为 src thumbnail: processedContent.thumbnail || '', duration: processedContent.duration || 0, videoUrl: processedContent.url || '' }; } // 🔥 最终调试输出 console.log('✅ formatMessage 完成:', { messageId, msgType: baseMessage.msgType, // 使用转换后的字符串类型 msgTypeNumber: messageType, // 原始数字类型(用于调试) content: processedContent, deliveryStatus: baseMessage.deliveryStatus, isSelf, timestamp }); return baseMessage; }, // 🔥 解析语音内容 - 支持 NIM SDK V2 的 attachment 对象格式 parseVoiceContent(content) { try { if (!content) return { url: '', duration: 0 }; // NIM SDK V2 的语音附件对象格式 // attachment 通常包含: url, dur (duration), size 等字段 if (typeof content === 'object' && content !== null) { const url = content.url || content.path || content.voiceUrl || ''; const duration = content.dur || content.duration || 0; // NIM SDK V2 使用 dur const size = content.size || content.fileSize || 0; console.log('🎤 语音附件对象解析:', { url, duration, size }); return { url, duration, size }; } // 如果是字符串,尝试JSON解析 if (typeof content === 'string') { const trimmed = content.trim(); if (trimmed.startsWith('{') && trimmed.endsWith('}')) { try { const obj = JSON.parse(trimmed); const url = obj.url || obj.path || obj.voiceUrl || ''; const duration = obj.dur || obj.duration || 0; const size = obj.size || obj.fileSize || 0; return { url, duration, size }; } catch (e) { // 继续按URL兜底 } } // 直接当作URL使用 return { url: trimmed, duration: 0 }; } return { url: '', duration: 0 }; } catch (error) { console.error('❌ 解析语音内容失败:', error, content); return { url: '', duration: 0 }; } }, // 🔥 解析文件内容 - 支持 NIM SDK V2 的 attachment 对象格式 parseFileContent(content) { try { if (!content) return { url: '', fileName: '', fileSize: 0 }; // NIM SDK V2 的文件附件对象格式 if (typeof content === 'object' && content !== null) { const url = content.url || content.path || content.file_url || content.fileUrl || ''; const fileName = content.name || content.fileName || content.file_name || ''; const fileSize = content.size || content.fileSize || content.file_size || 0; const ext = content.ext || content.fileType || content.file_type || ''; console.log('📄 文件附件对象解析:', { url, fileName, fileSize, ext }); return { url, fileName, fileSize, fileType: ext }; } // JSON字符串 if (typeof content === 'string') { const trimmed = content.trim(); if (trimmed.startsWith('{') && trimmed.endsWith('}')) { try { const obj = JSON.parse(trimmed); const url = obj.url || obj.path || obj.file_url || obj.fileUrl || ''; const fileName = obj.name || obj.fileName || obj.file_name || ''; const fileSize = obj.size || obj.fileSize || obj.file_size || 0; const fileType = obj.ext || obj.fileType || obj.file_type || ''; return { url, fileName, fileSize, fileType }; } catch (e) { // fallthrough to plain URL } } // 纯URL return { url: trimmed, fileName: '', fileSize: 0 }; } return { url: '', fileName: '', fileSize: 0 }; } catch (error) { console.error('❌ 解析文件内容失败:', error, content); return { url: '', fileName: '', fileSize: 0 }; } }, // 🔥 解析位置内容 - 支持 NIM SDK V2 的 attachment 对象格式 parseLocationContent(content) { try { if (!content) return null; let obj = content; // NIM SDK V2 的位置附件对象格式 // attachment 通常包含: lat, lng, title, address 等字段 if (typeof content === 'string') { const trimmed = content.trim(); if (trimmed.startsWith('{') && trimmed.endsWith('}')) { try { obj = JSON.parse(trimmed); } catch (e) { return null; } } else { // 非JSON字符串,无法解析 return null; } } // NIM SDK V2 使用 lat/lng,兼容 latitude/longitude const lat = obj.lat ?? obj.latitude; const lng = obj.lng ?? obj.longitude; const address = obj.address || obj.addr || ''; const locationName = obj.title || obj.locationName || obj.name || '位置'; console.log('📍 位置附件对象解析:', { lat, lng, address, locationName }); if (lat == null || lng == null) return null; return { latitude: Number(lat), longitude: Number(lng), address, locationName }; } catch (err) { console.error('❌ 解析位置内容失败:', err, content); return null; } }, // 🔥 解析视频内容 - 支持 NIM SDK V2 的 attachment 对象格式 parseVideoContent(content) { try { if (!content) return { url: '', thumbnail: '' }; // NIM SDK V2 的视频附件对象格式 if (typeof content === 'object' && content !== null) { const url = content.url || content.path || ''; const thumbnail = content.thumbUrl || content.coverUrl || content.thumbnail || ''; const duration = content.dur || content.duration || 0; const size = content.size || content.fileSize || 0; const width = content.width || content.w || 0; const height = content.height || content.h || 0; console.log('🎬 视频附件对象解析:', { url, thumbnail, duration, size, width, height }); return { url, thumbnail, duration, size, width, height }; } // JSON 字符串 if (typeof content === 'string') { const trimmed = content.trim(); if (trimmed.startsWith('{') && trimmed.endsWith('}')) { try { const obj = JSON.parse(trimmed); const url = obj.url || obj.path || ''; const thumbnail = obj.thumbUrl || obj.coverUrl || obj.thumbnail || ''; const duration = obj.dur || obj.duration || 0; return { url, thumbnail, duration }; } catch (e) { // 继续按URL兜底 } } // 直接当作URL使用 return { url: trimmed, thumbnail: '' }; } return { url: '', thumbnail: '' }; } catch (error) { console.error('❌ 解析视频内容失败:', error, content); return { url: '', thumbnail: '' }; } }, // 🔥 根据官方文档,NIM SDK V2 的正确枚举值(0=未知, 1=成功, 2=失败, 3=发送中) convertStatus(apiStatus) { // NIM SDK V2 的 V2NIMMessageSendingState 枚举: // V2NIM_MESSAGE_SENDING_STATE_UNKNOWN = 0 (未知状态) // V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED = 1 (发送成功) // V2NIM_MESSAGE_SENDING_STATE_FAILED = 2 (发送失败) // V2NIM_MESSAGE_SENDING_STATE_SENDING = 3 (发送中) console.log('🔄 convertStatus 输入:', { apiStatus, type: typeof apiStatus, isUndefined: apiStatus === undefined, isNull: apiStatus === null }); // 🔥 直接返回数字枚举值,不转换为字符串 // undefined/null 默认为成功状态 1 (历史消息通常没有状态字段,说明已成功) if (apiStatus === undefined || apiStatus === null) { console.log('🔄 状态映射 convertStatus: undefined/null -> 1 (历史消息默认成功)'); return 1; // V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED } // 如果已经是数字,直接返回 if (typeof apiStatus === 'number') { // 验证是否为有效的 sendingState 枚举值 (0, 1, 2, 3) if (apiStatus === 0 || apiStatus === 1 || apiStatus === 2 || apiStatus === 3) { console.log('🔄 状态映射 convertStatus:', apiStatus, '(有效枚举值,保持不变)'); return apiStatus; } // 对于历史消息,其他数字状态也默认为成功(因为能从服务器获取到说明已发送成功) console.log('🔄 状态映射 convertStatus:', apiStatus, '-> 1 (历史消息其他数字状态,默认成功)'); return 1; } // 字符串状态转换为枚举值 const s = String(apiStatus).toLowerCase(); let result; if (s === 'succeeded' || s === 'success' || s === 'sent' || s === 'delivered') { result = 1; // SUCCEEDED } else if (s === 'failed' || s === 'fail') { result = 2; // FAILED } else if (s === 'sending') { result = 3; // SENDING } else { // 对于历史消息,未知状态默认为成功 result = 1; // 默认成功 } console.log('🔄 状态映射 convertStatus:', apiStatus, '->', result, '(历史消息处理)'); return result; }, // 🔥 解析图片内容 - 支持 NIM SDK V2 的 attachment 对象格式 async parseImageContent(content) { try { if (!content) return { type: 'url', url: '' }; // 1) NIM SDK V2 的 attachment 对象格式 // attachment 通常包含: url, name, size, width, height 等字段 if (typeof content === 'object') { // NIM SDK V2 图片附件字段 const url = content.url || content.file_url || content.path || ''; const fileName = content.name || content.fileName || content.file_name || ''; const fileSize = content.size || content.fileSize || content.file_size || 0; const width = content.width || content.w || 0; const height = content.height || content.h || 0; console.log('🖼️ 图片附件对象解析:', { url, fileName, fileSize, width, height }); if (/^data:image\//i.test(url)) { return { type: 'base64', url, fileName, fileSize, width, height }; } if (url) { const cachedUrl = await imageCacheManager.preloadImage(url); return { type: 'url', url: cachedUrl, fileName, fileSize, width, height }; } // 对象但无 url,视为错误 return { type: 'error', url: '', error: '图片内容缺少URL', originalContent: JSON.stringify(content).slice(0, 200) }; } // 2) 字符串:Base64 if (typeof content === 'string' && /^data:image\//i.test(content)) { return { type: 'base64', url: content }; } // 3) 字符串:URL if (typeof content === 'string' && /^https?:\/\//i.test(content)) { const cachedUrl = await imageCacheManager.preloadImage(content); return { type: 'url', url: cachedUrl }; } // 4) 字符串:尝试 JSON 解析 if (typeof content === 'string') { const s = content.trim(); if (s.startsWith('{') && s.endsWith('}')) { try { const obj = JSON.parse(s); const u = obj.url || obj.file_url || obj.path || ''; if (u) { const cachedUrl = await imageCacheManager.preloadImage(u); return { type: /^data:image\//i.test(u) ? 'base64' : 'url', url: cachedUrl, fileName: obj.name || obj.file_name || obj.fileName, fileSize: obj.size || obj.file_size || obj.fileSize, width: obj.width || obj.w, height: obj.height || obj.h }; } } catch (e) { // fallthrough } } } // 5) 其他:无法识别,按错误返回,避免把纯文本当图片 return { type: 'error', url: '', error: '图片内容无效', originalContent: String(content).slice(0, 200) }; } catch (err) { console.error('❌ 解析图片内容失败:', err, content); return { type: 'error', url: '', error: '解析失败', originalContent: String(content).slice(0, 200) }; } }, // 🔥 新增:解析视频内容,统一返回可用于