// 聊天页面逻辑 const app = getApp(); const apiClient = require('../../../utils/api-client.js'); const wsManager = require('../../../utils/websocket-manager-v2.js'); const wsdiagnostic = require('../../../utils/websocket-diagnostic.js'); const mediaPicker = require('../../../utils/media-picker.js'); const voiceMessageManager = require('../../../utils/voice-message-manager.js'); const { initPageSystemInfo } = require('../../../utils/system-info-modern.js'); const imageCacheManager = require('../../../utils/image-cache-manager.js'); Page({ data: { // 聊天基本信息 conversationId: '', targetId: '', chatName: '', chatType: 0, // 0: 单聊, 1: 群聊 isOnline: false, isNewConversation: false, // 是否为新会话 // 消息数据 messages: [], hasMore: true, lastMessageId: '', unreadCount: 0, loadingMessages: false, // 🔥 添加加载状态 // 输入相关 inputType: 'text', // 'text' 或 'voice' inputText: '', inputFocus: false, recording: false, isComposing: false, // 中文输入法状态 // 面板状态 showEmojiPanel: false, showMorePanel: false, showVoiceRecorder: false, // 滚动相关 scrollTop: 0, scrollIntoView: '', showScrollToBottom: false, // 🔥 控制滚动到底部按钮显示 // 常用表情 commonEmojis: [ '😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '🥰', '😍', '🤩', '😘', '😗', '😚', '😙', '😋', '😛', '😜', '🤪', '😝', '🤑', '🤗', '🤭', '🤫', '🤔', '🤐', '🤨', '😐', '😑', '😶', '😏', '😒', '🙄', '😬', '🤥', '😌', '😔', '😪', '🤤', '😴', '😷', '🤒', '🤕', '🤢', '🤮', '🤧', '🥵', '🥶', '🥴', '😵', '🤯', '🤠', '🥳', '🥸', '😎', '🤓', '🧐', '😕', '😟', '🙁', '☹️', '😮', '😯', '😲', '😳', '🥺', '😦', '😧', '😨', '😰', '😥', '😢', '😭', '😱', '😖', '😣', '😞', '😓', '😩', '😫', '🥱', '😤', '😡', '😠', '🤬', '👋', '🤚', '🖐️', '✋', '🖖', '👌', '🤌', '🤏', '✌️', '🤞', '🤟', '🤘', '🤙', '👈', '👉', '👆', '🖕', '👇', '☝️', '👍', '👎', '👊', '✊', '🤛', '🤜', '👏', '🙌', '👐', '🤲', '🤝' ], // 用户信息 userInfo: null, userAvatar: '', targetUserInfo: null, // 对方用户信息 targetAvatar: '', // 对方头像 // 消息相关 messages: [], hasMore: true, lastMessageId: '', loadingMessages: false, // 🔥 添加加载状态字段 // 系统适配信息 systemInfo: {}, statusBarHeight: 0, menuButtonHeight: 0, menuButtonTop: 0, navBarHeight: 0, windowHeight: 0, safeAreaBottom: 0, // 表情数据 emojis: [ '😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '🥰', '😍', '🤩', '😘', '😗', '😚', '😙', '😋', '😛', '😜', '🤪', '😝', '🤑', '🤗', '🤭', '🤫', '🤔' ], // 🔥 新增:组件状态(不影响现有功能) // 媒体预览组件 mediaPreviewVisible: false, mediaPreviewList: [], currentMediaIndex: 0, // 消息操作菜单组件 messageActionVisible: false, currentMessage: null, isOwnMessage: false, // @提醒选择器组件 mentionSelectorVisible: false, // 加载状态 isLoading: false, // 主题相关 themeClass: 'theme-dark', isThemeTransitioning: false, // 主题切换径向遮罩 showThemeOverlay: false, overlayPlaying: false, overlayTo: 'theme-light', overlayX: 0, overlayY: 0, overlayDiameter: 0, overlayLeft: 0, overlayTop: 0 }, async onLoad(options) { console.log('聊天页面加载:', options); // 显示加载状态 this.setData({ isLoading: true }); // 初始化系统信息(同步,立即完成) this.initSystemInfo(); // 🔥 并行执行用户信息初始化和聊天信息初始化 const userInfoPromise = this.initUserInfo(); const chatInfoPromise = this.initChatInfo(options); // 等待用户信息完成(消息加载需要用户信息) await userInfoPromise; // 初始化WebSocket(不等待) this.initWebSocket(); // 等待聊天信息初始化完成 await chatInfoPromise; // 隐藏加载状态 this.setData({ isLoading: false }); }, onShow: function () { // 初始化主题 const savedTheme = wx.getStorageSync('theme') || 'theme-dark'; this.setData({ themeClass: savedTheme }); // 页面显示时连接WebSocket if (wsManager && !wsManager.isConnected) { wsManager.connect(); } // 🔥 进入聊天页面时自动标记所有消息已读 this.markAllMessagesRead(); }, onHide: function () { // 页面隐藏时可以保持WebSocket连接,因为其他页面可能需要 }, onUnload: function () { // 页面卸载时清理WebSocket监听器 if (wsManager) { wsManager.off('new_message'); wsManager.off('message_status'); wsManager.off('message_recalled'); } }, // 初始化系统信息 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 }); }, // 初始化聊天信息 - 修正:从API获取正确的conversationId async initChatInfo(options) { const inputConversationId = options.conversationId || ''; const targetId = options.targetId || ''; const chatName = decodeURIComponent(options.name || '聊天'); const chatType = parseInt(options.chatType || 0); // 临时设置基本信息 this.setData({ targetId, chatName, chatType, conversationId: inputConversationId // 临时设置,等API返回正确的 }); // 🔥 动态设置导航栏标题为用户昵称 wx.setNavigationBarTitle({ title: chatName }); console.log('聊天信息初始化:', { inputConversationId, targetId, chatName, chatType }); // 🔥 从API获取正确的conversationId await this.getCorrectConversationId(targetId, chatType); }, // 从API获取正确的conversationId async getCorrectConversationId(targetId, chatType) { try { console.log('🔍 从API获取正确的conversationId...'); // 调用conversations接口获取会话列表 const response = await apiClient.getConversations(); if (response && response.code === 0) { const conversations = response.data || []; console.log('✅ 获取到会话列表:', conversations.length, '个'); // 🔥 调试:显示所有会话信息 console.log('🔍 调试会话查找:'); console.log('- 目标targetId:', targetId); console.log('- 聊天类型chatType:', chatType); console.log('- 会话列表:', conversations.map(conv => ({ id: conv.id || conv.conversationId, targetId: conv.targetId, type: conv.type, targetName: conv.targetName }))); // 查找与目标用户的会话 let targetConversation = null; if (chatType === 0) { // 单聊:查找targetId匹配的会话 targetConversation = conversations.find(conv => conv.type === 0 && conv.targetId === targetId ); console.log('🔍 单聊查找结果:', targetConversation); } else { // 群聊:查找群ID匹配的会话 targetConversation = conversations.find(conv => conv.type === 1 && conv.targetId === targetId ); console.log('🔍 群聊查找结果:', targetConversation); } // 🔥 检查会话ID字段 const conversationId = targetConversation?.id || targetConversation?.conversationId; console.log('🔍 会话ID检查:', { hasConversation: !!targetConversation, id: targetConversation?.id, conversationId: targetConversation?.conversationId, finalId: conversationId }); if (targetConversation && conversationId) { // 🔥 找到了现有会话,使用API返回的会话ID console.log('✅ 找到现有会话:', conversationId); this.setData({ conversationId: conversationId, // 同时更新其他会话信息 chatName: targetConversation.targetName || this.data.chatName, unreadCount: targetConversation.unreadCount || 0, isNewConversation: false }); // 🔥 并行获取对方用户信息和加载消息 if (chatType === 0) { // 不等待头像加载完成,立即开始加载消息 this.loadTargetUserInfo(targetId).catch(error => { console.error('获取对方用户信息失败:', error); }); } // 立即加载消息,不等待头像 this.loadMessages(); } else { // 🔥 没有找到现有会话,这是新会话,不设置conversationId // 等发送第一条消息时,后端会创建会话并返回会话ID console.log('⚠️ 未找到现有会话,这是新会话'); this.setData({ conversationId: '', // 🔥 新会话不设置ID isNewConversation: true, messages: [], hasMore: false }); // 🔥 获取对方用户信息(包括头像)- 异步执行 if (chatType === 0) { this.loadTargetUserInfo(targetId).catch(error => { console.error('获取对方用户信息失败:', error); }); } console.log('🆕 新会话,等待发送消息时创建'); } } else { throw new Error(response?.message || '获取会话列表失败'); } } catch (error) { console.error('❌ 获取conversationId失败:', error); // 🔥 对于新会话,不设置conversationId,等后端创建 this.setData({ conversationId: '', isNewConversation: true, messages: [], hasMore: false }); // 🔥 获取对方用户信息(包括头像) if (this.data.chatType === 0) { await this.loadTargetUserInfo(this.data.targetId); } console.log('🔄 降级处理:新会话模式'); } }, // 🔥 新增:获取对方用户信息(包括头像) async loadTargetUserInfo(targetId) { try { console.log('👤 开始获取对方用户信息:', targetId); // 调用好友详情API获取用户信息 const response = await apiClient.getFriendDetail(targetId); if (response && response.code === 0) { const userInfo = response.data; console.log('✅ 获取到对方用户信息:', userInfo); // 设置对方头像 const targetAvatar = userInfo.avatar || ''; console.log('👤 设置对方头像:', targetAvatar); // 🔥 先设置用户信息,头像缓存异步执行 this.setData({ targetUserInfo: userInfo, targetAvatar: targetAvatar // 先使用原始URL }); // 🔥 异步缓存头像,不阻塞UI imageCacheManager.cacheAvatar(targetAvatar).then(cachedAvatar => { this.setData({ targetAvatar: cachedAvatar }); console.log('✅ 对方头像缓存完成'); }).catch(error => { console.error('❌ 对方头像缓存失败:', error); }); } else { console.warn('⚠️ 获取对方用户信息失败:', response?.message); } } catch (error) { console.error('❌ 获取对方用户信息异常:', 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; console.log('从本地存储恢复用户信息'); } } catch (error) { console.error('从本地存储获取用户信息失败:', error); } } // 确保有用户信息 if (userInfo && userInfo.token) { // 正确获取用户头像 const userAvatar = userInfo.user?.avatar || userInfo.avatar || ''; console.log('👤 设置用户头像:', userAvatar); console.log('👤 用户信息结构:', userInfo); // 🔥 先设置用户信息,头像缓存异步执行 this.setData({ userInfo: userInfo, userAvatar: userAvatar // 先使用原始URL }); // 🔥 异步缓存头像,不阻塞UI imageCacheManager.cacheAvatar(userAvatar).then(cachedAvatar => { this.setData({ userAvatar: cachedAvatar }); console.log('✅ 用户头像缓存完成'); }).catch(error => { console.error('❌ 用户头像缓存失败:', error); }); console.log('用户信息初始化成功:', userInfo.user?.customId || userInfo.customId); } else { console.error('用户信息初始化失败,跳转到登录页'); wx.reLaunch({ url: '/pages/login/login' }); } }, // 初始化WebSocket initWebSocket() { console.log('🔌 初始化WebSocket连接...'); // 🔥 禁用WebSocket诊断,避免创建额外连接 // this.runWebSocketDiagnostic(); // 获取用户token const token = this.data.userInfo?.token || wx.getStorageSync('token'); if (!token) { console.error('❌ 用户token不存在,无法连接WebSocket'); return; } // 设置WebSocket管理器的token wsManager.setToken(token); // 连接WebSocket wsManager.connect().then(() => { console.log('✅ WebSocket连接成功'); // 监听新消息 wsManager.on('new_message', (messageData) => { console.log('📨 收到新消息:', messageData); this.handleNewMessage(messageData); }); // 监听消息状态更新(兼容 data 包裹与直传两种格式) wsManager.on('message_status', (statusMsg) => { const payload = statusMsg && statusMsg.data ? statusMsg.data : statusMsg; console.log('📊 消息状态更新:', payload); this.updateMessageStatus(payload); }); // 监听服务端发送回执,使用clientTempId替换临时消息 wsManager.on('message_sent', async (sentData) => { try { const srv = sentData?.data || {}; const formatted = await this.formatMessage(srv); let replaced = false; const newMessages = this.data.messages.map(msg => { if (!replaced && msg.isSelf && (msg.deliveryStatus === 'sending' || msg.deliveryStatus === 'uploading')) { const matchByTempId = srv.clientTempId && msg.messageId === srv.clientTempId; if (matchByTempId) { replaced = true; return { ...formatted, deliveryStatus: 'sent' }; } } return msg; }); this.setMessagesWithDate(replaced ? newMessages : this.data.messages.concat([{ ...formatted, deliveryStatus: 'sent' }])); setTimeout(() => this.scrollToBottom(), 100); } catch (e) { console.error('❌ 处理message_sent回执失败:', e); } }); // 监听服务端发送回执,用服务端消息替换临时消息,避免重复 wsManager.on('message_sent', async (sentData) => { try { // sentData.data 内应包含服务端消息详情 const srv = sentData?.data || {}; const formatted = await this.formatMessage(srv); // 查找最近一条同类型、自己发送、且仍为sending/上传完成的临时消息进行替换 let replaced = false; const newMessages = this.data.messages.map(msg => { if (!replaced && msg.isSelf && (msg.msgType === formatted.msgType) && (msg.deliveryStatus === 'sending' || msg.deliveryStatus === 'uploading')) { // 文件/图片等有可能通过content.url对齐 const sameUrl = (typeof msg.content === 'object' ? msg.content.url : msg.content) === (typeof formatted.content === 'object' ? formatted.content.url : formatted.content); // 近5分钟内的临时消息 const closeTime = Math.abs((msg.timestamp || 0) - (formatted.timestamp || 0)) < 5 * 60 * 1000; if (sameUrl || closeTime) { replaced = true; return { ...formatted, deliveryStatus: 'sent' }; } } return msg; }); // 如果未替换,直接追加(兜底) this.setMessagesWithDate(replaced ? newMessages : newMessages.concat([{ ...formatted, deliveryStatus: 'sent' }])); // 滚动到底 setTimeout(() => this.scrollToBottom(), 100); } catch (e) { console.error('❌ 处理message_sent回执失败:', e); } }); // 监听消息撤回 wsManager.on('message_recalled', (recallData) => { console.log('↩️ 收到消息撤回通知:', recallData); this.handleMessageRecalled(recallData); }); // 监听连接状态变化 wsManager.on('connection_status', (status) => { console.log('🔌 WebSocket连接状态变化:', status); this.setData({ wsConnected: status.connected }); }); }).catch((error) => { console.error('❌ WebSocket连接失败:', error); this.setData({ wsConnected: false }); }); }, // 🔥 运行WebSocket连接全面诊断 async runWebSocketDiagnostic() { console.log('🔍 开始WebSocket连接全面诊断...'); try { const results = await wsdiagnostic.runFullDiagnostic(); // 检查诊断结果 const wsResult = results.find(r => r.test === 'WebSocket连接'); if (wsResult && wsResult.status === 'success') { console.log('✅ WebSocket连接诊断通过,可以正常连接'); } else { console.error('❌ WebSocket连接诊断失败,请查看上方的详细报告'); } } catch (error) { console.error('❌ WebSocket诊断异常:', error); } }, // 处理新消息 async handleNewMessage(messageData) { console.log('🔍 收到新消息,开始处理:', messageData); console.log('🔍 当前会话ID:', this.data.conversationId); console.log('🔍 消息会话ID:', messageData.conversationId); // 检查是否是当前会话的消息 if (messageData.conversationId !== this.data.conversationId) { console.log('⚠️ 消息会话ID不匹配,跳过处理'); console.log('⚠️ 期望:', this.data.conversationId); console.log('⚠️ 实际:', messageData.conversationId); return; } console.log('✅ 消息会话ID匹配,开始格式化消息'); const newMessage = await this.formatMessage(messageData); console.log('✅ 消息格式化完成:', newMessage); const messages = [...this.data.messages, newMessage]; this.setMessagesWithDate(messages); console.log('✅ 消息已添加到列表,当前消息数:', messages.length); // 🔥 确保新消息后滚动到底部 setTimeout(() => { this.scrollToBottom(); }, 100); // 发送已读回执 this.sendReadReceipt(messageData.messageId); }, // 更新消息状态(兼容多格式payload) updateMessageStatus(statusData) { try { const payload = statusData || {}; const messageId = payload.messageId || payload.id || payload.msgId; // 可能是 data.status 或 payload.status 或 payload.state const rawStatus = payload.status !== undefined ? payload.status : (payload.state !== undefined ? payload.state : payload.deliveryStatus); // 将后端状态统一映射为前端 deliveryStatus let mapped = this.convertStatus(rawStatus); // 显式错误态 if (rawStatus === 'error' || rawStatus === 'fail' || rawStatus === 'failed') { mapped = 'failed'; } if (!messageId) { console.warn('⚠️ message_status 缺少 messageId,忽略:', payload); return; } const messages = this.data.messages.map(msg => { if (msg.messageId === messageId) { // 仅对自己发送的消息展示状态;他人消息不显示 deliveryStatus if (msg.isSelf) { return { ...msg, deliveryStatus: mapped }; } } return msg; }); this.setData({ messages }); } catch (e) { console.error('❌ 更新消息状态失败:', e, statusData); } }, // 处理收到的消息撤回通知 handleMessageRecalled(recallData) { const { messageId, senderName } = recallData; const messages = this.data.messages.map(msg => { if (msg.messageId === messageId) { return { ...msg, isRecalled: true, content: msg.isSelf ? '你撤回了一条消息' : `${senderName || '对方'}撤回了一条消息`, originalContent: msg.content, // 保存原始内容 recallTime: Date.now(), msgType: 'system' // 改为系统消息样式 }; } return msg; }); this.setMessagesWithDate(messages); }, // 加载消息 - 使用正确的API接口和分页逻辑 async loadMessages(isLoadMore = false) { try { console.log('📨 加载聊天消息:', { isLoadMore, targetId: this.data.targetId, chatType: this.data.chatType, conversationId: this.data.conversationId, lastMessageId: this.data.lastMessageId }); // 检查必要参数 if (!this.data.targetId) { console.log('⚠️ 缺少targetId,无法加载消息'); return; } // 🔥 避免重复请求 if (this.data.loadingMessages) { console.log('⚠️ 消息正在加载中,跳过重复请求'); console.log('🔍 当前loadingMessages状态:', this.data.loadingMessages); return; } // 设置加载状态 this.setData({ loadingMessages: true }); console.log('🔧 loadMessages设置loadingMessages: true'); // 🔥 使用正确的API接口:GET /api/v1/chat/history const params = { receiverId: this.data.targetId, chatType: this.data.chatType, limit: 20, direction: 'before' }; // 如果是加载更多,添加lastMsgId参数 if (isLoadMore && this.data.lastMessageId) { params.lastMsgId = this.data.lastMessageId; } console.log('📡 API请求参数:', params); // 调用API const response = await apiClient.get(`/api/v1/chat/history`, params); if (response && response.code === 0) { const newMessages = response.data || []; console.log('✅ 获取到消息:', newMessages.length, '条'); // 🔥 异步格式化消息(包括头像缓存) const formattedMessages = await Promise.all(newMessages.map(msg => this.formatMessage(msg))); // 🔥 判断是否是首次加载 const isFirstLoad = this.data.messages.length === 0; // 🔥 修正hasMore逻辑:如果返回的消息数等于请求数,说明可能还有更多 const hasMoreMessages = newMessages.length === 20; console.log('📊 hasMore逻辑:', { returnedCount: newMessages.length, requestedLimit: 20, hasMoreMessages, isFirstLoad }); // 🔥 去重处理:避免重复消息 const existingIds = new Set(this.data.messages.map(msg => msg.messageId)); const uniqueNewMessages = formattedMessages.filter(msg => !existingIds.has(msg.messageId)); console.log('🔄 去重后新消息数量:', uniqueNewMessages.length, '原数量:', formattedMessages.length); // 🔥 修复消息排序:确保消息按时间正序排列(旧消息在前,新消息在后) let sortedMessages; if (isFirstLoad) { // 首次加载:直接使用API返回的消息,按时间排序 sortedMessages = uniqueNewMessages.sort((a, b) => a.timestamp - b.timestamp); console.log('📅 首次加载,按时间排序消息'); } else { // 加载更多历史消息:新加载的历史消息应该插入到现有消息前面 const allMessages = uniqueNewMessages.concat(this.data.messages); sortedMessages = allMessages.sort((a, b) => a.timestamp - b.timestamp); console.log('📅 加载更多消息,重新排序所有消息'); } // 🔥 更新lastMessageId:使用最早的消息ID(用于下次分页) const earliestMessage = newMessages.length > 0 ? newMessages[newMessages.length - 1] : null; const newLastMessageId = earliestMessage ? (earliestMessage.id || earliestMessage.messageId) : ''; const annotated = this.recomputeDateDividers(sortedMessages); this.setData({ messages: annotated, hasMore: hasMoreMessages, lastMessageId: newLastMessageId, loadingMessages: false }); console.log('🔧 重置loadingMessages: false'); console.log('✅ 消息加载完成:', { totalMessages: sortedMessages.length, hasMore: hasMoreMessages, lastMessageId: newLastMessageId }); // 🔥 首次加载时自动滚动到底部 if (isFirstLoad && this.data.messages.length > 0) { console.log('📍 首次加载,自动滚动到最新消息'); setTimeout(() => { this.scrollToBottom(); }, 100); } } else { console.warn('⚠️ 获取消息响应异常:', response); this.setData({ hasMore: false, loadingMessages: false }); console.log('🔧 异常情况重置loadingMessages: false'); } } catch (error) { console.error('❌ 加载消息失败:', error); this.setData({ hasMore: false, loadingMessages: false }); console.log('🔧 错误情况重置loadingMessages: false'); wx.showToast({ title: '加载消息失败', icon: 'none' }); } }, // 格式化消息 - 适配接口文档的数据结构,特殊处理图片消息 async formatMessage(msgData) { // 🔍 调试:查看消息数据中的头像字段 console.log('🔍 消息数据字段检查:', { senderAvatar: msgData.senderAvatar, avatar: msgData.avatar, userAvatar: msgData.userAvatar, targetAvatar: msgData.targetAvatar, fromAvatar: msgData.fromAvatar, allFields: Object.keys(msgData) }); // 🔥 多重获取当前用户ID,确保能正确获取 let currentUserId = this.data.userInfo?.user?.customId || this.data.userInfo?.customId || ''; // 🔥 如果从data中获取不到,尝试从全局获取 if (!currentUserId) { const app = getApp(); const globalUserInfo = app.globalData.userInfo; currentUserId = globalUserInfo?.user?.customId || globalUserInfo?.customId || ''; console.log('🔍 从全局获取用户ID:', currentUserId); } // 🔥 如果还是获取不到,尝试从本地存储获取 if (!currentUserId) { try { const storedUserInfo = wx.getStorageSync('userInfo'); currentUserId = storedUserInfo?.user?.customId || storedUserInfo?.customId || ''; console.log('🔍 从本地存储获取用户ID:', currentUserId); } catch (error) { console.error('从本地存储获取用户ID失败:', error); } } // 适配接口文档中的字段名 const senderId = msgData.senderId || msgData.senderCustomId; const messageId = msgData.id || msgData.messageId; const isSelf = senderId === currentUserId; // 🔥 调试:检查发送者判断逻辑 console.log('🔍 发送者判断调试:', { currentUserId, senderId, messageId, isSelf, userInfo: this.data.userInfo, msgData: { senderId: msgData.senderId, senderCustomId: msgData.senderCustomId, id: msgData.id, messageId: msgData.messageId } }); // 🔥 智能消息类型识别:如果msgType是1但内容是文字,自动识别为文字消息 let msgType = this.convertMsgType(msgData.msgType); if (msgType === 'image' && typeof msgData.content === 'string' && !msgData.content.startsWith('http') && !msgData.content.startsWith('data:')) { console.log('🔧 检测到错误的图片类型,自动修正为文字消息'); msgType = 'text'; } // 🔥 特殊处理媒体消息内容 let processedContent = msgData.content; if (msgType === 'image') { processedContent = await this.parseImageContent(msgData.content); console.log('🖼️ 图片消息处理结果:', processedContent); } else if (msgType === 'voice') { processedContent = this.parseVoiceContent(msgData.content); console.log('🎤 语音消息处理结果:', processedContent); } else if (msgType === 'file') { processedContent = this.parseFileContent(msgData.content); console.log('📄 文件消息处理结果:', processedContent); } else if (msgType === 'location') { processedContent = this.parseLocationContent(msgData.content); console.log('📍 位置消息处理结果:', processedContent); } // 🔥 统一时间戳字段,确保排序正确 const timestamp = new Date(msgData.sendTime).getTime(); // 🔥 先使用默认头像,异步更新 let senderAvatar = ''; if (isSelf) { senderAvatar = this.data.userAvatar || ''; } else { senderAvatar = this.data.targetAvatar || ''; } // 🔥 异步提取和缓存头像 this.extractSenderAvatar(msgData, isSelf).then(cachedAvatar => { // 更新对应消息的头像 const messages = this.data.messages.map(msg => { if (msg.messageId === messageId) { return { ...msg, senderAvatar: cachedAvatar }; } return msg; }); this.setData({ messages }); }).catch(error => { console.error('提取头像失败:', error); }); const baseMessage = { messageId: messageId, senderId: senderId, targetId: msgData.receiverId || msgData.targetId, content: processedContent, // 使用处理后的内容 msgType: msgType, sendTime: timestamp, // 确保是时间戳格式 timestamp: timestamp, // 🔥 添加timestamp字段用于排序 isSelf: isSelf, senderName: msgData.senderName || (isSelf ? '我' : '对方'), senderAvatar: senderAvatar, // 🔥 使用异步提取的头像 deliveryStatus: this.convertStatus(msgData.status), // 转换状态 bubbleTime: this.formatBubbleTime(timestamp) }; // 文件消息补充文件展示字段,便于WXML直接使用 if (msgType === 'file') { return { ...baseMessage, fileName: processedContent.fileName || msgData.fileName || '', fileSize: this.formatFileSize(processedContent.fileSize || msgData.fileSize || 0), fileType: processedContent.fileType || msgData.fileType || '' }; } // 🔥 位置消息:为兼容旧渲染逻辑,将名称/地址/经纬度同步到根级字段,避免显示默认“位置” if (msgType === 'location') { return { ...baseMessage, locationName: processedContent?.locationName || msgData.locationName || '位置', locationAddress: processedContent?.address || msgData.locationAddress || '', latitude: processedContent?.latitude, longitude: processedContent?.longitude }; } return baseMessage; }, // 🔥 解析语音内容:兼容字符串URL、JSON字符串、对象三种情况 parseVoiceContent(content) { try { if (!content) return { url: '' }; // 如果已经是对象且包含url,直接返回 if (typeof content === 'object' && content !== null) { if (content.url) return content; // 兼容部分后端字段 if (content.voiceUrl) return { url: content.voiceUrl, duration: content.duration }; // 无法识别,返回空URL,避免崩溃 return { url: '' }; } // 如果是字符串,尝试JSON解析 if (typeof content === 'string') { const trimmed = content.trim(); if (trimmed.startsWith('{') && trimmed.endsWith('}')) { try { const obj = JSON.parse(trimmed); if (obj && obj.url) return obj; // 兼容 file_url 字段 if (obj && obj.file_url) return { url: obj.file_url, duration: obj.duration }; } catch (e) { // 继续按URL兜底 } } // 直接当作URL使用 return { url: trimmed }; } return { url: '' }; } catch (error) { console.error('❌ 解析语音内容失败:', error, content); return { url: '' }; } }, // 🔥 解析文件内容:兼容URL字符串、JSON字符串、对象 parseFileContent(content) { try { if (!content) return { url: '', fileName: '', fileSize: 0 }; // 对象格式 if (typeof content === 'object' && content !== null) { const url = content.url || content.file_url || content.fileUrl || ''; const fileName = content.fileName || content.file_name || ''; const fileSize = content.fileSize || content.file_size || 0; const fileType = content.fileType || content.file_type || ''; return { url, fileName, fileSize, fileType }; } // 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.file_url || obj.fileUrl || ''; const fileName = obj.fileName || obj.file_name || ''; const fileSize = obj.fileSize || obj.file_size || 0; const fileType = 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 }; } }, // 🔥 解析位置内容:兼容JSON字符串或对象,统一为 {latitude, longitude, address, locationName} parseLocationContent(content) { try { if (!content) return null; let obj = content; 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; } } const lat = obj.latitude ?? obj.lat; const lng = obj.longitude ?? obj.lng; const address = obj.address || ''; const locationName = obj.locationName || obj.name || '位置'; if (lat == null || lng == null) return null; return { latitude: Number(lat), longitude: Number(lng), address, locationName }; } catch (err) { console.error('❌ 解析位置内容失败:', err, content); return null; } }, // 🔥 提取发送者头像 - 兼容多种字段名,支持缓存 async extractSenderAvatar(msgData, isSelf) { console.log('🔍 开始提取头像,isSelf:', isSelf, 'msgData:', msgData); // 如果是自己的消息,使用当前用户头像(已缓存) if (isSelf) { const userAvatar = this.data.userAvatar || ''; console.log('👤 使用当前用户头像:', userAvatar); return userAvatar; } // 🔥 如果是对方的消息,优先使用获取到的对方头像(已缓存) const targetAvatar = this.data.targetAvatar || ''; if (targetAvatar) { console.log('👥 使用获取到的对方头像:', targetAvatar); return targetAvatar; } // 🔥 尝试多种可能的头像字段名,兼容不同API返回格式 const possibleAvatarFields = [ 'senderAvatar', // 标准字段 'avatar', // 简化字段 'userAvatar', // 用户头像 'targetAvatar', // 目标头像(与消息列表一致) 'fromAvatar', // 发送者头像 'user.avatar', // 嵌套用户对象 'sender.avatar' // 嵌套发送者对象 ]; console.log('🔍 尝试查找头像字段,可能的字段:', possibleAvatarFields); for (const field of possibleAvatarFields) { let value; // 处理嵌套字段 if (field.includes('.')) { const parts = field.split('.'); value = msgData; for (const part of parts) { value = value?.[part]; if (!value) break; } } else { value = msgData[field]; } console.log(`🔍 检查字段 ${field}:`, value); // 如果找到有效的头像URL,缓存并返回 if (value && typeof value === 'string' && value.length > 0) { console.log(`🎯 找到头像字段 ${field}:`, value); // 🔥 缓存头像 const cachedAvatar = await imageCacheManager.cacheAvatar(value); return cachedAvatar; } } console.log('⚠️ 未找到有效的头像字段,消息数据:', msgData); console.log('⚠️ 消息数据的所有字段:', Object.keys(msgData)); return ''; // 没有找到头像,返回空字符串 }, // 转换消息类型(根据后端实际常量定义) convertMsgType(apiMsgType) { // 🔥 根据API文档修正映射 const typeMap = { 0: 'text', // 文字消息 1: 'image', // 图片消息 2: 'voice', // 语音消息 3: 'video', // 视频消息 4: 'file', // 文件消息 5: 'location', // 位置消息 6: 'emoji', // 表情消息 99: 'system' // 系统消息 }; console.log('🔄 转换msgType:', apiMsgType, '->', typeMap[apiMsgType] || 'text'); return typeMap[apiMsgType] || 'text'; }, // 转换消息状态(映射为前端deliveryStatus:sending/sent/delivered/read/...) convertStatus(apiStatus) { // 后端状态(根据文档):0=未读(unread),1=已读(read),2=撤回(recalled),3=删除(deleted) // 前端 deliveryStatus 需要的是 sent/delivered/read 等 const normalize = (val) => { if (val === undefined || val === null) return 'sent'; // 数字状态 if (typeof val === 'number') { if (val === 1) return 'read'; if (val === 0) return 'delivered'; if (val === 2) return 'recalled'; if (val === 3) return 'deleted'; return 'sent'; } // 字符串状态 const s = String(val).toLowerCase(); if (s === 'read') return 'read'; if (s === 'unread') return 'delivered'; if (s === 'delivered' || s === 'sent') return s; if (s === 'recalled' || s === 'deleted' || s === 'failed') return s; return 'sent'; }; const mapped = normalize(apiStatus); console.log('🔄 状态映射 convertStatus:', apiStatus, '->', mapped); return mapped; }, // 🔥 新增:解析图片内容(支持OSS URL和Base64两种格式),支持缓存 async parseImageContent(content) { console.log('🖼️ 解析图片内容 - 完整内容:', content); console.log('🖼️ 内容类型:', typeof content); console.log('🖼️ 内容长度:', content?.length); if (!content) { console.log('⚠️ 图片内容为空'); return { type: 'url', url: '' }; } // 🔥 检查是否是错误的文本内容(应该是图片URL) if (typeof content === 'string' && !content.startsWith('http') && !content.startsWith('data:')) { console.error('❌ 图片消息包含无效内容,可能是后端数据错误:', content); return { type: 'error', url: '', error: '图片内容无效', originalContent: content }; } // 检查是否是Base64格式 if (content.startsWith('data:image/')) { console.log('📷 检测到Base64图片格式'); return { type: 'base64', url: content // Base64数据可以直接作为src使用 }; } // 检查是否是OSS URL格式 if (content.startsWith('http://') || content.startsWith('https://')) { console.log('🌐 检测到OSS URL图片格式'); // 🔥 缓存聊天图片 const cachedUrl = await imageCacheManager.preloadImage(content); return { type: 'url', url: cachedUrl }; } // 尝试解析JSON格式(可能包含文件信息) try { const parsed = JSON.parse(content); if (parsed.file_url) { console.log('📄 检测到JSON图片格式'); // 🔥 缓存聊天图片 const cachedUrl = await imageCacheManager.preloadImage(parsed.file_url); return { type: 'url', url: cachedUrl, fileName: parsed.file_name, fileSize: parsed.file_size, fileType: parsed.file_type }; } } catch (e) { console.log('⚠️ JSON解析失败,当作普通URL处理'); } // 默认当作URL处理 return { type: 'url', url: content }; }, // 判断是否显示时间 shouldShowTime(timestamp) { // 简单实现:每5分钟显示一次时间 const lastMessage = this.data.messages[this.data.messages.length - 1]; if (!lastMessage) return true; const diff = timestamp - lastMessage.sendTime; return diff > 5 * 60 * 1000; // 5分钟 }, // 气泡内时间 HH:mm formatBubbleTime(timestamp) { const d = new Date(timestamp); const hh = d.getHours().toString().padStart(2, '0'); const mm = d.getMinutes().toString().padStart(2, '0'); return `${hh}:${mm}`; }, // 日期标签:今天/昨天/YYYY/MM/DD formatDateLabel(timestamp) { const d = new Date(timestamp); const today = new Date(); const yst = new Date(); yst.setDate(today.getDate() - 1); const sameDay = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); if (sameDay(d, today)) return '今天'; if (sameDay(d, yst)) return '昨天'; const y = d.getFullYear(); const m = (d.getMonth() + 1).toString().padStart(2, '0'); const day = d.getDate().toString().padStart(2, '0'); return `${y}/${m}/${day}`; }, // 重新计算日期分隔(仅按日期),返回新数组 recomputeDateDividers(list) { if (!Array.isArray(list)) return []; let lastDateKey = ''; return list.map((m, idx) => { const d = new Date(m.timestamp || m.sendTime || 0); const key = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`; const show = key !== lastDateKey; const dateText = this.formatDateLabel(d.getTime()); lastDateKey = key; return { ...m, showDateDivider: show, dateText, bubbleTime: m.bubbleTime || this.formatBubbleTime(m.timestamp || m.sendTime || Date.now()) }; }); }, // 便捷设置消息并自动打日期分隔 setMessagesWithDate(messages) { const annotated = this.recomputeDateDividers(messages); this.setData({ messages: annotated }); }, // 输入变化 onInputChange(e) { const inputText = e.detail.value; this.setData({ inputText }); // 如果正在输入中文,不要发送输入状态 if (!this.data.isComposing) { // 发送输入状态 if (inputText.length > 0) { this.sendTypingStatus(true); } else { this.sendTypingStatus(false); } } }, // 处理中文输入法开始 onCompositionStart(e) { this.setData({ isComposing: true }); }, // 处理中文输入法结束 onCompositionEnd(e) { this.setData({ isComposing: false }); // 输入法结束后,更新输入状态 const inputText = this.data.inputText; if (inputText.length > 0) { this.sendTypingStatus(true); } else { this.sendTypingStatus(false); } }, // 处理文本框行数变化 onLineChange(e) { console.log('📝 文本框行数变化:', e.detail); // 可以在这里处理输入框高度变化的逻辑 // 例如调整页面布局或滚动位置 }, // 🔥 ===== 媒体选择功能 ===== // 显示媒体选择菜单 showMediaPicker() { console.log('📱 显示媒体选择菜单'); wx.showActionSheet({ itemList: ['拍照', '拍摄视频'], success: (res) => { switch (res.tapIndex) { case 0: this.takePhoto(); break; case 1: this.recordVideo(); break; } }, fail: (error) => { if (error.errMsg !== 'showActionSheet:fail cancel') { console.error('❌ 显示媒体选择菜单失败:', error); } } }); }, // 拍照 async takePhoto() { console.log('📸 开始拍照'); try { const result = await mediaPicker.chooseImages({ count: 1, sourceType: ['camera'] }); if (result.success && result.files.length > 0) { await this.sendImageMessage(result.files[0]); } } catch (error) { console.error('❌ 拍照失败:', error); wx.showToast({ title: '拍照失败', icon: 'none' }); } }, // 从相册选择图片 async chooseImageFromAlbum() { console.log('🖼️ 打开相册选择菜单'); try { // 在相册内二次分流:图片 或 视频 const tapIndex = await new Promise((resolve) => { wx.showActionSheet({ itemList: ['从相册选择图片', '从相册选择视频'], success: (res) => resolve(res.tapIndex), fail: (err) => { if (err.errMsg !== 'showActionSheet:fail cancel') { console.error('❌ 显示相册选择菜单失败:', err); } resolve(-1); } }); }); if (tapIndex === 0) { // 选择图片 const result = await mediaPicker.chooseImages({ count: 9, sourceType: ['album'] }); if (result.success && Array.isArray(result.files) && result.files.length > 0) { for (const image of result.files) { await this.sendImageMessage(image); } } } else if (tapIndex === 1) { // 选择视频 const result = await mediaPicker.chooseVideo({ sourceType: ['album'], maxDuration: 60 }); if (result.success && Array.isArray(result.files) && result.files.length > 0) { await this.sendVideoMessage(result.files[0]); } } } catch (error) { console.error('❌ 处理相册选择发生异常:', error); } }, // 拍摄视频 async recordVideo() { console.log('🎥 开始拍摄视频'); try { const result = await mediaPicker.chooseVideo({ sourceType: ['camera'], maxDuration: 60 }); if (result.success && result.files.length > 0) { await this.sendVideoMessage(result.files[0]); } } catch (error) { console.error('❌ 拍摄视频失败:', error); wx.showToast({ title: '拍摄视频失败', icon: 'none' }); } }, // 从相册选择视频 async chooseVideoFromAlbum() { console.log('🎞️ 从相册选择视频'); try { const result = await mediaPicker.chooseVideo({ sourceType: ['album'], maxDuration: 60 }); if (result.success && Array.isArray(result.files) && result.files.length > 0) { await this.sendVideoMessage(result.files[0]); } } catch (error) { console.error('❌ 选择视频失败:', error); wx.showToast({ title: '选择视频失败', icon: 'none' }); } }, // 🔥 头像加载错误处理 onAvatarError(e) { console.error('❌ 头像加载失败:', e.detail); console.log('❌ 失败的头像URL:', e.currentTarget.src); console.log('❌ 头像元素信息:', { src: e.currentTarget.src, dataset: e.currentTarget.dataset, id: e.currentTarget.id }); // 🔥 尝试从dataset中获取更多信息 const dataset = e.currentTarget.dataset; console.log('❌ 头像dataset信息:', dataset); }, // 🔥 头像加载成功处理 onAvatarLoad(e) { console.log('✅ 头像加载成功:', e.currentTarget.src); }, // 🔥 ===== 媒体消息发送功能 ===== // 发送图片消息 - 兼容媒体选择器 async sendImageMessage(imageFile) { console.log('📸 发送图片消息:', imageFile); // 预生成临时ID,便于在任何失败路径更新状态 const clientTempId = 'temp_' + Date.now(); try { // 如果传入的是字符串路径,转换为文件对象格式 if (typeof imageFile === 'string') { await this.uploadAndSendImage(imageFile); return; } // 检查用户信息 if (!this.data.userInfo) { console.error('❌ 用户信息不存在,无法发送图片'); return; } // 检查文件大小 if (imageFile.size > 10 * 1024 * 1024) { // 10MB限制 wx.showToast({ title: '图片大小不能超过10MB', icon: 'none' }); return; } // 获取用户ID const currentUserId = this.data.userInfo.user?.customId || this.data.userInfo.customId || ''; if (!currentUserId) { console.error('无法获取用户ID'); wx.showToast({ title: '用户ID错误', icon: 'none' }); return; } // 创建临时图片消息 const timestamp = Date.now(); const tempMessage = { messageId: clientTempId, senderId: currentUserId, targetId: this.data.targetId, content: { type: 'url', url: imageFile.path // 先使用本地路径显示 }, msgType: 'image', sendTime: timestamp, timestamp: timestamp, isSelf: true, senderName: '我', senderAvatar: this.data.userAvatar || '', deliveryStatus: 'sending', bubbleTime: this.formatBubbleTime(timestamp) }; // 立即添加到消息列表 const messages = this.data.messages.concat([tempMessage]); this.setMessagesWithDate(messages); // 滚动到底部 setTimeout(() => { this.scrollToBottom(); }, 100); wx.showLoading({ title: '发送中...' }); // 上传图片到服务器 const uploadResult = await apiClient.uploadFile(imageFile.path, 'image', 'message'); if (uploadResult && uploadResult.code === 0) { const imageUrl = uploadResult.data.file_url || uploadResult.data.url; console.log('📸 获取到图片URL:', imageUrl); if (!imageUrl) { throw new Error('上传成功但未获取到图片URL'); } // 更新临时消息的图片URL const updatedMessages = this.data.messages.map(msg => { if (msg.messageId === tempMessage.messageId) { return { ...msg, content: { type: 'url', url: imageUrl }, // 上传成功,仅标记为sending,待WebSocket回执更新为sent deliveryStatus: 'sending' }; } return msg; }); this.setData({ messages: updatedMessages }); // 🔥 发送图片消息 - 只使用WebSocket const success = wsManager.sendChatMessage( this.data.targetId, imageUrl, 'image', this.data.chatType ); if (!success) { this.updateMessageDeliveryStatus(tempMessage.messageId, 'failed'); throw new Error('WebSocket连接异常,图片发送失败'); } wx.hideLoading(); } else { this.updateMessageDeliveryStatus(tempMessage.messageId, 'failed'); throw new Error(uploadResult?.message || '图片上传失败'); } } catch (error) { wx.hideLoading(); console.error('❌ 发送图片消息失败:', error); // 标记为失败 this.updateMessageDeliveryStatus(clientTempId, 'failed'); wx.showToast({ title: '图片发送失败', icon: 'none' }); } }, // 发送视频消息 - 兼容媒体选择器 async sendVideoMessage(videoFile) { console.log('🎥 发送视频消息:', videoFile); const clientTempId = 'temp_' + Date.now(); try { // 如果传入的是字符串路径,使用原有方法 if (typeof videoFile === 'string') { await this.uploadAndSendVideo(videoFile); return; } // 检查用户信息 if (!this.data.userInfo) { console.error('❌ 用户信息不存在,无法发送视频'); return; } // 检查文件大小 if (videoFile.size > 100 * 1024 * 1024) { // 100MB限制 wx.showToast({ title: '视频大小不能超过100MB', icon: 'none' }); return; } // 获取用户ID const currentUserId = this.data.userInfo.user?.customId || this.data.userInfo.customId || ''; if (!currentUserId) { console.error('无法获取用户ID'); wx.showToast({ title: '用户ID错误', icon: 'none' }); return; } // 创建临时视频消息 const timestamp = Date.now(); const tempMessage = { messageId: clientTempId, senderId: currentUserId, targetId: this.data.targetId, content: videoFile.path, // 先使用本地路径显示 msgType: 'video', sendTime: timestamp, timestamp: timestamp, isSelf: true, senderName: '我', senderAvatar: this.data.userAvatar || '', deliveryStatus: 'sending', bubbleTime: this.formatBubbleTime(timestamp) }; // 立即添加到消息列表 const messages = this.data.messages.concat([tempMessage]); this.setMessagesWithDate(messages); // 滚动到底部 setTimeout(() => { this.scrollToBottom(); }, 100); wx.showLoading({ title: '发送中...' }); // 上传视频到服务器 const uploadResult = await apiClient.uploadFile(videoFile.path, 'video', 'message'); if (uploadResult && uploadResult.code === 0) { const videoUrl = uploadResult.data.file_url || uploadResult.data.url; console.log('🎥 获取到视频URL:', videoUrl); if (!videoUrl) { throw new Error('上传成功但未获取到视频URL'); } // 更新临时消息的视频URL const updatedMessages = this.data.messages.map(msg => { if (msg.messageId === tempMessage.messageId) { return { ...msg, content: videoUrl, // 上传成功,等待WS回执 deliveryStatus: 'sending' }; } return msg; }); this.setData({ messages: updatedMessages }); // 🔥 发送视频消息 - 只使用WebSocket const success = wsManager.sendChatMessage( this.data.targetId, videoUrl, 'video', this.data.chatType ); if (!success) { this.updateMessageDeliveryStatus(tempMessage.messageId, 'failed'); throw new Error('WebSocket连接异常,视频发送失败'); } wx.hideLoading(); } else { throw new Error(uploadResult?.message || '视频上传失败'); } } catch (error) { wx.hideLoading(); console.error('❌ 发送视频消息失败:', error); this.updateMessageDeliveryStatus(clientTempId, 'failed'); wx.showToast({ title: '视频发送失败', icon: 'none' }); } }, // 🔥 ===== 消息发送功能 ===== // 发送文本消息 - 优化新会话处理 async sendTextMessage() { const content = this.data.inputText.trim(); if (!content) return; // 检查用户信息 if (!this.data.userInfo) { console.error('用户信息不存在,无法发送消息'); wx.showToast({ title: '用户信息错误', icon: 'none' }); return; } // 清空输入框 this.setData({ inputText: '', showEmojiPanel: false, showMorePanel: false }); // 停止输入状态 this.sendTypingStatus(false); // 获取用户ID const currentUserId = this.data.userInfo.user?.customId || this.data.userInfo.customId || ''; if (!currentUserId) { console.error('无法获取用户ID'); wx.showToast({ title: '用户ID错误', icon: 'none' }); return; } // 创建临时消息 const timestamp = Date.now(); const tempMessage = { messageId: 'temp_' + timestamp, senderId: currentUserId, targetId: this.data.targetId, content: content, msgType: 'text', sendTime: timestamp, timestamp: timestamp, // 🔥 添加timestamp字段确保排序正确 isSelf: true, senderName: '我', senderAvatar: this.data.userAvatar || '', // 🔥 使用用户头像,便于WXML判断 deliveryStatus: 'sending', bubbleTime: this.formatBubbleTime(timestamp) }; // 添加到消息列表 const messages = this.data.messages.concat([tempMessage]); this.setMessagesWithDate(messages); // 🔥 确保消息添加后立即滚动到底部 setTimeout(() => { this.scrollToBottom(); }, 100); try { // 🔥 检查WebSocket连接状态 console.log('📡 WebSocket连接状态:', wsManager.isConnected); console.log('📤 准备发送消息:', { targetId: this.data.targetId, content: content, msgType: 'text', chatType: this.data.chatType }); if (!wsManager.isConnected) { console.warn('⚠️ WebSocket未连接,尝试重新连接...'); // 🔥 使用事件监听方式等待连接 const connectPromise = new Promise((resolve, reject) => { const timeout = setTimeout(() => { wsManager.off('connected', onConnected); wsManager.off('error', onError); reject(new Error('WebSocket连接超时(15秒)')); }, 15000); // 增加到15秒超时 const onConnected = () => { clearTimeout(timeout); wsManager.off('connected', onConnected); wsManager.off('error', onError); console.log('✅ WebSocket连接成功'); resolve(); }; const onError = (error) => { clearTimeout(timeout); wsManager.off('connected', onConnected); wsManager.off('error', onError); console.error('❌ WebSocket连接失败:', error); reject(new Error('WebSocket连接失败')); }; // 监听连接事件 wsManager.on('connected', onConnected); wsManager.on('error', onError); // 如果已经连接,直接resolve if (wsManager.isConnected) { clearTimeout(timeout); resolve(); } else { // 开始连接 wsManager.connect(); } }); await connectPromise; } // 🔥 发送消息 - 只使用WebSocket,根据API文档要求 const success = wsManager.sendChatMessage( this.data.targetId, content, 'text', this.data.chatType ); if (!success) { // WebSocket发送失败,抛出错误 throw new Error('WebSocket发送消息失败'); } // 更新消息状态为已发送 this.updateMessageDeliveryStatus(tempMessage.messageId, 'sent'); // 🔥 如果是新会话,发送第一条消息后刷新会话列表获取正确ID if (this.data.isNewConversation) { console.log('🔄 新会话发送消息后,重新获取正确的conversationId...'); setTimeout(() => { this.refreshConversationId(); }, 1000); // 延迟1秒确保后端已处理完成 } } catch (error) { console.error('发送消息失败:', error); // 更新消息状态为失败 this.updateMessageDeliveryStatus(tempMessage.messageId, 'failed'); wx.showToast({ title: '发送失败', icon: 'none' }); } }, // 刷新conversationId(用于新会话发送消息后) async refreshConversationId() { try { const response = await apiClient.getConversations(); if (response && response.code === 0) { const conversations = response.data || []; // 查找与当前targetId的会话 const targetConversation = conversations.find(conv => conv.type === this.data.chatType && conv.targetId === this.data.targetId ); if (targetConversation && targetConversation.id !== this.data.conversationId) { console.log('✅ 更新会话ID:', this.data.conversationId, '->', targetConversation.id); this.setData({ conversationId: targetConversation.id, isNewConversation: false }); } } } catch (error) { console.error('❌ 刷新conversationId失败:', error); } }, // 更新消息发送状态 updateMessageDeliveryStatus(messageId, status) { const messages = this.data.messages.map(msg => { if (msg.messageId === messageId) { return { ...msg, deliveryStatus: status }; } return msg; }); this.setData({ messages }); }, // 发送输入状态 sendTypingStatus(isTyping) { wsManager.sendTypingStatus(this.data.conversationId, isTyping); }, // 发送已读回执 sendReadReceipt(messageId) { wsManager.sendReadReceipt(messageId); }, // 标记所有消息已读(进入聊天页面时调用) async markAllMessagesRead() { try { if (!this.data.conversationId || this.data.conversationId.trim() === '') { console.log('⚠️ 会话ID为空,跳过标记已读(可能是新会话)'); return; } console.log('📖 标记会话所有消息已读:', this.data.conversationId); // 🔥 使用正确的API方法名 const response = await apiClient.markAllRead(this.data.conversationId); // 🔥 兼容后端业务码:可能为 0 或 200 都表示成功 if (response && (response.code === 0 || response.code === 200)) { console.log('✅ 消息已全部标记已读'); // 更新本地消息状态 const updatedMessages = this.data.messages.map(msg => ({ ...msg, deliveryStatus: msg.isSelf ? msg.deliveryStatus : 'read' })); this.setData({ messages: updatedMessages }); } else { console.warn('⚠️ 标记已读响应异常:', response); } } catch (error) { console.error('❌ 标记消息已读失败:', error); // 🔥 不显示错误提示,避免影响用户体验 } }, // 标记消息已读(接收到新消息时调用) markMessagesRead() { if (this.data.messages.length > 0) { const lastMessageId = this.data.messages[this.data.messages.length - 1].messageId; this.sendReadReceipt(lastMessageId); } }, // 切换输入类型 toggleInputType() { const inputType = this.data.inputType === 'text' ? 'voice' : 'text'; this.setData({ inputType, showEmojiPanel: false, showMorePanel: false }); }, // 切换表情面板 toggleEmojiPanel() { this.setData({ showEmojiPanel: !this.data.showEmojiPanel, showMorePanel: false }); }, // 切换更多面板 toggleMorePanel() { this.setData({ showMorePanel: !this.data.showMorePanel, showEmojiPanel: false }); }, // 阻止事件冒泡 stopPropagation() { // 阻止事件冒泡,防止点击面板内容时关闭面板 }, // 🔥 新增:组件方法(不影响现有功能) // 媒体预览组件方法 closeMediaPreview() { this.setData({ mediaPreviewVisible: false, mediaPreviewList: [], currentMediaIndex: 0 }); }, onMediaIndexChange(e) { const { currentIndex } = e.detail; this.setData({ currentMediaIndex: currentIndex }); }, // 消息操作菜单组件方法 closeMessageAction() { this.setData({ messageActionVisible: false, currentMessage: null, isOwnMessage: false }); }, onMessageAction(e) { const { action, data } = e.detail; console.log('消息操作:', action, data); // 根据操作类型执行相应功能 switch (action) { case 'copy': this.copyMessage(this.data.currentMessage); break; case 'delete': this.deleteMessage(this.data.currentMessage); break; case 'recall': this.recallMessage(this.data.currentMessage); break; case 'reaction': // 处理表情回应 console.log('表情回应:', data); break; default: console.log('未处理的操作:', action); } this.closeMessageAction(); }, // @提醒选择器组件方法 closeMentionSelector() { this.setData({ mentionSelectorVisible: false }); }, onMentionSelect(e) { const { type, text, userIds } = e.detail; console.log('@提醒选择:', type, text, userIds); // 将@信息添加到输入框 if (type === 'all') { this.setData({ inputText: this.data.inputText + '@所有人 ' }); } else { this.setData({ inputText: this.data.inputText + `@${text} ` }); } this.closeMentionSelector(); }, // 🎤 ===== 语音消息功能 ===== // 显示语音录制界面(重命名避免与 data 字段同名造成绑定混淆) openVoiceRecorder() { console.log('🎤 显示语音录制界面'); this.setData({ showVoiceRecorder: true, showEmojiPanel: false, showMorePanel: false }); }, // 关闭语音录制界面 closeVoiceRecorder() { console.log('🎤 关闭语音录制界面'); this.setData({ showVoiceRecorder: false }); }, // 语音发送事件处理 onVoiceSend(e) { const voiceData = e.detail; console.log('🎤 发送语音消息:', voiceData); // 发送语音消息 this.sendVoiceMessage(voiceData); }, // 发送语音消息 async sendVoiceMessage(voiceData) { // 预分配临时ID,便于错误时标记 const clientTempId = 'temp_' + Date.now(); try { console.log('📤 发送语音消息:', voiceData); // 与文本/图片一致的临时消息结构 const timestamp = Date.now(); const currentUserId = this.data.userInfo?.user?.customId || this.data.userInfo?.customId || this.data.userInfo?.id || 'unknown'; const voiceMessage = { messageId: clientTempId, conversationId: this.data.conversationId, senderId: currentUserId, senderName: this.data.userInfo?.nickname || '我', senderAvatar: this.data.userAvatar || '', targetId: this.data.targetId, msgType: 'voice', content: { url: voiceData.url, duration: voiceData.duration, size: voiceData.size, tempFilePath: voiceData.tempFilePath }, sendTime: timestamp, timestamp: timestamp, isSelf: true, deliveryStatus: 'sending', bubbleTime: this.formatBubbleTime(timestamp) }; // 添加到消息列表并滚动到底 const messages = this.data.messages.concat([voiceMessage]); this.setMessagesWithDate(messages); setTimeout(() => { this.scrollToBottom(); }, 100); // 🔥 使用正确的WebSocket消息格式发送语音消息 const success = wsManager.sendChatMessage( this.data.targetId, JSON.stringify(voiceMessage.content), 'voice', this.data.chatType ); if (success) { this.updateMessageStatus({ messageId: voiceMessage.messageId, status: 'sent' }); console.log('✅ 语音消息发送成功'); } else { this.updateMessageStatus({ messageId: voiceMessage.messageId, status: 'failed' }); console.error('❌ 语音消息发送失败'); wx.showToast({ title: '发送失败', icon: 'none' }); } } catch (error) { console.error('❌ 发送语音消息失败:', error); // 标记失败 this.updateMessageDeliveryStatus(clientTempId, 'failed'); wx.showToast({ title: '发送失败', icon: 'none' }); } }, // 选择表情 selectEmoji(e) { const emoji = e.currentTarget.dataset.emoji; const inputText = this.data.inputText + emoji; this.setData({ inputText }); }, // 开始录音 startRecording(e) { console.log('开始录音'); this.setData({ recording: true }); // TODO: 实现录音功能 wx.showToast({ title: '录音功能开发中', icon: 'none' }); }, // 停止录音 stopRecording(e) { console.log('停止录音'); this.setData({ recording: false }); // TODO: 处理录音结果 }, // 取消录音 cancelRecording(e) { console.log('取消录音'); this.setData({ recording: false }); }, // 选择图片 selectImage() { wx.chooseImage({ count: 9, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success: (res) => { console.log('选择图片:', res); this.uploadAndSendImage(res.tempFilePaths[0]); } }); this.setData({ showMorePanel: false }); }, // 上传并发送图片 async uploadAndSendImage(imagePath) { try { // 获取用户ID const currentUserId = this.data.userInfo?.user?.customId || this.data.userInfo?.customId || ''; if (!currentUserId) { console.error('无法获取用户ID'); wx.showToast({ title: '用户ID错误', icon: 'none' }); return; } // 创建临时图片消息 const timestamp = Date.now(); const tempMessage = { messageId: 'temp_' + timestamp, senderId: currentUserId, targetId: this.data.targetId, content: { type: 'url', url: imagePath // 先使用本地路径显示 }, msgType: 'image', sendTime: timestamp, timestamp: timestamp, isSelf: true, senderName: '我', senderAvatar: this.data.userAvatar || '', deliveryStatus: 'sending', bubbleTime: this.formatBubbleTime(timestamp) }; // 立即添加到消息列表 const messages = this.data.messages.concat([tempMessage]); this.setMessagesWithDate(messages); // 滚动到底部 setTimeout(() => { this.scrollToBottom(); }, 100); wx.showLoading({ title: '发送中...' }); // 上传图片 const uploadResult = await apiClient.uploadFile(imagePath, 'image', 'message'); if (uploadResult && uploadResult.code === 0) { const imageUrl = uploadResult.data.file_url || uploadResult.data.url; console.log('📸 获取到图片URL:', imageUrl); if (!imageUrl) { throw new Error('上传成功但未获取到图片URL'); } // 更新临时消息的图片URL const updatedMessages = this.data.messages.map(msg => { if (msg.messageId === tempMessage.messageId) { return { ...msg, content: { type: 'url', url: imageUrl }, deliveryStatus: 'sending' }; } return msg; }); this.setData({ messages: updatedMessages }); // 🔥 发送图片消息 - 只使用WebSocket const success = wsManager.sendChatMessage( this.data.targetId, imageUrl, 'image', this.data.chatType ); if (!success) { this.updateMessageDeliveryStatus(tempMessage.messageId, 'failed'); throw new Error('WebSocket连接异常,图片发送失败'); } wx.hideLoading(); } else { throw new Error(uploadResult?.message || '图片上传失败'); } } catch (error) { wx.hideLoading(); console.error('发送图片失败:', error); // 兜底:尝试将最后一条sending的自发图片置为failed const lastIndex = [...this.data.messages].reverse().findIndex(m => m.isSelf && m.msgType === 'image' && m.deliveryStatus === 'sending'); if (lastIndex !== -1) { const idx = this.data.messages.length - 1 - lastIndex; const failed = this.data.messages.map((m, i) => i === idx ? { ...m, deliveryStatus: 'failed' } : m); this.setData({ messages: failed }); } wx.showToast({ title: '发送失败', icon: 'none' }); } }, // 选择视频 selectVideo() { wx.chooseVideo({ sourceType: ['album', 'camera'], maxDuration: 60, compressed: true, success: (res) => { console.log('选择视频:', res); // TODO: 实现视频上传和发送 wx.showToast({ title: '视频功能开发中', icon: 'none' }); } }); this.setData({ showMorePanel: false }); }, // 选择文件 selectFile() { // 先收起面板 this.setData({ showMorePanel: false }); try { wx.chooseMessageFile({ count: 1, type: 'file', success: async (res) => { if (!res || !res.tempFiles || res.tempFiles.length === 0) { wx.showToast({ title: '未选择文件', icon: 'none' }); return; } const file = res.tempFiles[0]; const filePath = file.path || file.tempFilePath; const fileName = file.name || '文件'; const fileSize = file.size || 0; const readableSize = this.formatFileSize(fileSize); // 可选:限制大小 100MB if (fileSize > 100 * 1024 * 1024) { wx.showToast({ title: '文件不能超过100MB', icon: 'none' }); return; } // 获取当前用户ID const currentUserId = this.data.userInfo?.user?.customId || this.data.userInfo?.customId || ''; if (!currentUserId) { wx.showToast({ title: '用户信息错误', icon: 'none' }); return; } // 创建临时消息 const timestamp = Date.now(); const clientTempId = 'temp_' + timestamp; const tempMessage = { messageId: clientTempId, senderId: currentUserId, targetId: this.data.targetId, content: { url: '' }, msgType: 'file', fileName: fileName, fileSize: readableSize, sendTime: timestamp, timestamp: timestamp, isSelf: true, senderName: '我', senderAvatar: this.data.userAvatar || '', deliveryStatus: 'sending', bubbleTime: this.formatBubbleTime(timestamp) }; this.setMessagesWithDate(this.data.messages.concat([tempMessage])); setTimeout(() => this.scrollToBottom(), 100); wx.showLoading({ title: '发送中...' }); try { // 上传文件 const uploadResult = await apiClient.uploadFile(filePath, 'file', 'message'); if (uploadResult && uploadResult.code === 0) { const fileUrl = uploadResult.data.file_url || uploadResult.data.url; if (!fileUrl) throw new Error('上传成功但未返回URL'); // 更新临时消息为已发送并写入URL const updatedMessages = this.data.messages.map(msg => { if (msg.messageId === tempMessage.messageId) { return { ...msg, content: { url: fileUrl }, deliveryStatus: 'sending' }; } return msg; }); this.setData({ messages: updatedMessages }); // 通过WebSocket发送 const payload = { url: fileUrl, file_name: fileName, file_size: fileSize, clientTempId }; const ok = wsManager.sendChatMessage( this.data.targetId, JSON.stringify(payload), 'file', this.data.chatType ); if (!ok) throw new Error('WebSocket连接异常,文件发送失败'); wx.hideLoading(); // 状态改为等待服务端回执后再置为sent } else { throw new Error(uploadResult?.message || '文件上传失败'); } } catch (err) { console.error('❌ 发送文件失败:', err); // 更新为失败 const failedMessages = this.data.messages.map(msg => { if (msg.messageId === tempMessage.messageId) { return { ...msg, deliveryStatus: 'failed' }; } return msg; }); this.setData({ messages: failedMessages }); wx.hideLoading(); wx.showToast({ title: '发送失败', icon: 'none' }); } }, fail: (error) => { if (error && error.errMsg && !/cancel/.test(error.errMsg)) { console.error('❌ 选择文件失败:', error); wx.showToast({ title: '选择文件失败', icon: 'none' }); } } }); } catch (e) { console.error('❌ 文件选择异常:', e); wx.showToast({ title: '选择文件异常', icon: 'none' }); } }, // 将字节大小格式化成人类可读 formatFileSize(bytes) { try { if (!bytes || bytes <= 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); const value = bytes / Math.pow(1024, i); return `${value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)} ${units[i]}`; } catch (e) { return `${bytes} B`; } }, // 选择位置 selectLocation() { wx.chooseLocation({ success: async (res) => { try { console.log('📍 选择位置:', res); const { name, address, latitude, longitude } = res || {}; // 基本校验 if (latitude == null || longitude == null) { wx.showToast({ title: '位置信息无效', icon: 'none' }); return; } // 当前用户 const currentUserId = this.data.userInfo?.user?.customId || this.data.userInfo?.customId || ''; if (!currentUserId) { wx.showToast({ title: '用户信息错误', icon: 'none' }); return; } // 构造临时消息 const timestamp = Date.now(); const clientTempId = 'temp_' + timestamp; const tempMsg = { messageId: clientTempId, senderId: currentUserId, targetId: this.data.targetId, msgType: 'location', content: { latitude, longitude, address, locationName: name || '位置' }, locationName: name || '位置', locationAddress: address || '', latitude, longitude, sendTime: timestamp, timestamp, isSelf: true, senderName: '我', senderAvatar: this.data.userAvatar || '', deliveryStatus: 'sending', bubbleTime: this.formatBubbleTime(timestamp) }; this.setMessagesWithDate(this.data.messages.concat([tempMsg])); setTimeout(() => this.scrollToBottom(), 100); // 通过 WebSocket 发送位置消息 const ok = wsManager.sendLocationMessage( this.data.targetId, latitude, longitude, address || '', name || '位置', this.data.chatType, { extra: JSON.stringify({ clientTempId }) } ); if (!ok) { this.updateMessageDeliveryStatus(clientTempId, 'failed'); throw new Error('WebSocket连接异常,位置发送失败'); } } catch (err) { console.error('❌ 发送位置失败:', err); // 标记失败 if (clientTempId) this.updateMessageDeliveryStatus(clientTempId, 'failed'); wx.showToast({ title: '位置发送失败', icon: 'none' }); } }, fail: (e) => { if (e && e.errMsg && !/cancel/.test(e.errMsg)) { console.error('❌ 选择位置失败:', e); wx.showToast({ title: '选择位置失败', icon: 'none' }); } }, complete: () => { this.setData({ showMorePanel: false }); } }); }, // 图片加载错误处理 onImageError(e) { console.error('❌ 图片加载失败:', e.detail); const url = e.currentTarget.dataset.url; wx.showToast({ title: '图片加载失败', icon: 'none', duration: 2000 }); }, // 预览图片 - 支持新的图片格式 previewImage(e) { const url = e.currentTarget.dataset.url; const type = e.currentTarget.dataset.type; console.log('🖼️ 预览图片:', { url, type }); if (!url) { wx.showToast({ title: '图片地址无效', icon: 'none' }); return; } // Base64格式需要特殊处理 if (type === 'base64' || url.startsWith('data:image/')) { // 小程序的previewImage可以直接处理Base64 wx.previewImage({ current: url, urls: [url] }); } else { // 普通URL格式 wx.previewImage({ current: url, urls: [url] }); } }, // 播放音频 playAudio(e) { const message = e.currentTarget.dataset.message; console.log('播放音频:', message); // TODO: 实现音频播放 wx.showToast({ title: '音频播放功能开发中', icon: 'none' }); }, // 显示位置 showLocation(e) { const message = e.currentTarget.dataset.message; console.log('📍 打开位置:', message); try { // 统一解析位置内容(content 可能是 JSON 字符串或对象) const parsed = this.parseLocationContent(message.content) || {}; const lat = message.latitude ?? parsed.latitude; const lng = message.longitude ?? parsed.longitude; const name = message.locationName || parsed.locationName || '位置'; const address = message.locationAddress || parsed.address || ''; if (lat == null || lng == null) { wx.showToast({ title: '无效位置', icon: 'none' }); return; } wx.openLocation({ latitude: Number(lat), longitude: Number(lng), name: name, address: address, scale: 16 }); } catch (err) { console.error('❌ 打开位置失败:', err); wx.showToast({ title: '打开位置失败', icon: 'none' }); } }, // 显示消息菜单 showMessageMenu(e) { const message = e.currentTarget.dataset.message; const itemList = []; // 撤回的消息只能删除 if (message.isRecalled) { itemList.push('删除'); } else { // 普通消息的菜单选项 if (message.msgType === 'text') { itemList.push('复制'); } itemList.push('删除'); if (message.isSelf) { // 检查是否可以撤回(2分钟内) const canRecall = this.canRecallMessage(message); if (canRecall) { itemList.push('撤回'); } } } wx.showActionSheet({ itemList: itemList, success: (res) => { const action = itemList[res.tapIndex]; switch (action) { case '复制': this.copyMessage(message); break; case '删除': this.deleteMessage(message); break; case '撤回': this.recallMessage(message); break; } } }); }, // 复制消息 copyMessage(message) { if (message.msgType === 'text') { wx.setClipboardData({ data: message.content, success: () => { wx.showToast({ title: '已复制', icon: 'success' }); } }); } }, // 删除消息 deleteMessage(message) { wx.showModal({ title: '确认删除', content: '确定要删除这条消息吗?', success: (res) => { if (res.confirm) { const messages = this.data.messages.filter(msg => msg.messageId !== message.messageId); this.setMessagesWithDate(messages); } } }); }, // 撤回消息 async recallMessage(message) { // 检查撤回时间限制 if (!this.canRecallMessage(message)) { wx.showToast({ title: '超过2分钟无法撤回', icon: 'none' }); return; } wx.showModal({ title: '确认撤回', content: '确定要撤回这条消息吗?', success: async (res) => { if (res.confirm) { try { // 显示加载状态 wx.showLoading({ title: '撤回中...', mask: true }); // 检查WebSocket连接状态 if (!wsManager || !wsManager.isConnected) { throw new Error('网络连接异常,请检查网络后重试'); } // 通过WebSocket发送撤回消息 const result = await wsManager.recallMessage(message.messageId); // 撤回成功,本地立即更新消息状态为已撤回 this.updateMessageToRecalled(message.messageId); wx.showToast({ title: '已撤回', icon: 'success' }); } catch (error) { console.error('撤回消息失败:', error); wx.showToast({ title: error.message || '撤回失败', icon: 'none' }); } finally { wx.hideLoading(); } } } }); }, // 检查是否可以撤回消息 canRecallMessage(message) { if (!message.isSelf) return false; if (message.msgType === 'system' || message.isRecalled) return false; // 检查时间限制(2分钟 = 120秒) const now = Date.now(); const messageTime = message.timestamp || message.createTime || now; const timeDiff = (now - messageTime) / 1000; // 转换为秒 return timeDiff <= 120; // 2分钟内可撤回 }, // 更新消息为已撤回状态 updateMessageToRecalled(messageId) { const messages = this.data.messages.map(msg => { if (msg.messageId === messageId) { return { ...msg, isRecalled: true, content: '你撤回了一条消息', originalContent: msg.content, // 保存原始内容用于调试 recallTime: Date.now() }; } return msg; }); this.setData({ messages }); }, // 重发消息 async resendMessage(e) { const message = e.currentTarget.dataset.message; if (!message) return; console.log('🔁 重发消息:', message); // 标记为发送中 this.updateMessageDeliveryStatus(message.messageId, 'sending'); try { // 确保WS已连接,必要时尝试快速重连 if (!wsManager || !wsManager.isConnected) { console.warn('⚠️ WebSocket未连接,尝试重连以重新发送...'); const tryConnect = new Promise((resolve, reject) => { let done = false; const to = setTimeout(() => { if (!done) { done = true; cleanup(); reject(new Error('WS重连超时')); } }, 8000); const onConn = () => { if (!done) { done = true; cleanup(); resolve(); } }; const onErr = (err) => { if (!done) { done = true; cleanup(); reject(err || new Error('WS重连失败')); } }; const cleanup = () => { try { wsManager.off('connected', onConn); wsManager.off('error', onErr); clearTimeout(to); } catch (e) {} }; try { wsManager.on('connected', onConn); wsManager.on('error', onErr); wsManager.connect(); } catch (e) { cleanup(); reject(e); } }); await tryConnect; } let ok = false; switch (message.msgType) { case 'text': ok = wsManager.sendChatMessage(this.data.targetId, message.content, 'text', this.data.chatType); break; case 'image': { const url = message.content?.url || message.content; ok = wsManager.sendChatMessage(this.data.targetId, url, 'image', this.data.chatType); break; } case 'video': { const url = message.content?.url || message.content; ok = wsManager.sendChatMessage(this.data.targetId, url, 'video', this.data.chatType); break; } case 'voice': { const payload = typeof message.content === 'string' ? message.content : JSON.stringify(message.content || {}); ok = wsManager.sendChatMessage(this.data.targetId, payload, 'voice', this.data.chatType); break; } case 'file': { const payload = JSON.stringify({ url: message.content?.url || '', file_name: message.fileName, file_size: message.fileSize }); ok = wsManager.sendChatMessage(this.data.targetId, payload, 'file', this.data.chatType); break; } case 'location': { const lat = message.latitude ?? message.content?.latitude; const lng = message.longitude ?? message.content?.longitude; const address = message.locationAddress || message.content?.address || ''; const name = message.locationName || message.content?.locationName || '位置'; ok = wsManager.sendLocationMessage(this.data.targetId, lat, lng, address, name, this.data.chatType); break; } default: ok = false; } if (!ok) throw new Error('网络异常,发送失败'); // 成功由服务端回执最终置为sent,这里不强行改 } catch (err) { console.error('❌ 重发失败:', err); this.updateMessageDeliveryStatus(message.messageId, 'failed'); wx.showToast({ title: '发送失败', icon: 'none' }); } }, // 将消息置为失败并可选记录原因 markMessageFailed(messageId, reason) { const messages = this.data.messages.map(m => m.messageId === messageId ? { ...m, deliveryStatus: 'failed', errorReason: reason || '' } : m); this.setData({ messages }); }, // 🔥 加载更多消息 - 仅在用户向上滚动时触发 loadMoreMessages() { console.log('🔄 用户向上滚动,触发加载更多历史消息'); console.log('🔍 当前状态检查:', { hasMore: this.data.hasMore, loadingMessages: this.data.loadingMessages, lastMessageId: this.data.lastMessageId, messagesCount: this.data.messages.length }); // 🔥 检查是否还有更多消息 if (!this.data.hasMore) { console.log('⚠️ 没有更多历史消息了'); return; } // 🔥 检查是否正在加载中 if (this.data.loadingMessages) { console.log('⚠️ 正在加载中,忽略重复请求'); return; } // 🔥 检查是否有lastMessageId用于分页 if (!this.data.lastMessageId) { console.log('⚠️ 缺少lastMessageId,无法加载更多'); this.setData({ hasMore: false }); return; } console.log('✅ 开始加载更多历史消息,lastMessageId:', this.data.lastMessageId); // 🔥 直接调用loadMessages,不在这里设置loadingMessages状态 // loadMessages方法内部会处理状态管理 this.loadMessages(true); // 传递true表示是加载更多 }, // 🔥 滚动事件处理 - 控制滚动到底部按钮显示 onScroll(e) { const { scrollTop, scrollHeight } = e.detail; const windowHeight = this.data.windowHeight - 200; // 减去输入框高度 // 计算距离底部的距离 const distanceFromBottom = scrollHeight - scrollTop - windowHeight; // 如果距离底部超过一个屏幕高度,显示滚动到底部按钮 const shouldShow = distanceFromBottom > windowHeight; if (this.data.showScrollToBottom !== shouldShow) { this.setData({ showScrollToBottom: shouldShow }); } }, // 🔥 滚动到底部 - 参考Flutter app的实现 scrollToBottom() { if (this.data.messages.length === 0) return; const lastMessage = this.data.messages[this.data.messages.length - 1]; if (!lastMessage) return; console.log('📍 自动滚动到最新消息,消息ID:', lastMessage.messageId); // 🔥 参考Flutter的animateTo(maxScrollExtent)实现 // 方法1:使用scrollIntoView精确定位到最后一条消息 const scrollIntoViewId = 'msg-' + lastMessage.messageId; this.setData({ scrollIntoView: scrollIntoViewId, scrollTop: 999999, // 🔥 同时设置一个大值确保滚动到底部 showScrollToBottom: false // 🔥 滚动到底部后隐藏按钮 }); // 🔥 延迟再次滚动,确保DOM完全渲染(模仿Flutter的延迟机制) setTimeout(() => { this.setData({ scrollIntoView: scrollIntoViewId, scrollTop: 999999 }); }, 100); // 🔥 最后的保障:强制滚动到底部 setTimeout(() => { this.setData({ scrollTop: 999999 }); }, 300); }, // 显示聊天菜单 showChatMenu() { const itemList = ['清空聊天记录', '查找聊天记录']; if (this.data.chatType === 0) { itemList.push('视频通话', '语音通话'); } else { itemList.push('群聊信息'); } wx.showActionSheet({ itemList: itemList, success: (res) => { console.log('选择菜单项:', res.tapIndex); wx.showToast({ title: '功能开发中', icon: 'none' }); } }); }, // 下载文件 downloadFile(e) { const message = e.currentTarget.dataset.message; console.log('📄 下载文件:', message); if (!message.content || !message.content.url) { wx.showToast({ title: '文件地址无效', icon: 'none' }); return; } wx.showModal({ title: '下载文件', content: `确定要下载文件 "${message.fileName || '未知文件'}" 吗?`, success: (res) => { if (res.confirm) { wx.downloadFile({ url: message.content.url, success: (downloadRes) => { if (downloadRes.statusCode === 200) { wx.openDocument({ filePath: downloadRes.tempFilePath, success: () => { console.log('✅ 文件打开成功'); }, fail: (err) => { console.error('❌ 文件打开失败:', err); wx.showToast({ title: '文件打开失败', icon: 'none' }); } }); } }, fail: (err) => { console.error('❌ 文件下载失败:', err); wx.showToast({ title: '文件下载失败', icon: 'none' }); } }); } } }); }, // 查看名片 viewCard(e) { const message = e.currentTarget.dataset.message; console.log('👤 查看名片:', message); wx.showModal({ title: '联系人名片', content: `姓名: ${message.cardName || '未知'}\n电话: ${message.cardPhone || '未知'}`, showCancel: true, cancelText: '关闭', confirmText: '添加联系人', success: (res) => { if (res.confirm) { // TODO: 实现添加联系人功能 wx.showToast({ title: '添加联系人功能开发中', icon: 'none' }); } } }); }, // 切换主题 toggleTheme() { const next = this.data.themeClass === 'theme-dark' ? 'theme-light' : 'theme-dark'; const query = wx.createSelectorQuery(); query.select('.theme-toggle').boundingClientRect(); query.selectViewport().boundingClientRect(); query.exec((res) => { const btnRect = res && res[0]; const vpRect = res && res[1]; // 兜底:左上角区域,和按钮在左侧的位置一致 let cx = 24; let cy = (this.data.statusBarHeight || 0) + 16; if (btnRect) { cx = btnRect.left + btnRect.width / 2; cy = btnRect.top + btnRect.height / 2; } // 计算到四角的最大距离,得到覆盖半径 const w = vpRect ? vpRect.width : (this.data.systemInfo?.windowWidth || 375); const h = vpRect ? vpRect.height : (this.data.systemInfo?.windowHeight || this.data.windowHeight || 667); const dx = Math.max(cx, w - cx); const dy = Math.max(cy, h - cy); const radius = Math.ceil(Math.sqrt(dx * dx + dy * dy)) + 12; const diameter = radius * 2; const left = Math.round(cx - radius); const top = Math.round(cy - radius); // 开启过渡与遮罩 this.setData({ isThemeTransitioning: true, showThemeOverlay: true, overlayPlaying: false, overlayTo: next, overlayX: cx, overlayY: cy, overlayDiameter: diameter, overlayLeft: left, overlayTop: top }); // 下一帧启动遮罩动画,并切换主题变量 setTimeout(() => { this.setData({ overlayPlaying: true, themeClass: next }); try { wx.setStorageSync('theme', next); } catch (e) { console.warn('保存主题失败', e); } }, 16); // 动画结束后收尾 setTimeout(() => { this.setData({ showThemeOverlay: false, overlayPlaying: false, isThemeTransitioning: false }); }, 420); }); }, });