miniprogramme/pages/message/chat/chat.js
2025-09-12 16:08:17 +08:00

3085 lines
No EOL
98 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 聊天页面逻辑
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';
},
// 转换消息状态映射为前端deliveryStatussending/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);
});
},
});