3222 lines
103 KiB
JavaScript
3222 lines
103 KiB
JavaScript
// 聊天页面逻辑
|
||
const app = getApp();
|
||
const mediaPicker = require('../../../utils/media-picker.js');
|
||
const { initPageSystemInfo } = require('../../../utils/system-info-modern.js');
|
||
const imageCacheManager = require('../../../utils/image-cache-manager.js');
|
||
const nimConversationManager = require('../../../utils/nim-conversation-manager.js');
|
||
const nimUserManager = require('../../../utils/nim-user-manager.js');
|
||
const friendAPI = require('../../../utils/friend-api.js');
|
||
|
||
Page({
|
||
data: {
|
||
// 聊天基本信息
|
||
conversationId: '',
|
||
targetId: '',
|
||
chatName: '',
|
||
chatType: 0, // 0: 单聊, 1: 群聊
|
||
isOnline: false,
|
||
isNewConversation: false, // 是否为新会话
|
||
|
||
// 消息数据
|
||
messages: [],
|
||
hasMore: true,
|
||
lastMessageTime: 0,
|
||
unreadCount: 0,
|
||
loadingMessages: false, // 🔥 添加加载状态
|
||
|
||
// 输入相关
|
||
inputType: 'text', // 'text' 或 'voice'
|
||
inputText: '',
|
||
inputFocus: false,
|
||
recording: false,
|
||
voiceCancel: false, // 🔥 语音录制取消标识
|
||
isComposing: false, // 中文输入法状态
|
||
|
||
// 面板状态
|
||
showEmojiPanel: false,
|
||
showMorePanel: false,
|
||
showVoiceRecorder: false,
|
||
|
||
// 滚动相关
|
||
scrollTop: 0,
|
||
scrollIntoView: '',
|
||
showScrollToBottom: false, // 🔥 控制滚动到底部按钮显示
|
||
|
||
// 常用表情
|
||
commonEmojis: [
|
||
'😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃',
|
||
'😉', '😊', '😇', '🥰', '😍', '🤩', '😘', '😗', '😚', '😙',
|
||
'😋', '😛', '😜', '🤪', '😝', '🤑', '🤗', '🤭', '🤫', '🤔',
|
||
'🤐', '🤨', '😐', '😑', '😶', '😏', '😒', '🙄', '😬', '🤥',
|
||
'😌', '😔', '😪', '🤤', '😴', '😷', '🤒', '🤕', '🤢', '🤮',
|
||
'🤧', '🥵', '🥶', '🥴', '😵', '🤯', '🤠', '🥳', '🥸', '😎',
|
||
'🤓', '🧐', '😕', '😟', '🙁', '☹️', '😮', '😯', '😲', '😳',
|
||
'🥺', '😦', '😧', '😨', '😰', '😥', '😢', '😭', '😱', '😖',
|
||
'😣', '😞', '😓', '😩', '😫', '🥱', '😤', '😡', '😠', '🤬',
|
||
'👋', '🤚', '🖐️', '✋', '🖖', '👌', '🤌', '🤏', '✌️', '🤞',
|
||
'🤟', '🤘', '🤙', '👈', '👉', '👆', '🖕', '👇', '☝️', '👍',
|
||
'👎', '👊', '✊', '🤛', '🤜', '👏', '🙌', '👐', '🤲', '🤝'
|
||
],
|
||
|
||
// 用户信息
|
||
userInfo: null,
|
||
userAvatar: '',
|
||
targetUserInfo: null, // 对方用户信息
|
||
targetAvatar: '', // 对方头像
|
||
|
||
// 系统适配信息
|
||
systemInfo: {},
|
||
statusBarHeight: 0,
|
||
menuButtonHeight: 0,
|
||
menuButtonTop: 0,
|
||
navBarHeight: 0,
|
||
windowHeight: 0,
|
||
safeAreaBottom: 0,
|
||
keyboardHeight: 0, // 键盘高度
|
||
inputAreaHeight: 0, // 输入框区域高度
|
||
emojiPanelHeight: 0, // 表情面板高度
|
||
|
||
// 加载状态
|
||
isLoading: false,
|
||
},
|
||
|
||
// 🔍 判断内容是否像图片(URL/Base64/JSON中包含图片URL)
|
||
_looksLikeImageContent(content) {
|
||
try {
|
||
if (!content) return false;
|
||
const isImgUrl = (u) => {
|
||
if (typeof u !== 'string') return false;
|
||
if (/^data:image\//i.test(u)) return true;
|
||
if (!/^https?:\/\//i.test(u)) return false;
|
||
return /\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(u);
|
||
};
|
||
if (typeof content === 'string') {
|
||
const s = content.trim();
|
||
if (isImgUrl(s)) return true;
|
||
if (s.startsWith('{') && s.endsWith('}')) {
|
||
try {
|
||
const obj = JSON.parse(s);
|
||
const u = obj?.file_url || obj?.url;
|
||
return isImgUrl(u || '');
|
||
} catch (_) { return false; }
|
||
}
|
||
return false;
|
||
}
|
||
if (typeof content === 'object') {
|
||
const u = content.file_url || content.url;
|
||
return isImgUrl(u || '');
|
||
}
|
||
return false;
|
||
} catch (_) {
|
||
return false;
|
||
}
|
||
},
|
||
|
||
// 🔥 将 NIM SDK V2 的数字类型枚举转换为 WXML 中使用的字符串类型
|
||
// V2NIMMessageType: 0=文本, 1=图片, 2=语音, 3=视频, 4=位置, 5=通知, 6=文件, 10=提示, 100=自定义
|
||
convertMessageTypeToString(messageType) {
|
||
const typeMap = {
|
||
0: 'text', // 文本
|
||
1: 'image', // 图片
|
||
2: 'audio', // 语音(WXML中也兼容'voice')
|
||
3: 'video', // 视频
|
||
4: 'location', // 位置
|
||
5: 'notification', // 通知
|
||
6: 'file', // 文件
|
||
10: 'tip', // 提示
|
||
100: 'custom' // 自定义
|
||
};
|
||
return typeMap[messageType] || 'text';
|
||
},
|
||
|
||
|
||
/**
|
||
* 点击自己头像 -> 跳转个人资料页
|
||
*/
|
||
onSelfAvatarTap(e) {
|
||
try {
|
||
const userId = this.data.userInfo?.user?.customId;
|
||
if (!userId) return;
|
||
wx.navigateTo({
|
||
url: `/subpackages/profile/profile/profile?userId=${userId}`
|
||
});
|
||
} catch (err) {
|
||
console.error('进入个人资料失败', err);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 点击对方头像 -> 跳转好友详情页(单聊)或成员资料(群聊可后续拓展)
|
||
*/
|
||
async onPeerAvatarTap(e) {
|
||
try {
|
||
const senderId = e.currentTarget.dataset.senderId;
|
||
if (!senderId) {
|
||
console.warn('⚠️ senderId 为空,无法跳转');
|
||
return;
|
||
}
|
||
// // 如果是自己就直接走自己的逻辑
|
||
// const selfId = this.data.userInfo?.user?.customId;
|
||
// if (senderId === selfId) {
|
||
// this.onSelfAvatarTap();
|
||
// return;
|
||
// }
|
||
|
||
wx.showLoading({ title: '加载中...', mask: true });
|
||
// 调用好友详情接口判断关系
|
||
let res = await friendAPI.getFriendDetail(senderId)
|
||
wx.hideLoading();
|
||
if (res?.code == 0 || res?.code == 200) {
|
||
console.log('√ 是好友,跳转好友详情页');
|
||
wx.navigateTo({
|
||
url: `/subpackages/social/friend-detail/friend-detail?customId=${senderId}`
|
||
});
|
||
} else {
|
||
console.log('! 非好友,跳转陌生人预览页');
|
||
wx.navigateTo({
|
||
url: `/subpackages/social/user-preview/user-preview?customId=${senderId}`
|
||
});
|
||
}
|
||
} catch (err) {
|
||
console.error('× 查询好友关系失败:', err);
|
||
wx.hideLoading();
|
||
const senderId = e.currentTarget.dataset.senderId;
|
||
if (senderId) {
|
||
wx.navigateTo({
|
||
url: `/subpackages/social/user-preview/user-preview?customId=${senderId}`
|
||
});
|
||
}
|
||
}
|
||
},
|
||
|
||
async onLoad(options) {
|
||
|
||
// 显示加载状态
|
||
this.setData({ isLoading: true });
|
||
|
||
// 初始化系统信息(同步,立即完成)
|
||
this.initSystemInfo();
|
||
|
||
// 检查 NIM 状态
|
||
const appInstance = getApp();
|
||
this.nim = appInstance?.globalData?.nim || null;
|
||
this._nimOldestMessage = null;
|
||
|
||
if (!this.nim || !this.nim.V2NIMMessageService || !this.nim.V2NIMConversationService) {
|
||
console.error('❌ NIM 服务不可用');
|
||
wx.showToast({
|
||
title: 'NIM服务不可用',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
console.log('✅ NIM消息服务可用,启用NIM模式加载消息');
|
||
|
||
// 并行初始化用户与聊天信息
|
||
const userInfoPromise = this.initUserInfo();
|
||
const chatInfoPromise = this.initChatInfo(options);
|
||
|
||
await userInfoPromise;
|
||
|
||
await chatInfoPromise;
|
||
|
||
// 隐藏加载状态
|
||
this.setData({ isLoading: false });
|
||
|
||
// 注册 NIM 监听器
|
||
this.attachNIMListeners();
|
||
},
|
||
|
||
onShow: function () {
|
||
console.log('聊天页面显示');
|
||
|
||
// 🔥 检查并重连 NIM(防止从后台恢复后 NIM 断开)
|
||
this.checkAndReconnectNim();
|
||
|
||
// 进入聊天页面时自动标记所有消息已读
|
||
this.markAllMessagesRead();
|
||
},
|
||
|
||
onReady() {
|
||
// 页面已就绪,获取输入框区域的实际高度
|
||
this.updateInputAreaHeight();
|
||
},
|
||
|
||
// 更新输入框区域高度
|
||
updateInputAreaHeight() {
|
||
wx.createSelectorQuery()
|
||
.select('.input-area')
|
||
.boundingClientRect(rect => {
|
||
if (rect && rect.height) {
|
||
console.log('📏 输入框区域高度:', rect.height);
|
||
this.setData({
|
||
inputAreaHeight: rect.height
|
||
});
|
||
}
|
||
})
|
||
.exec();
|
||
},
|
||
|
||
onHide: function () {
|
||
// 页面隐藏时可以保持会话状态
|
||
},
|
||
|
||
onUnload: function () {
|
||
// 🔥 清理录音相关定时器
|
||
if (this.voiceStartTimer) {
|
||
clearTimeout(this.voiceStartTimer);
|
||
this.voiceStartTimer = null;
|
||
}
|
||
|
||
// 页面卸载时清理 NIM 监听器
|
||
this.detachNIMListeners();
|
||
},
|
||
|
||
// 注册 NIM 事件监听器
|
||
attachNIMListeners() {
|
||
console.log('📡 注册 NIM 事件监听器');
|
||
|
||
const nim = getApp().globalData.nim;
|
||
if (!nim || !nim.V2NIMMessageService) {
|
||
console.warn('⚠️ NIM SDK 不可用,无法注册监听器');
|
||
return;
|
||
}
|
||
|
||
// 🔥 使用 bind(this) 确保回调中的 this 上下文正确
|
||
// 监听新消息
|
||
this._nimMessageListener = this.handleNewNIMMessage.bind(this);
|
||
|
||
// 🔥 监听消息撤回通知
|
||
this._nimMessageRevokeListener = this.handleMessageRevokeNotifications.bind(this);
|
||
|
||
// 注册监听器(根据NIM SDK文档调整)
|
||
try {
|
||
nim.V2NIMMessageService.on('onReceiveMessages', this._nimMessageListener);
|
||
nim.V2NIMMessageService.on('onMessageRevokeNotifications', this._nimMessageRevokeListener);
|
||
console.log('✅ NIM 监听器注册成功');
|
||
} catch (error) {
|
||
console.error('❌ 注册 NIM 监听器失败:', error);
|
||
}
|
||
},
|
||
|
||
// 清理 NIM 事件监听器
|
||
detachNIMListeners() {
|
||
console.log('🔌 清理 NIM 事件监听器');
|
||
|
||
const nim = getApp().globalData.nim;
|
||
if (!nim || !nim.V2NIMMessageService) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (this._nimMessageListener) {
|
||
nim.V2NIMMessageService.off('onReceiveMessages', this._nimMessageListener);
|
||
this._nimMessageListener = null;
|
||
}
|
||
if (this._nimMessageRevokeListener) {
|
||
nim.V2NIMMessageService.off('onMessageRevokeNotifications', this._nimMessageRevokeListener);
|
||
this._nimMessageRevokeListener = null;
|
||
}
|
||
console.log('✅ NIM 监听器清理成功');
|
||
} catch (error) {
|
||
console.error('❌ 清理 NIM 监听器失败:', error);
|
||
}
|
||
},
|
||
|
||
// 🔥 检查并重连 NIM(页面级别)
|
||
async checkAndReconnectNim() {
|
||
try {
|
||
const app = getApp();
|
||
|
||
// 触发全局 NIM 重连检查
|
||
if (app && typeof app.checkAndReconnectNim === 'function') {
|
||
await app.checkAndReconnectNim();
|
||
}
|
||
|
||
// 检查 NIM 实例是否可用
|
||
const nim = app.globalData.nim;
|
||
if (!nim || !nim.V2NIMMessageService) {
|
||
console.warn('⚠️ NIM 实例不可用');
|
||
return;
|
||
}
|
||
|
||
// 重新注册事件监听器(确保监听器有效)
|
||
if (!this._nimMessageListener || !this._nimMessageRevokeListener) {
|
||
console.log('🔄 重新注册 NIM 事件监听器');
|
||
this.detachNIMListeners(); // 先清理旧的
|
||
this.attachNIMListeners(); // 重新注册
|
||
}
|
||
|
||
// 检查会话管理器状态
|
||
if (!nimConversationManager.getInitStatus()) {
|
||
console.log('🔄 重新初始化会话管理器');
|
||
nimConversationManager.init(nim);
|
||
}
|
||
|
||
console.log('✅ 聊天页面 NIM 状态检查完成');
|
||
} catch (error) {
|
||
console.error('❌ 聊天页面 NIM 重连检查失败:', error);
|
||
}
|
||
},
|
||
|
||
// 处理 NIM 新消息
|
||
async handleNewNIMMessage(messages) {
|
||
try {
|
||
console.log('📨 处理 NIM 新消息,数量:', messages?.length || 0);
|
||
|
||
// 🔥 NIM SDK V2 的 onReceiveMessages 回调参数是消息数组
|
||
if (!Array.isArray(messages) || messages.length === 0) {
|
||
console.warn('⚠️ 收到空消息数组或非数组数据');
|
||
return;
|
||
}
|
||
|
||
// 过滤出属于当前会话的消息
|
||
const currentConversationMessages = messages.filter(
|
||
msg => msg.conversationId === this.data.conversationId
|
||
);
|
||
|
||
if (currentConversationMessages.length === 0) {
|
||
console.log('⚠️ 没有属于当前会话的消息,跳过');
|
||
return;
|
||
}
|
||
|
||
console.log('✅ 收到当前会话的新消息:', currentConversationMessages.length, '条');
|
||
|
||
// 格式化所有新消息
|
||
const formattedMessages = await Promise.all(
|
||
currentConversationMessages.map(msg => this.formatMessage(msg))
|
||
);
|
||
|
||
// 去重处理 - 防止重复显示已存在的消息
|
||
const existingIds = new Set(this.data.messages.map(msg => msg.messageId));
|
||
const uniqueNewMessages = formattedMessages.filter(
|
||
msg => !existingIds.has(msg.messageId)
|
||
);
|
||
|
||
if (uniqueNewMessages.length === 0) {
|
||
console.log('⚠️ 所有新消息均已存在,跳过重复添加');
|
||
return;
|
||
}
|
||
|
||
console.log('✅ 去重后新消息数量:', uniqueNewMessages.length);
|
||
|
||
// 添加到消息列表
|
||
const updatedMessages = this.data.messages.concat(uniqueNewMessages);
|
||
this.setMessagesWithDate(updatedMessages);
|
||
|
||
// 滚动到底部
|
||
setTimeout(() => this.scrollToBottom(), 100);
|
||
|
||
// 标记已读
|
||
await nimConversationManager.markConversationRead(this.data.conversationId);
|
||
|
||
console.log('✅ 新消息处理完成');
|
||
} catch (error) {
|
||
console.error('❌ 处理新消息失败:', error);
|
||
}
|
||
},
|
||
|
||
// 处理消息撤回通知
|
||
handleMessageRevokeNotifications(revokeNotifications) {
|
||
try {
|
||
if (!Array.isArray(revokeNotifications) || revokeNotifications.length === 0) {
|
||
return;
|
||
}
|
||
|
||
// 过滤出属于当前会话的撤回通知
|
||
const currentConversationRevokes = revokeNotifications.filter(
|
||
revoke => revoke.messageRefer?.conversationId === this.data.conversationId
|
||
);
|
||
|
||
if (currentConversationRevokes.length === 0) {
|
||
return;
|
||
}
|
||
|
||
// 更新本地消息状态
|
||
currentConversationRevokes.forEach(revoke => {
|
||
const messageRefer = revoke.messageRefer;
|
||
if (!messageRefer) return;
|
||
|
||
const messageServerId = messageRefer.messageServerId;
|
||
const messageClientId = messageRefer.messageClientId;
|
||
|
||
// 尝试找到匹配的消息
|
||
let foundMessage = null;
|
||
|
||
// 方式1: 通过 messageServerId 查找
|
||
if (messageServerId) {
|
||
foundMessage = this.data.messages.find(m =>
|
||
m.messageId === messageServerId ||
|
||
m._rawMessage?.messageServerId === messageServerId
|
||
);
|
||
}
|
||
|
||
// 方式2: 通过 messageClientId 查找
|
||
if (!foundMessage && messageClientId) {
|
||
foundMessage = this.data.messages.find(m =>
|
||
m.messageId === messageClientId ||
|
||
m._rawMessage?.messageClientId === messageClientId
|
||
);
|
||
}
|
||
|
||
if (foundMessage) {
|
||
this.updateMessageToRecalled(foundMessage.messageId);
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('处理撤回通知失败:', error);
|
||
}
|
||
},
|
||
|
||
// 初始化系统信息
|
||
initSystemInfo() {
|
||
const pageSystemInfo = initPageSystemInfo();
|
||
|
||
this.setData({
|
||
systemInfo: pageSystemInfo.systemInfo,
|
||
statusBarHeight: pageSystemInfo.statusBarHeight,
|
||
menuButtonHeight: pageSystemInfo.menuButtonHeight,
|
||
menuButtonTop: pageSystemInfo.menuButtonTop,
|
||
navBarHeight: pageSystemInfo.navBarHeight,
|
||
windowHeight: pageSystemInfo.windowHeight,
|
||
safeAreaBottom: pageSystemInfo.safeAreaBottom,
|
||
keyboardHeight: 0 // 初始化键盘高度
|
||
});
|
||
},
|
||
|
||
// 🔥 监听键盘高度变化
|
||
onKeyboardHeightChange(e) {
|
||
const { height, duration } = e.detail;
|
||
|
||
const inputAreaHeight = this.data.inputAreaHeight;
|
||
const totalBottom = inputAreaHeight + (height || 0);
|
||
|
||
console.log('⌨️ 键盘高度变化:', {
|
||
keyboardHeight: height,
|
||
inputAreaHeight,
|
||
totalBottom,
|
||
duration
|
||
});
|
||
|
||
// 直接使用微信提供的键盘高度(单位:px)
|
||
this.setData({
|
||
keyboardHeight: height || 0
|
||
}, () => {
|
||
// 键盘弹出时,滚动到底部,确保最新消息可见
|
||
if (height > 0) {
|
||
this.scrollToBottom(true);
|
||
}
|
||
});
|
||
},
|
||
|
||
// 初始化聊天信息
|
||
async initChatInfo(options) {
|
||
const inputConversationId = options.conversationId || '';
|
||
const targetId = options.targetId || '';
|
||
const chatName = decodeURIComponent(options.name || '聊天');
|
||
const chatType = parseInt(options.chatType || 0);
|
||
const incomingTargetAvatar = options.targetAvatar || '';
|
||
const sendMessage = options.sendMessage?.trim() || '';
|
||
|
||
// 临时设置基本信息
|
||
this.setData({
|
||
targetId,
|
||
chatName,
|
||
chatType,
|
||
inputText: sendMessage,
|
||
conversationId: inputConversationId,
|
||
targetAvatar: incomingTargetAvatar || this.data.targetAvatar || ''
|
||
});
|
||
|
||
// 🔥 动态设置导航栏标题为用户昵称
|
||
wx.setNavigationBarTitle({
|
||
title: chatName
|
||
});
|
||
|
||
// 🔥 从 NIM 获取对方用户信息(头像和昵称)
|
||
// 注意:如果 targetId 是会话ID格式(如 "p2p-xxx" 或 "16950956|1|33981539"),需要提取真实用户ID
|
||
if (targetId) {
|
||
let realUserId = targetId;
|
||
|
||
// 🔥 如果是 NIM 会话ID格式(包含 | 分隔符),提取真实用户ID
|
||
// 格式:senderId|conversationType|receiverId(如 "16950956|1|33981539")
|
||
if (targetId.includes('|')) {
|
||
const parts = targetId.split('|');
|
||
if (parts.length >= 3) {
|
||
// 对于单聊(type=1),receiverId 是对方用户ID(parts[2])
|
||
realUserId = parts[2];
|
||
console.log('🔧 从会话ID提取用户ID:', { conversationId: targetId, userId: realUserId });
|
||
}
|
||
}
|
||
|
||
await this.loadTargetUserInfo(realUserId);
|
||
}
|
||
|
||
// 🔥 如果没有传入conversationId,先检查会话是否已存在
|
||
if (!inputConversationId) {
|
||
console.log('🔍 未传入会话ID,检查是否已存在该会话');
|
||
|
||
if (!targetId) {
|
||
console.error('❌ 缺少 targetId,无法查询会话');
|
||
return;
|
||
}
|
||
|
||
// 使用NIM生成会话ID
|
||
const conversationId = this.buildNIMConversationId(targetId, chatType);
|
||
if (!conversationId) {
|
||
console.error('❌ 无法生成会话ID');
|
||
return;
|
||
}
|
||
|
||
console.log('🔍 生成的会话ID:', conversationId);
|
||
|
||
// 🔥 检查会话是否已存在(从NIM SDK获取会话列表查询)
|
||
try {
|
||
const nim = getApp().globalData.nim;
|
||
if (nim && nim.V2NIMConversationService) {
|
||
// 🔥 获取会话列表并查找目标会话
|
||
console.log('🔍 从NIM SDK查询会话是否存在...');
|
||
const result = await nim.V2NIMConversationService.getConversationList(0, 100);
|
||
const existingConv = result?.conversationList?.find(c => c.conversationId === conversationId);
|
||
|
||
if (existingConv) {
|
||
console.log('✅ 会话已存在,使用现有会话:', existingConv);
|
||
this.setData({
|
||
conversationId,
|
||
isNewConversation: false
|
||
});
|
||
|
||
// 加载消息
|
||
await this.loadMessages();
|
||
return;
|
||
} else {
|
||
console.log('📝 会话不存在,标记为新会话');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.warn('⚠️ 查询会话失败,将作为新会话处理:', error);
|
||
}
|
||
|
||
// 🔥 会话不存在,标记为新会话(不加载历史消息)
|
||
console.log('✅ 创建新会话ID:', conversationId);
|
||
this.setData({
|
||
conversationId,
|
||
isNewConversation: true,
|
||
messages: []
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 使用传入的conversationId
|
||
this.setData({ conversationId: inputConversationId });
|
||
|
||
// 加载消息
|
||
await this.loadMessages();
|
||
},
|
||
|
||
// 🔥 验证并使用传入的会话ID
|
||
buildNIMConversationId(targetId, chatType) {
|
||
try {
|
||
if (!this.nim || !this.nim.V2NIMConversationIdUtil) {
|
||
return '';
|
||
}
|
||
if (!targetId) {
|
||
return '';
|
||
}
|
||
if (Number(chatType) === 1) {
|
||
return this.nim.V2NIMConversationIdUtil.teamConversationId(String(targetId));
|
||
}
|
||
return this.nim.V2NIMConversationIdUtil.p2pConversationId(String(targetId));
|
||
} catch (err) {
|
||
console.warn('构造 NIM 会话ID 失败:', err);
|
||
return '';
|
||
}
|
||
},
|
||
|
||
// 🔥 从 NIM SDK 获取对方用户信息(包括头像和昵称)
|
||
async loadTargetUserInfo(targetId) {
|
||
try {
|
||
console.log('📡 从 NIM 获取用户信息:', targetId);
|
||
|
||
// 🔥 使用 NIM SDK 获取用户信息
|
||
const userInfo = await nimUserManager.getUserInfo(targetId);
|
||
|
||
if (userInfo) {
|
||
const targetAvatar = userInfo.avatar || '';
|
||
const targetNickname = userInfo.nickname || '';
|
||
|
||
// 🔥 先设置用户信息,头像缓存异步执行
|
||
this.setData({
|
||
targetUserInfo: {
|
||
avatar: targetAvatar,
|
||
nickname: targetNickname
|
||
},
|
||
targetAvatar: targetAvatar, // 先使用原始URL
|
||
chatName: targetNickname || this.data.chatName // 更新聊天名称
|
||
});
|
||
|
||
console.log('✅ 用户信息加载成功(从 NIM):', { targetNickname, targetAvatar });
|
||
|
||
// 🔥 异步缓存头像,不阻塞UI
|
||
if (targetAvatar) {
|
||
imageCacheManager.cacheAvatar(targetAvatar).then(cachedAvatar => {
|
||
this.setData({
|
||
targetAvatar: cachedAvatar
|
||
});
|
||
}).catch(error => {
|
||
console.error('❌ 对方头像缓存失败:', error);
|
||
});
|
||
}
|
||
} else {
|
||
console.warn('⚠️ 从 NIM 未获取到用户信息');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('❌ 从 NIM 获取用户信息异常:', error);
|
||
}
|
||
},
|
||
|
||
// 初始化用户信息
|
||
async initUserInfo() {
|
||
// 先尝试从全局获取
|
||
let userInfo = app.globalData.userInfo;
|
||
|
||
// 如果全局没有,尝试从本地存储获取
|
||
if (!userInfo || !userInfo.token) {
|
||
try {
|
||
const storedUserInfo = wx.getStorageSync('userInfo');
|
||
if (storedUserInfo && storedUserInfo.token) {
|
||
userInfo = storedUserInfo;
|
||
// 更新全局数据
|
||
app.globalData.userInfo = userInfo;
|
||
app.globalData.isLoggedIn = true;
|
||
|
||
}
|
||
} catch (error) {
|
||
console.error('从本地存储获取用户信息失败:', error);
|
||
}
|
||
}
|
||
|
||
// 确保有用户信息
|
||
if (userInfo && userInfo.token) {
|
||
// 正确获取用户头像
|
||
const userAvatar = userInfo.user?.avatar || userInfo.avatar || '';
|
||
|
||
|
||
// 🔥 先设置用户信息,头像缓存异步执行
|
||
this.setData({
|
||
userInfo: userInfo,
|
||
userAvatar: userAvatar // 先使用原始URL
|
||
});
|
||
|
||
// 🔥 异步缓存头像,不阻塞UI
|
||
imageCacheManager.cacheAvatar(userAvatar).then(cachedAvatar => {
|
||
this.setData({
|
||
userAvatar: cachedAvatar
|
||
});
|
||
|
||
}).catch(error => {
|
||
console.error('❌ 用户头像缓存失败:', error);
|
||
});
|
||
|
||
} else {
|
||
console.error('用户信息初始化失败,跳转到登录页');
|
||
wx.reLaunch({
|
||
url: '/pages/login/login'
|
||
});
|
||
}
|
||
},
|
||
|
||
// 加载消息 - 使用 NIM SDK
|
||
async loadMessages(isLoadMore = false) {
|
||
try {
|
||
console.log('📨 加载聊天消息 (NIM SDK):', {
|
||
isLoadMore,
|
||
conversationId: this.data.conversationId
|
||
});
|
||
|
||
// 检查 conversationId
|
||
if (!this.data.conversationId) {
|
||
console.log('⚠️ 缺少 conversationId,无法加载消息');
|
||
return;
|
||
}
|
||
|
||
// 避免重复请求
|
||
if (this.data.loadingMessages) {
|
||
console.log('⚠️ 消息正在加载中,跳过重复请求');
|
||
return;
|
||
}
|
||
|
||
// 设置加载状态
|
||
this.setData({ loadingMessages: true });
|
||
|
||
// 获取 NIM 实例
|
||
const nim = getApp().globalData.nim;
|
||
if (!nim || !nim.V2NIMMessageService) {
|
||
throw new Error('NIM SDK 未初始化');
|
||
}
|
||
|
||
// 使用 NIM SDK 获取历史消息
|
||
const limit = 20;
|
||
const option = {
|
||
conversationId: this.data.conversationId,
|
||
endTime: isLoadMore && this.data.lastMessageTime ? this.data.lastMessageTime : Date.now(),
|
||
limit: limit,
|
||
direction: 0 // 1 表示查询比 endTime 更早的消息
|
||
};
|
||
|
||
console.log('📡 查询参数:', option);
|
||
|
||
const result = await nim.V2NIMMessageService.getMessageList(option);
|
||
|
||
if (!result) {
|
||
console.log('⚠️ NIM SDK 返回空结果');
|
||
this.setData({ loadingMessages: false, hasMore: false });
|
||
return;
|
||
}
|
||
|
||
const newMessages = result || [];
|
||
console.log('✅ NIM 消息回调:', newMessages);
|
||
console.log('✅ 从 NIM 获取到消息:', newMessages.length, '条');
|
||
|
||
// 格式化消息
|
||
const formattedMessages = await Promise.all(
|
||
newMessages.map(msg => this.formatMessage(msg))
|
||
);
|
||
|
||
// 判断是否是首次加载
|
||
const isFirstLoad = this.data.messages.length === 0;
|
||
|
||
// 去重处理
|
||
const existingIds = new Set(this.data.messages.map(msg => msg.messageId));
|
||
const uniqueNewMessages = formattedMessages.filter(
|
||
msg => !existingIds.has(msg.messageId)
|
||
);
|
||
|
||
// 合并并排序消息
|
||
let sortedMessages;
|
||
if (isFirstLoad) {
|
||
sortedMessages = uniqueNewMessages.sort((a, b) => a.timestamp - b.timestamp);
|
||
} else {
|
||
const allMessages = uniqueNewMessages.concat(this.data.messages);
|
||
sortedMessages = allMessages.sort((a, b) => a.timestamp - b.timestamp);
|
||
}
|
||
|
||
// 更新 lastMessageTime(最早消息的时间戳,用于分页)
|
||
const earliestMessage = newMessages.length > 0
|
||
? newMessages[newMessages.length - 1]
|
||
: null;
|
||
const newLastMessageTime = earliestMessage?.createTime || earliestMessage?.timestamp || 0;
|
||
|
||
// 添加日期分隔符
|
||
const annotated = this.recomputeDateDividers(this._filterDeleted(sortedMessages));
|
||
|
||
// 更新数据
|
||
this.setData({
|
||
messages: annotated,
|
||
hasMore: newMessages.length === limit,
|
||
lastMessageTime: newLastMessageTime,
|
||
loadingMessages: false
|
||
});
|
||
|
||
console.log('✅ 消息加载完成:', {
|
||
totalMessages: sortedMessages.length,
|
||
hasMore: newMessages.length === limit,
|
||
lastMessageTime: newLastMessageTime
|
||
});
|
||
|
||
// 首次加载时滚动到底部
|
||
if (isFirstLoad && this.data.messages.length > 0) {
|
||
setTimeout(() => this.scrollToBottom(), 100);
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ 加载消息失败:', error);
|
||
this.setData({
|
||
hasMore: false,
|
||
loadingMessages: false
|
||
});
|
||
wx.showToast({
|
||
title: '加载消息失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
},
|
||
|
||
// 格式化消息 - 适配接口文档的数据结构,特殊处理图片消息
|
||
async formatMessage(msgData) {
|
||
// 获取当前用户的NIM账号ID(用于判断消息是否为自己发送)
|
||
let currentNimAccountId = this.data.userInfo?.neteaseIMAccid || this.data.userInfo?.user?.neteaseIMAccid || '';
|
||
|
||
// 如果从data中获取不到,尝试从全局获取
|
||
if (!currentNimAccountId) {
|
||
const app = getApp();
|
||
const globalUserInfo = app.globalData.userInfo;
|
||
currentNimAccountId = globalUserInfo?.neteaseIMAccid || globalUserInfo?.user?.neteaseIMAccid || '';
|
||
}
|
||
|
||
// 如果还是获取不到,尝试从本地存储获取
|
||
if (!currentNimAccountId) {
|
||
try {
|
||
const storedUserInfo = wx.getStorageSync('userInfo');
|
||
currentNimAccountId = storedUserInfo?.neteaseIMAccid || storedUserInfo?.user?.neteaseIMAccid || '';
|
||
} catch (error) {
|
||
console.error('从本地存储获取NIM账号ID失败:', error);
|
||
}
|
||
}
|
||
|
||
// 适配 NIM SDK V2 官方字段名
|
||
const senderId = msgData.senderId || msgData.senderAccountId;
|
||
const messageId = msgData.messageClientId || msgData.messageServerId || msgData.id || msgData.messageId;
|
||
const isSelf = senderId === currentNimAccountId;
|
||
|
||
// 🔥 直接使用 NIM SDK V2 的 messageType 枚举值(无需转换)
|
||
// V2NIMMessageType: 0=文本, 1=图片, 2=语音, 3=视频, 4=位置, 5=通知, 6=文件, 10=提示, 100=自定义
|
||
let messageType = msgData.messageType !== undefined ? msgData.messageType : msgData.msgType;
|
||
|
||
// 🔥 获取消息内容 - NIM SDK V2 使用 text(文本) 或 attachment(附件)
|
||
let _rawContent;
|
||
if (msgData.text !== undefined && msgData.text !== null) {
|
||
_rawContent = msgData.text;
|
||
} else if (msgData.attachment !== undefined && msgData.attachment !== null) {
|
||
_rawContent = msgData.attachment;
|
||
} else if (msgData.content !== undefined && msgData.content !== null) {
|
||
_rawContent = msgData.content;
|
||
} else {
|
||
_rawContent = '';
|
||
}
|
||
|
||
// 标记为图片消息(1)但内容不是可用图片 -> 降级为文本(0)
|
||
if (messageType === 1) {
|
||
let hasValidImage = false;
|
||
try {
|
||
if (typeof _rawContent === 'string') {
|
||
const s = _rawContent.trim();
|
||
if (/^https?:\/\//i.test(s) || /^data:image\//i.test(s)) {
|
||
hasValidImage = true;
|
||
} else if (s.startsWith('{') && s.endsWith('}')) {
|
||
const obj = JSON.parse(s);
|
||
const u = obj?.file_url || obj?.url;
|
||
hasValidImage = !!u;
|
||
}
|
||
} else if (typeof _rawContent === 'object' && _rawContent !== null) {
|
||
const u = _rawContent.file_url || _rawContent.url;
|
||
hasValidImage = !!u;
|
||
}
|
||
} catch (_) { hasValidImage = false; }
|
||
if (!hasValidImage) {
|
||
console.log('🔧 检测到图片类型但无有效URL,降级为文本');
|
||
messageType = 0; // 降级为文本
|
||
}
|
||
}
|
||
// 标记为文本(0)但内容像图片 -> 升级为图片(1)
|
||
if (messageType === 0 && this._looksLikeImageContent(_rawContent)) {
|
||
console.log('🖼️ 文本消息内容识别为图片,自动升级为图片类型');
|
||
messageType = 1;
|
||
}
|
||
|
||
// 🔥 撤回态识别(历史记录/服务端数据)
|
||
// 注意:撤回状态应该通过 msgData.isRecalled 字段判断,不应该依赖 sendingState
|
||
// sendingState 枚举值: 0=未知, 1=成功, 2=失败, 3=发送中
|
||
const recalledFromServer = !!msgData.isRecalled; // 只通过 isRecalled 字段判断是否撤回
|
||
|
||
// 🔥 特殊处理媒体消息内容
|
||
let processedContent = _rawContent;
|
||
if (messageType === 1) { // 图片
|
||
processedContent = await this.parseImageContent(_rawContent);
|
||
console.log('🖼️ 图片消息处理结果:', processedContent);
|
||
} else if (messageType === 2) { // 语音
|
||
processedContent = this.parseVoiceContent(_rawContent);
|
||
console.log('🎤 语音消息处理结果:', processedContent);
|
||
} else if (messageType === 6) { // 文件
|
||
processedContent = this.parseFileContent(_rawContent);
|
||
console.log('📄 文件消息处理结果:', processedContent);
|
||
} else if (messageType === 4) { // 位置
|
||
processedContent = this.parseLocationContent(_rawContent);
|
||
console.log('📍 位置消息处理结果:', processedContent);
|
||
} else if (messageType === 3) { // 视频
|
||
processedContent = this.parseVideoContent(_rawContent);
|
||
console.log('🎬 视频消息处理结果:', processedContent);
|
||
} else if (messageType === 0) { // 文本
|
||
processedContent = msgData.text || _rawContent;
|
||
}
|
||
|
||
// 🔥 统一时间戳字段 - NIM SDK V2 使用 createTime
|
||
let timestamp = 0;
|
||
try {
|
||
// 优先使用 createTime,兼容其他时间字段
|
||
if (typeof msgData.createTime === 'number') {
|
||
timestamp = msgData.createTime;
|
||
} else if (typeof msgData.sendTime === 'number') {
|
||
timestamp = msgData.sendTime;
|
||
} else if (typeof msgData.sendTime === 'string') {
|
||
const t = Date.parse(msgData.sendTime);
|
||
timestamp = isNaN(t) ? Date.now() : t;
|
||
} else {
|
||
timestamp = Number(msgData.timestamp || Date.now());
|
||
}
|
||
} catch (_) {
|
||
timestamp = Date.now();
|
||
}
|
||
|
||
// 🔥 获取发送者头像(已通过 NIM getUserInfo 获取并缓存)
|
||
const senderAvatar = isSelf ? (this.data.userAvatar || '') : (this.data.targetAvatar || '');
|
||
|
||
// 🔥 处理消息发送状态
|
||
let deliveryStatus;
|
||
if (isSelf) {
|
||
// 根据官方文档的正确枚举值处理
|
||
if (msgData.sendingState === 3) {
|
||
deliveryStatus = 3; // 发送中 (V2NIM_MESSAGE_SENDING_STATE_SENDING)
|
||
} else if (msgData.sendingState === 2) {
|
||
deliveryStatus = 2; // 发送失败 (V2NIM_MESSAGE_SENDING_STATE_FAILED)
|
||
} else {
|
||
// 对于历史消息,默认为成功状态
|
||
// 包括: sendingState为0(未知)、1(成功)、undefined、null或其他值的情况
|
||
deliveryStatus = 1; // V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED
|
||
console.log('📜 历史消息状态设为成功:', {
|
||
messageId: msgData.messageClientId || msgData.messageServerId,
|
||
sendingState: msgData.sendingState,
|
||
status: msgData.status,
|
||
finalStatus: deliveryStatus
|
||
});
|
||
}
|
||
} else {
|
||
// 对方消息:成功状态
|
||
deliveryStatus = 1;
|
||
}
|
||
|
||
const baseMessage = {
|
||
messageId: messageId,
|
||
senderId: senderId,
|
||
targetId: msgData.receiverId || msgData.targetId,
|
||
content: processedContent,
|
||
msgType: this.convertMessageTypeToString(messageType), // 🔥 转换为字符串类型 (text/image/audio/video/location/file)
|
||
sendTime: timestamp,
|
||
timestamp: timestamp,
|
||
isSelf: isSelf,
|
||
senderName: msgData.senderName || (isSelf ? '我' : '对方'),
|
||
senderAvatar: senderAvatar,
|
||
deliveryStatus: deliveryStatus,
|
||
bubbleTime: this.formatBubbleTime(timestamp),
|
||
_rawMessage: msgData // 🔥 保存 NIM SDK 原始消息对象,用于撤回等操作
|
||
};
|
||
|
||
// 🔥 调试:查看消息状态转换结果
|
||
console.log('🔍 消息状态转换:', {
|
||
isSelf: isSelf,
|
||
sendingState: msgData.sendingState,
|
||
status: msgData.status,
|
||
deliveryStatus: baseMessage.deliveryStatus,
|
||
messageId: baseMessage.messageId,
|
||
messageType: 'formatMessage调用 - 历史消息处理'
|
||
});
|
||
|
||
// NIM SDK V2 撤回的消息会通过 onMessageRevokeNotifications 事件通知
|
||
if (recalledFromServer) {
|
||
return {
|
||
...baseMessage,
|
||
isRecalled: true,
|
||
msgType: 'system', // 🔥 系统消息类型使用字符串
|
||
content: isSelf ? '你撤回了一条消息' : (msgData.senderName ? `${msgData.senderName}撤回了一条消息` : '对方撤回了一条消息'),
|
||
originalContent: processedContent
|
||
};
|
||
}
|
||
|
||
// 文件消息补充文件展示字段,便于WXML直接使用
|
||
if (messageType === 6) { // 文件
|
||
return {
|
||
...baseMessage,
|
||
fileName: processedContent.fileName || msgData.fileName || '',
|
||
fileSize: this.formatFileSize(processedContent.fileSize || msgData.fileSize || 0),
|
||
fileType: processedContent.fileType || msgData.fileType || ''
|
||
};
|
||
}
|
||
|
||
// 🔥 位置消息:为兼容旧渲染逻辑,将名称/地址/经纬度同步到根级字段,避免显示默认“位置”
|
||
if (messageType === 4) { // 位置
|
||
return {
|
||
...baseMessage,
|
||
locationName: processedContent?.locationName || msgData.locationName || '位置',
|
||
locationAddress: processedContent?.address || msgData.locationAddress || '',
|
||
latitude: processedContent?.latitude,
|
||
longitude: processedContent?.longitude
|
||
};
|
||
}
|
||
|
||
// 🔥 视频消息:将URL和缩略图同步到根级字段,兼容WXML渲染
|
||
if (messageType === 3) { // 视频
|
||
return {
|
||
...baseMessage,
|
||
content: processedContent.url || processedContent, // WXML 中使用 item.content 作为 src
|
||
thumbnail: processedContent.thumbnail || '',
|
||
duration: processedContent.duration || 0,
|
||
videoUrl: processedContent.url || ''
|
||
};
|
||
}
|
||
|
||
// 🔥 最终调试输出
|
||
console.log('✅ formatMessage 完成:', {
|
||
messageId,
|
||
msgType: baseMessage.msgType, // 使用转换后的字符串类型
|
||
msgTypeNumber: messageType, // 原始数字类型(用于调试)
|
||
content: processedContent,
|
||
deliveryStatus: baseMessage.deliveryStatus,
|
||
isSelf,
|
||
timestamp
|
||
});
|
||
|
||
return baseMessage;
|
||
},
|
||
|
||
// 🔥 解析语音内容 - 支持 NIM SDK V2 的 attachment 对象格式
|
||
parseVoiceContent(content) {
|
||
try {
|
||
if (!content) return { url: '', duration: 0 };
|
||
|
||
// NIM SDK V2 的语音附件对象格式
|
||
// attachment 通常包含: url, dur (duration), size 等字段
|
||
if (typeof content === 'object' && content !== null) {
|
||
const url = content.url || content.path || content.voiceUrl || '';
|
||
const duration = content.dur || content.duration || 0; // NIM SDK V2 使用 dur
|
||
const size = content.size || content.fileSize || 0;
|
||
|
||
console.log('🎤 语音附件对象解析:', { url, duration, size });
|
||
return { url, duration, size };
|
||
}
|
||
|
||
// 如果是字符串,尝试JSON解析
|
||
if (typeof content === 'string') {
|
||
const trimmed = content.trim();
|
||
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
||
try {
|
||
const obj = JSON.parse(trimmed);
|
||
const url = obj.url || obj.path || obj.voiceUrl || '';
|
||
const duration = obj.dur || obj.duration || 0;
|
||
const size = obj.size || obj.fileSize || 0;
|
||
return { url, duration, size };
|
||
} catch (e) {
|
||
// 继续按URL兜底
|
||
}
|
||
}
|
||
// 直接当作URL使用
|
||
return { url: trimmed, duration: 0 };
|
||
}
|
||
|
||
return { url: '', duration: 0 };
|
||
} catch (error) {
|
||
console.error('❌ 解析语音内容失败:', error, content);
|
||
return { url: '', duration: 0 };
|
||
}
|
||
},
|
||
|
||
// 🔥 解析文件内容 - 支持 NIM SDK V2 的 attachment 对象格式
|
||
parseFileContent(content) {
|
||
try {
|
||
if (!content) return { url: '', fileName: '', fileSize: 0 };
|
||
|
||
// NIM SDK V2 的文件附件对象格式
|
||
if (typeof content === 'object' && content !== null) {
|
||
const url = content.url || content.path || content.file_url || content.fileUrl || '';
|
||
const fileName = content.name || content.fileName || content.file_name || '';
|
||
const fileSize = content.size || content.fileSize || content.file_size || 0;
|
||
const ext = content.ext || content.fileType || content.file_type || '';
|
||
|
||
console.log('📄 文件附件对象解析:', { url, fileName, fileSize, ext });
|
||
return { url, fileName, fileSize, fileType: ext };
|
||
}
|
||
|
||
// JSON字符串
|
||
if (typeof content === 'string') {
|
||
const trimmed = content.trim();
|
||
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
||
try {
|
||
const obj = JSON.parse(trimmed);
|
||
const url = obj.url || obj.path || obj.file_url || obj.fileUrl || '';
|
||
const fileName = obj.name || obj.fileName || obj.file_name || '';
|
||
const fileSize = obj.size || obj.fileSize || obj.file_size || 0;
|
||
const fileType = obj.ext || obj.fileType || obj.file_type || '';
|
||
return { url, fileName, fileSize, fileType };
|
||
} catch (e) {
|
||
// fallthrough to plain URL
|
||
}
|
||
}
|
||
// 纯URL
|
||
return { url: trimmed, fileName: '', fileSize: 0 };
|
||
}
|
||
|
||
return { url: '', fileName: '', fileSize: 0 };
|
||
} catch (error) {
|
||
console.error('❌ 解析文件内容失败:', error, content);
|
||
return { url: '', fileName: '', fileSize: 0 };
|
||
}
|
||
},
|
||
|
||
// 🔥 解析位置内容 - 支持 NIM SDK V2 的 attachment 对象格式
|
||
parseLocationContent(content) {
|
||
try {
|
||
if (!content) return null;
|
||
let obj = content;
|
||
|
||
// NIM SDK V2 的位置附件对象格式
|
||
// attachment 通常包含: lat, lng, title, address 等字段
|
||
if (typeof content === 'string') {
|
||
const trimmed = content.trim();
|
||
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
||
try {
|
||
obj = JSON.parse(trimmed);
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
} else {
|
||
// 非JSON字符串,无法解析
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// NIM SDK V2 使用 lat/lng,兼容 latitude/longitude
|
||
const lat = obj.lat ?? obj.latitude;
|
||
const lng = obj.lng ?? obj.longitude;
|
||
const address = obj.address || obj.addr || '';
|
||
const locationName = obj.title || obj.locationName || obj.name || '位置';
|
||
|
||
console.log('📍 位置附件对象解析:', { lat, lng, address, locationName });
|
||
|
||
if (lat == null || lng == null) return null;
|
||
return { latitude: Number(lat), longitude: Number(lng), address, locationName };
|
||
} catch (err) {
|
||
console.error('❌ 解析位置内容失败:', err, content);
|
||
return null;
|
||
}
|
||
},
|
||
|
||
// 🔥 解析视频内容 - 支持 NIM SDK V2 的 attachment 对象格式
|
||
parseVideoContent(content) {
|
||
try {
|
||
if (!content) return { url: '', thumbnail: '' };
|
||
|
||
// NIM SDK V2 的视频附件对象格式
|
||
if (typeof content === 'object' && content !== null) {
|
||
const url = content.url || content.path || '';
|
||
const thumbnail = content.thumbUrl || content.coverUrl || content.thumbnail || '';
|
||
const duration = content.dur || content.duration || 0;
|
||
const size = content.size || content.fileSize || 0;
|
||
const width = content.width || content.w || 0;
|
||
const height = content.height || content.h || 0;
|
||
|
||
console.log('🎬 视频附件对象解析:', { url, thumbnail, duration, size, width, height });
|
||
return { url, thumbnail, duration, size, width, height };
|
||
}
|
||
|
||
// JSON 字符串
|
||
if (typeof content === 'string') {
|
||
const trimmed = content.trim();
|
||
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
||
try {
|
||
const obj = JSON.parse(trimmed);
|
||
const url = obj.url || obj.path || '';
|
||
const thumbnail = obj.thumbUrl || obj.coverUrl || obj.thumbnail || '';
|
||
const duration = obj.dur || obj.duration || 0;
|
||
return { url, thumbnail, duration };
|
||
} catch (e) {
|
||
// 继续按URL兜底
|
||
}
|
||
}
|
||
// 直接当作URL使用
|
||
return { url: trimmed, thumbnail: '' };
|
||
}
|
||
|
||
return { url: '', thumbnail: '' };
|
||
} catch (error) {
|
||
console.error('❌ 解析视频内容失败:', error, content);
|
||
return { url: '', thumbnail: '' };
|
||
}
|
||
},
|
||
|
||
// 🔥 根据官方文档,NIM SDK V2 的正确枚举值(0=未知, 1=成功, 2=失败, 3=发送中)
|
||
convertStatus(apiStatus) {
|
||
// NIM SDK V2 的 V2NIMMessageSendingState 枚举:
|
||
// V2NIM_MESSAGE_SENDING_STATE_UNKNOWN = 0 (未知状态)
|
||
// V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED = 1 (发送成功)
|
||
// V2NIM_MESSAGE_SENDING_STATE_FAILED = 2 (发送失败)
|
||
// V2NIM_MESSAGE_SENDING_STATE_SENDING = 3 (发送中)
|
||
|
||
console.log('🔄 convertStatus 输入:', {
|
||
apiStatus,
|
||
type: typeof apiStatus,
|
||
isUndefined: apiStatus === undefined,
|
||
isNull: apiStatus === null
|
||
});
|
||
|
||
// 🔥 直接返回数字枚举值,不转换为字符串
|
||
// undefined/null 默认为成功状态 1 (历史消息通常没有状态字段,说明已成功)
|
||
if (apiStatus === undefined || apiStatus === null) {
|
||
console.log('🔄 状态映射 convertStatus: undefined/null -> 1 (历史消息默认成功)');
|
||
return 1; // V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED
|
||
}
|
||
|
||
// 如果已经是数字,直接返回
|
||
if (typeof apiStatus === 'number') {
|
||
// 验证是否为有效的 sendingState 枚举值 (0, 1, 2, 3)
|
||
if (apiStatus === 0 || apiStatus === 1 || apiStatus === 2 || apiStatus === 3) {
|
||
console.log('🔄 状态映射 convertStatus:', apiStatus, '(有效枚举值,保持不变)');
|
||
return apiStatus;
|
||
}
|
||
// 对于历史消息,其他数字状态也默认为成功(因为能从服务器获取到说明已发送成功)
|
||
console.log('🔄 状态映射 convertStatus:', apiStatus, '-> 1 (历史消息其他数字状态,默认成功)');
|
||
return 1;
|
||
}
|
||
|
||
// 字符串状态转换为枚举值
|
||
const s = String(apiStatus).toLowerCase();
|
||
let result;
|
||
if (s === 'succeeded' || s === 'success' || s === 'sent' || s === 'delivered') {
|
||
result = 1; // SUCCEEDED
|
||
} else if (s === 'failed' || s === 'fail') {
|
||
result = 2; // FAILED
|
||
} else if (s === 'sending') {
|
||
result = 3; // SENDING
|
||
} else {
|
||
// 对于历史消息,未知状态默认为成功
|
||
result = 1; // 默认成功
|
||
}
|
||
|
||
console.log('🔄 状态映射 convertStatus:', apiStatus, '->', result, '(历史消息处理)');
|
||
return result;
|
||
},
|
||
|
||
// 🔥 解析图片内容 - 支持 NIM SDK V2 的 attachment 对象格式
|
||
async parseImageContent(content) {
|
||
try {
|
||
|
||
if (!content) return { type: 'url', url: '' };
|
||
|
||
// 1) NIM SDK V2 的 attachment 对象格式
|
||
// attachment 通常包含: url, name, size, width, height 等字段
|
||
if (typeof content === 'object') {
|
||
// NIM SDK V2 图片附件字段
|
||
const url = content.url || content.file_url || content.path || '';
|
||
const fileName = content.name || content.fileName || content.file_name || '';
|
||
const fileSize = content.size || content.fileSize || content.file_size || 0;
|
||
const width = content.width || content.w || 0;
|
||
const height = content.height || content.h || 0;
|
||
|
||
console.log('🖼️ 图片附件对象解析:', { url, fileName, fileSize, width, height });
|
||
|
||
if (/^data:image\//i.test(url)) {
|
||
return { type: 'base64', url, fileName, fileSize, width, height };
|
||
}
|
||
if (url) {
|
||
const cachedUrl = await imageCacheManager.preloadImage(url);
|
||
return { type: 'url', url: cachedUrl, fileName, fileSize, width, height };
|
||
}
|
||
// 对象但无 url,视为错误
|
||
return { type: 'error', url: '', error: '图片内容缺少URL', originalContent: JSON.stringify(content).slice(0, 200) };
|
||
}
|
||
|
||
// 2) 字符串:Base64
|
||
if (typeof content === 'string' && /^data:image\//i.test(content)) {
|
||
return { type: 'base64', url: content };
|
||
}
|
||
|
||
// 3) 字符串:URL
|
||
if (typeof content === 'string' && /^https?:\/\//i.test(content)) {
|
||
const cachedUrl = await imageCacheManager.preloadImage(content);
|
||
return { type: 'url', url: cachedUrl };
|
||
}
|
||
|
||
// 4) 字符串:尝试 JSON 解析
|
||
if (typeof content === 'string') {
|
||
const s = content.trim();
|
||
if (s.startsWith('{') && s.endsWith('}')) {
|
||
try {
|
||
const obj = JSON.parse(s);
|
||
const u = obj.url || obj.file_url || obj.path || '';
|
||
if (u) {
|
||
const cachedUrl = await imageCacheManager.preloadImage(u);
|
||
return {
|
||
type: /^data:image\//i.test(u) ? 'base64' : 'url',
|
||
url: cachedUrl,
|
||
fileName: obj.name || obj.file_name || obj.fileName,
|
||
fileSize: obj.size || obj.file_size || obj.fileSize,
|
||
width: obj.width || obj.w,
|
||
height: obj.height || obj.h
|
||
};
|
||
}
|
||
} catch (e) {
|
||
// fallthrough
|
||
}
|
||
}
|
||
}
|
||
|
||
// 5) 其他:无法识别,按错误返回,避免把纯文本当图片
|
||
return { type: 'error', url: '', error: '图片内容无效', originalContent: String(content).slice(0, 200) };
|
||
} catch (err) {
|
||
console.error('❌ 解析图片内容失败:', err, content);
|
||
return { type: 'error', url: '', error: '解析失败', originalContent: String(content).slice(0, 200) };
|
||
}
|
||
},
|
||
|
||
// 🔥 新增:解析视频内容,统一返回可用于 <video src> 的字符串URL
|
||
parseVideoContent(content) {
|
||
try {
|
||
if (!content) return '';
|
||
|
||
// 对象格式
|
||
if (typeof content === 'object') {
|
||
const url = content.url || content.file_url || content.fileUrl || '';
|
||
return String(url || '');
|
||
}
|
||
|
||
// 字符串格式:可能是JSON或URL
|
||
const str = String(content).trim();
|
||
if (!str) return '';
|
||
if (str.startsWith('{') && str.endsWith('}')) {
|
||
try {
|
||
const obj = JSON.parse(str);
|
||
const url = obj.url || obj.file_url || obj.fileUrl || '';
|
||
return String(url || '');
|
||
} catch (_) {
|
||
// 继续按纯URL处理
|
||
}
|
||
}
|
||
return str;
|
||
} catch (e) {
|
||
console.error('❌ 解析视频内容失败:', e, content);
|
||
return '';
|
||
}
|
||
},
|
||
|
||
// 判断是否显示时间
|
||
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}`;
|
||
},
|
||
|
||
// 🔥 过滤已删除的消息
|
||
_filterDeleted(messages) {
|
||
if (!Array.isArray(messages)) return [];
|
||
return messages.filter(msg => !msg.isDeleted);
|
||
},
|
||
|
||
// 重新计算日期分隔(仅按日期),返回新数组
|
||
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 safe = this._filterDeleted(messages);
|
||
const annotated = this.recomputeDateDividers(safe);
|
||
console.log('📋 setMessagesWithDate 执行:', {
|
||
输入消息数: messages.length,
|
||
过滤后消息数: safe.length,
|
||
标注后消息数: annotated.length,
|
||
最后一条消息: annotated[annotated.length - 1]
|
||
});
|
||
|
||
this.setData({ messages: annotated });
|
||
},
|
||
|
||
// 输入变化
|
||
onInputChange(e) {
|
||
const inputText = e.detail.value;
|
||
this.setData({ inputText });
|
||
},
|
||
|
||
// 处理中文输入法开始
|
||
onCompositionStart(e) {
|
||
this.setData({ isComposing: true });
|
||
},
|
||
|
||
// 处理中文输入法结束
|
||
onCompositionEnd(e) {
|
||
this.setData({ isComposing: false });
|
||
},
|
||
|
||
// 处理文本框行数变化
|
||
onLineChange(e) {
|
||
|
||
// 可以在这里处理输入框高度变化的逻辑
|
||
},
|
||
|
||
// 显示媒体选择菜单
|
||
showMediaPicker() {
|
||
|
||
// 进入媒体选择时先收起工具栏,避免遮挡
|
||
this.setData({
|
||
showMorePanel: false,
|
||
showEmojiPanel: false,
|
||
emojiPanelHeight: 0,
|
||
keyboardHeight: 0,
|
||
showVoiceRecorder: false,
|
||
inputType: 'text'
|
||
});
|
||
|
||
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() {
|
||
|
||
try {
|
||
// 开始拍照前关闭工具栏
|
||
this.setData({ showMorePanel: false, showEmojiPanel: false, emojiPanelHeight: 0, keyboardHeight: 0, showVoiceRecorder: false, inputType: 'text' });
|
||
|
||
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() {
|
||
|
||
try {
|
||
// 打开相册前先收起工具栏
|
||
this.setData({ showMorePanel: false, showEmojiPanel: false, emojiPanelHeight: 0, keyboardHeight: 0, showVoiceRecorder: false, inputType: 'text' });
|
||
|
||
// 在相册内二次分流:图片 或 视频
|
||
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) {
|
||
// 保险再截断一次
|
||
const files = result.files.slice(0, 9);
|
||
if (result.files.length > 9) {
|
||
wx.showToast({ title: '最多发送9张图片', icon: 'none' });
|
||
}
|
||
for (const image of files) {
|
||
await this.sendImageMessage(image);
|
||
}
|
||
}
|
||
} else if (tapIndex === 1) {
|
||
// 选择视频
|
||
const result = await mediaPicker.chooseVideo({
|
||
sourceType: ['album'],
|
||
maxDuration: 60,
|
||
compressed: true
|
||
});
|
||
if (result.success && Array.isArray(result.files) && result.files.length > 0) {
|
||
const video = result.files[0];
|
||
if (video.size && video.size > 200 * 1024 * 1024) {
|
||
wx.showToast({ title: '视频超过200MB,无法发送', icon: 'none' });
|
||
return;
|
||
}
|
||
// 简单压缩提示(如果支持,可扩展自定义压缩逻辑)
|
||
if (!video.compressed && video.tempFilePath) {
|
||
// 微信 chooseVideo 已有压缩,兜底提示
|
||
|
||
}
|
||
await this.sendVideoMessage(video);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ 处理相册选择发生异常:', error);
|
||
}
|
||
},
|
||
|
||
// 拍摄视频
|
||
async recordVideo() {
|
||
|
||
try {
|
||
// 开始录制前关闭工具栏
|
||
this.setData({ showMorePanel: false, showEmojiPanel: false, emojiPanelHeight: 0, keyboardHeight: 0, showVoiceRecorder: false, inputType: 'text' });
|
||
|
||
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'
|
||
});
|
||
}
|
||
},
|
||
|
||
// 🔥 头像加载错误处理
|
||
onAvatarError(e) {
|
||
console.error('❌ 头像加载失败:', e.detail);
|
||
|
||
|
||
// 🔥 尝试从dataset中获取更多信息
|
||
const dataset = e.currentTarget.dataset;
|
||
|
||
},
|
||
|
||
// 🔥 头像加载成功处理
|
||
onAvatarLoad(e) {
|
||
|
||
},
|
||
|
||
// 🔥 ===== 媒体消息发送功能 =====
|
||
|
||
// 发送图片消息 - 兼容媒体选择器
|
||
async sendImageMessage(imageFile) {
|
||
|
||
try {
|
||
// 如果传入的是字符串路径,转换为文件对象格式
|
||
if (typeof imageFile === 'string') {
|
||
imageFile = { path: imageFile };
|
||
}
|
||
|
||
// 检查用户信息
|
||
if (!this.data.userInfo) {
|
||
console.error('❌ 用户信息不存在,无法发送图片');
|
||
return;
|
||
}
|
||
|
||
// 检查文件大小
|
||
if (imageFile.size && imageFile.size > 10 * 1024 * 1024) { // 10MB限制
|
||
wx.showToast({
|
||
title: '图片大小不能超过10MB',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 🔥 发送开始后立即收起工具栏/面板,回到聊天界面
|
||
this.setData({
|
||
showMorePanel: false,
|
||
showEmojiPanel: false,
|
||
emojiPanelHeight: 0,
|
||
keyboardHeight: 0,
|
||
showVoiceRecorder: false,
|
||
inputType: 'text'
|
||
});
|
||
|
||
wx.showLoading({ title: '发送中...' });
|
||
|
||
const imagePath = imageFile.path;
|
||
console.log('📸 图片本地路径:', imagePath);
|
||
|
||
// 🔥 获取当前用户的NIM账号ID
|
||
const currentNimAccountId = this.data.userInfo?.neteaseIMAccid || this.data.userInfo?.user?.neteaseIMAccid || '';
|
||
if (!currentNimAccountId) {
|
||
throw new Error('无法获取NIM账号ID');
|
||
}
|
||
|
||
// 🔥 创建临时图片消息
|
||
const timestamp = Date.now();
|
||
const tempMessage = {
|
||
messageId: 'temp_image_' + timestamp,
|
||
senderId: currentNimAccountId,
|
||
targetId: this.data.targetId,
|
||
content: {
|
||
url: imagePath // 临时使用本地路径
|
||
},
|
||
msgType: 1, // 图片
|
||
sendTime: timestamp,
|
||
timestamp: timestamp,
|
||
isSelf: true,
|
||
senderName: '我',
|
||
senderAvatar: this.data.userAvatar || '',
|
||
deliveryStatus: 3, // 发送中
|
||
bubbleTime: this.formatBubbleTime(timestamp)
|
||
};
|
||
|
||
// 添加到消息列表
|
||
const messages = this.data.messages.concat([tempMessage]);
|
||
this.setMessagesWithDate(messages);
|
||
setTimeout(() => this.scrollToBottom(), 100);
|
||
|
||
// 🔥 使用 NIM SDK 发送图片消息(SDK会自动上传)
|
||
const result = await nimConversationManager.sendImageMessage(
|
||
this.data.conversationId,
|
||
imagePath
|
||
);
|
||
|
||
wx.hideLoading();
|
||
|
||
if (result && result.message) {
|
||
// 🔥 格式化并替换临时消息
|
||
const sentMessage = await this.formatMessage(result.message);
|
||
sentMessage.deliveryStatus = 1; // 发送成功 (V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED)
|
||
|
||
// 替换临时消息
|
||
const tempIndex = this.data.messages.findIndex(msg => msg.messageId === tempMessage.messageId);
|
||
if (tempIndex !== -1) {
|
||
const updatedMessages = [...this.data.messages];
|
||
updatedMessages[tempIndex] = sentMessage;
|
||
this.setMessagesWithDate(updatedMessages);
|
||
console.log('✅ 图片消息UI更新成功');
|
||
}
|
||
} else {
|
||
throw new Error('NIM SDK 发送图片失败');
|
||
}
|
||
|
||
} catch (error) {
|
||
wx.hideLoading();
|
||
console.error('❌ 发送图片消息失败:', error);
|
||
|
||
// 更新临时消息状态为失败
|
||
const messages = this.data.messages.map(msg => {
|
||
if (msg.messageId && msg.messageId.startsWith('temp_image_')) {
|
||
return { ...msg, deliveryStatus: 1 }; // 发送失败
|
||
}
|
||
return msg;
|
||
});
|
||
this.setMessagesWithDate(messages);
|
||
|
||
wx.showToast({
|
||
title: '图片发送失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
},
|
||
|
||
// 发送视频消息 - 兼容媒体选择器
|
||
async sendVideoMessage(videoFile) {
|
||
|
||
try {
|
||
// 如果传入的是字符串路径,转换为文件对象格式
|
||
if (typeof videoFile === 'string') {
|
||
videoFile = { path: videoFile };
|
||
}
|
||
|
||
// 检查用户信息
|
||
if (!this.data.userInfo) {
|
||
console.error('❌ 用户信息不存在,无法发送视频');
|
||
return;
|
||
}
|
||
|
||
// 检查文件大小
|
||
if (videoFile.size && videoFile.size > 200 * 1024 * 1024) { // 200MB限制
|
||
wx.showToast({
|
||
title: '视频不能超过200MB',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 🔥 发送开始后立即收起工具栏/面板,回到聊天界面
|
||
this.setData({
|
||
showMorePanel: false,
|
||
showEmojiPanel: false,
|
||
emojiPanelHeight: 0,
|
||
keyboardHeight: 0,
|
||
showVoiceRecorder: false,
|
||
inputType: 'text'
|
||
});
|
||
|
||
wx.showLoading({ title: '发送中...' });
|
||
|
||
const videoPath = videoFile.path;
|
||
const duration = videoFile.duration || 0; // 视频时长(秒)
|
||
console.log('🎥 视频本地路径:', videoPath, '时长:', duration);
|
||
|
||
// 🔥 获取当前用户的NIM账号ID
|
||
const currentNimAccountId = this.data.userInfo?.neteaseIMAccid || this.data.userInfo?.user?.neteaseIMAccid || '';
|
||
if (!currentNimAccountId) {
|
||
throw new Error('无法获取NIM账号ID');
|
||
}
|
||
|
||
// 🔥 创建临时视频消息
|
||
const timestamp = Date.now();
|
||
const tempMessage = {
|
||
messageId: 'temp_video_' + timestamp,
|
||
senderId: currentNimAccountId,
|
||
targetId: this.data.targetId,
|
||
content: {
|
||
url: videoPath, // 临时使用本地路径
|
||
duration: duration
|
||
},
|
||
msgType: 3, // 视频
|
||
sendTime: timestamp,
|
||
timestamp: timestamp,
|
||
isSelf: true,
|
||
senderName: '我',
|
||
senderAvatar: this.data.userAvatar || '',
|
||
deliveryStatus: 3, // 发送中 (V2NIM_MESSAGE_SENDING_STATE_SENDING)
|
||
bubbleTime: this.formatBubbleTime(timestamp)
|
||
};
|
||
|
||
// 添加到消息列表
|
||
const messages = this.data.messages.concat([tempMessage]);
|
||
this.setMessagesWithDate(messages);
|
||
setTimeout(() => this.scrollToBottom(), 100);
|
||
|
||
// 🔥 使用 NIM SDK 发送视频消息(SDK会自动上传)
|
||
const result = await nimConversationManager.sendVideoMessage(
|
||
this.data.conversationId,
|
||
videoPath,
|
||
duration * 1000 // NIM SDK 需要毫秒
|
||
);
|
||
|
||
wx.hideLoading();
|
||
|
||
if (result && result.message) {
|
||
// 🔥 格式化并替换临时消息
|
||
const sentMessage = await this.formatMessage(result.message);
|
||
sentMessage.deliveryStatus = 1; // 发送成功 (V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED)
|
||
|
||
// 替换临时消息
|
||
const tempIndex = this.data.messages.findIndex(msg => msg.messageId === tempMessage.messageId);
|
||
if (tempIndex !== -1) {
|
||
const updatedMessages = [...this.data.messages];
|
||
updatedMessages[tempIndex] = sentMessage;
|
||
this.setMessagesWithDate(updatedMessages);
|
||
console.log('✅ 视频消息UI更新成功');
|
||
}
|
||
} else {
|
||
throw new Error('NIM SDK 发送视频失败');
|
||
}
|
||
|
||
} catch (error) {
|
||
wx.hideLoading();
|
||
console.error('❌ 发送视频消息失败:', error);
|
||
|
||
// 更新临时消息状态为失败
|
||
const messages = this.data.messages.map(msg => {
|
||
if (msg.messageId && msg.messageId.startsWith('temp_video_')) {
|
||
return { ...msg, deliveryStatus: 1 }; // 发送失败
|
||
}
|
||
return msg;
|
||
});
|
||
this.setMessagesWithDate(messages);
|
||
|
||
wx.showToast({
|
||
title: '视频发送失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
},
|
||
|
||
// 🔥 ===== 消息发送功能 =====
|
||
|
||
// 发送文本消息 - 使用 NIM SDK
|
||
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,
|
||
emojiPanelHeight: 0,
|
||
showMorePanel: false
|
||
});
|
||
|
||
// 🔥 获取当前用户的NIM账号ID(用于临时消息的senderId,保持与实际消息一致)
|
||
const currentNimAccountId = this.data.userInfo?.neteaseIMAccid || this.data.userInfo?.user?.neteaseIMAccid || '';
|
||
if (!currentNimAccountId) {
|
||
console.error('无法获取NIM账号ID');
|
||
wx.showToast({
|
||
title: 'NIM账号错误',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 创建临时消息(使用NIM账号ID作为senderId,确保与实际消息格式一致)
|
||
const timestamp = Date.now();
|
||
const tempMessage = {
|
||
messageId: 'temp_' + timestamp,
|
||
senderId: currentNimAccountId, // 🔥 使用NIM账号ID,而不是customId
|
||
targetId: this.data.targetId,
|
||
content: content,
|
||
msgType: 'text', // 文本
|
||
sendTime: timestamp,
|
||
timestamp: timestamp,
|
||
isSelf: true,
|
||
senderName: '我',
|
||
senderAvatar: this.data.userAvatar || '',
|
||
deliveryStatus: 3, // V2NIM_MESSAGE_SENDING_STATE_SENDING
|
||
bubbleTime: this.formatBubbleTime(timestamp)
|
||
};
|
||
|
||
|
||
// 添加到消息列表
|
||
const messages = this.data.messages.concat([tempMessage]);
|
||
this.setMessagesWithDate(messages);
|
||
|
||
// 滚动到底部
|
||
setTimeout(() => this.scrollToBottom(), 100);
|
||
|
||
try {
|
||
console.log(' 准备发送文本消息 (NIM SDK):', {
|
||
conversationId: this.data.conversationId,
|
||
content: content
|
||
});
|
||
|
||
// 使用 NIM SDK 发送文本消息
|
||
const result = await nimConversationManager.sendTextMessage(
|
||
this.data.conversationId,
|
||
content
|
||
);
|
||
|
||
if (result && result.message) {
|
||
console.log('✅ 消息发送成功');
|
||
|
||
const sentMessage = await this.formatMessage(result.message);
|
||
sentMessage.deliveryStatus = 1; // V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED
|
||
console.log('✅ 强制设置消息状态为成功 (deliveryStatus = 1)');
|
||
|
||
// 🔥 使用临时消息的索引位置来替换,而不是依赖 messageId 匹配
|
||
const tempIndex = this.data.messages.findIndex(msg => msg.messageId === tempMessage.messageId);
|
||
|
||
if (tempIndex !== -1) {
|
||
// 替换临时消息
|
||
const updatedMessages = [...this.data.messages];
|
||
updatedMessages[tempIndex] = sentMessage;
|
||
this.setMessagesWithDate(updatedMessages);
|
||
|
||
console.log('✅ 消息UI更新成功,新消息ID:', sentMessage.messageId);
|
||
} else {
|
||
// 临时消息未找到,直接添加实际消息
|
||
console.warn('⚠️ 未找到临时消息,直接添加实际消息');
|
||
const updatedMessages = this.data.messages.concat([sentMessage]);
|
||
this.setMessagesWithDate(updatedMessages);
|
||
}
|
||
} else {
|
||
throw new Error('NIM SDK 发送消息失败');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('发送消息失败:', error);
|
||
// 更新消息状态为失败
|
||
this.updateMessageDeliveryStatus(tempMessage.messageId, 1); // V2NIM_MESSAGE_SENDING_STATE_FAILED
|
||
wx.showToast({
|
||
title: '发送失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
},
|
||
|
||
// 更新消息发送状态
|
||
updateMessageDeliveryStatus(messageId, status) {
|
||
const messages = this.data.messages.map(msg => {
|
||
if (msg.messageId === messageId) {
|
||
return { ...msg, deliveryStatus: status };
|
||
}
|
||
return msg;
|
||
});
|
||
this.setData({ messages });
|
||
},
|
||
|
||
// 标记所有消息已读(进入聊天页面时调用)
|
||
async markAllMessagesRead() {
|
||
try {
|
||
if (!this.data.conversationId || this.data.conversationId.trim() === '') {
|
||
console.log('⚠️ 会话ID为空,跳过标记已读');
|
||
return;
|
||
}
|
||
|
||
console.log('📖 标记会话所有消息已读 (NIM SDK):', this.data.conversationId);
|
||
|
||
// 使用 NIM SDK 标记已读
|
||
await nimConversationManager.markConversationRead(this.data.conversationId);
|
||
|
||
console.log('✅ 消息已全部标记已读');
|
||
|
||
// 更新本地消息状态
|
||
const updatedMessages = this.data.messages.map(msg => ({
|
||
...msg,
|
||
deliveryStatus: msg.isSelf ? msg.deliveryStatus : 'read'
|
||
}));
|
||
|
||
this.setData({
|
||
messages: updatedMessages
|
||
});
|
||
} catch (error) {
|
||
console.error('❌ 标记消息已读失败:', error);
|
||
// 不显示错误提示,避免影响用户体验
|
||
}
|
||
},
|
||
|
||
// 切换输入类型
|
||
toggleInputType() {
|
||
const inputType = this.data.inputType === 'text' ? 'voice' : 'text';
|
||
|
||
// 关闭表情面板和更多面板
|
||
this.closeEmojiPanel();
|
||
|
||
this.setData({
|
||
inputType,
|
||
showMorePanel: false,
|
||
keyboardHeight: 0 // 切换输入类型时重置键盘高度
|
||
});
|
||
},
|
||
|
||
// 🔥 ===== 语音录制触摸事件处理 =====
|
||
|
||
// 开始录音(按下)
|
||
onVoiceTouchStart(e) {
|
||
console.log('🎤 开始录音');
|
||
|
||
// 🔥 防抖检查:如果上次录音结束时间距离现在太近,忽略本次点击
|
||
const now = Date.now();
|
||
if (this.lastVoiceEndTime && (now - this.lastVoiceEndTime) < 500) {
|
||
console.log('⚠️ 点击过快,忽略本次录音');
|
||
wx.showToast({
|
||
title: '要点坏了~~~~~',
|
||
icon: 'none',
|
||
duration: 1000
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 🔥 如果正在录音中,忽略重复点击
|
||
if (this.data.recording) {
|
||
console.log('⚠️ 正在录音中,忽略重复点击');
|
||
return;
|
||
}
|
||
|
||
// 设置录音状态
|
||
this.setData({
|
||
recording: true,
|
||
voiceCancel: false
|
||
});
|
||
|
||
// 重置取消标记
|
||
this.voiceRecordCancelled = false;
|
||
|
||
// 🔥 延迟开始录音,防止误触
|
||
this.voiceStartTimer = setTimeout(() => {
|
||
// 再次检查是否仍在录音状态(用户可能已经松开了)
|
||
if (this.data.recording && !this.voiceRecordCancelled) {
|
||
this.startVoiceRecord();
|
||
} else {
|
||
console.log('⚠️ 用户已松开,取消录音启动');
|
||
this.setData({ recording: false });
|
||
}
|
||
}, 150); // 延迟150ms开始录音
|
||
},
|
||
|
||
// 手指移动(判断是否取消)
|
||
onVoiceTouchMove(e) {
|
||
const touch = e.touches[0];
|
||
const target = e.currentTarget;
|
||
|
||
// 获取按钮位置信息
|
||
const query = wx.createSelectorQuery();
|
||
query.select('.voice-btn').boundingClientRect();
|
||
query.exec((res) => {
|
||
if (res && res[0]) {
|
||
const rect = res[0];
|
||
// 判断手指是否移出按钮区域(上滑取消)
|
||
const isOutside = touch.clientY < rect.top - 50; // 上移50px以上取消
|
||
|
||
this.setData({ voiceCancel: isOutside });
|
||
|
||
if (isOutside) {
|
||
console.log('⚠️ 上滑取消录音');
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
// 结束录音(松开)
|
||
onVoiceTouchEnd(e) {
|
||
console.log('🎤 结束录音');
|
||
|
||
// 🔥 清除延迟启动定时器
|
||
if (this.voiceStartTimer) {
|
||
clearTimeout(this.voiceStartTimer);
|
||
this.voiceStartTimer = null;
|
||
}
|
||
|
||
const voiceCancel = this.data.voiceCancel;
|
||
|
||
// 🔥 记录本次录音结束时间,用于防抖
|
||
this.lastVoiceEndTime = Date.now();
|
||
|
||
// 重置录音状态
|
||
this.setData({
|
||
recording: false,
|
||
voiceCancel: false
|
||
});
|
||
|
||
if (voiceCancel) {
|
||
// 🔥 设置取消标记,防止onStop回调发送消息
|
||
this.voiceRecordCancelled = true;
|
||
// 取消录音
|
||
this.cancelVoiceRecord();
|
||
} else {
|
||
// 完成录音并发送
|
||
this.stopVoiceRecord();
|
||
}
|
||
},
|
||
|
||
// 录音被中断(如来电)
|
||
onVoiceTouchCancel(e) {
|
||
console.log('⚠️ 录音被中断');
|
||
|
||
// 🔥 清除延迟启动定时器
|
||
if (this.voiceStartTimer) {
|
||
clearTimeout(this.voiceStartTimer);
|
||
this.voiceStartTimer = null;
|
||
}
|
||
|
||
this.setData({
|
||
recording: false,
|
||
voiceCancel: false
|
||
});
|
||
|
||
// 🔥 设置取消标记
|
||
this.voiceRecordCancelled = true;
|
||
|
||
// 🔥 记录结束时间
|
||
this.lastVoiceEndTime = Date.now();
|
||
|
||
// 取消录音
|
||
this.cancelVoiceRecord();
|
||
},
|
||
|
||
// 开始录音
|
||
startVoiceRecord() {
|
||
try {
|
||
const recorderManager = wx.getRecorderManager();
|
||
|
||
recorderManager.onStart(() => {
|
||
console.log('✅ 录音开始');
|
||
wx.vibrateShort(); // 震动反馈
|
||
});
|
||
|
||
// 🔥 在这里注册 onStop 回调,只注册一次
|
||
recorderManager.onStop((res) => {
|
||
console.log('✅ 录音结束 - 原始数据:', res);
|
||
|
||
// 🔥 检查是否已取消录音
|
||
if (this.voiceRecordCancelled) {
|
||
console.log('⚠️ 录音已取消,不发送消息');
|
||
this.voiceRecordCancelled = false; // 重置标记
|
||
return;
|
||
}
|
||
|
||
console.log('📁 tempFilePath 详情:', {
|
||
value: res.tempFilePath,
|
||
type: typeof res.tempFilePath,
|
||
length: res.tempFilePath?.length,
|
||
exists: !!res.tempFilePath
|
||
});
|
||
|
||
const { tempFilePath, duration } = res;
|
||
|
||
// 🔥 严格检查 tempFilePath
|
||
if (!tempFilePath || typeof tempFilePath !== 'string' || tempFilePath.trim() === '') {
|
||
console.error('❌ 录音文件路径无效:', tempFilePath);
|
||
wx.showToast({
|
||
title: '录音文件获取失败',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 🔥 检查录音时长 - 至少需要1秒
|
||
if (duration < 1000) {
|
||
console.log('⚠️ 录音时间太短,不发送 - 时长:', duration);
|
||
wx.showToast({
|
||
title: '时间太短了,没录到啊~~~~~~',
|
||
icon: 'none',
|
||
duration: 1500
|
||
});
|
||
return;
|
||
}
|
||
|
||
console.log('✅ 准备发送语音,路径:', tempFilePath);
|
||
|
||
// 🔥 使用微信文件系统获取文件信息验证
|
||
wx.getFileSystemManager().getFileInfo({
|
||
filePath: tempFilePath,
|
||
success: (fileInfo) => {
|
||
console.log('✅ 文件信息:', fileInfo);
|
||
// 🔥 调用新的语音发送方法
|
||
this.sendVoiceMessageWithNIM(tempFilePath, duration);
|
||
},
|
||
fail: (err) => {
|
||
console.error('❌ 获取文件信息失败:', err);
|
||
wx.showToast({
|
||
title: '录音文件无效',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
});
|
||
});
|
||
|
||
recorderManager.onError((err) => {
|
||
console.error('❌ 录音错误:', err);
|
||
wx.showToast({
|
||
title: '录音失败',
|
||
icon: 'none'
|
||
});
|
||
this.setData({ recording: false });
|
||
});
|
||
|
||
// 开始录音
|
||
recorderManager.start({
|
||
duration: 60000, // 最长60秒
|
||
sampleRate: 16000,
|
||
numberOfChannels: 1,
|
||
encodeBitRate: 48000,
|
||
format: 'mp3'
|
||
});
|
||
|
||
// 保存录音管理器实例
|
||
this.recorderManager = recorderManager;
|
||
|
||
} catch (error) {
|
||
console.error('❌ 启动录音失败:', error);
|
||
wx.showToast({
|
||
title: '录音功能异常',
|
||
icon: 'none'
|
||
});
|
||
this.setData({ recording: false });
|
||
}
|
||
},
|
||
|
||
// 停止录音并发送
|
||
stopVoiceRecord() {
|
||
if (!this.recorderManager) {
|
||
console.warn('⚠️ 录音管理器不存在');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 🔥 直接停止录音,onStop回调已在startVoiceRecord中注册
|
||
this.recorderManager.stop();
|
||
console.log(' 停止录音,等待处理...');
|
||
|
||
} catch (error) {
|
||
console.error('❌ 停止录音失败:', error);
|
||
wx.showToast({
|
||
title: '录音失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
},
|
||
|
||
// 取消录音
|
||
cancelVoiceRecord() {
|
||
if (!this.recorderManager) {
|
||
console.warn('⚠️ 录音管理器不存在');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 🔥 先设置取消标记,确保onStop回调不会发送消息
|
||
this.voiceRecordCancelled = true;
|
||
|
||
this.recorderManager.stop();
|
||
console.log('✅ 录音已取消');
|
||
wx.showToast({
|
||
title: '已取消',
|
||
icon: 'none',
|
||
duration: 1500
|
||
});
|
||
} catch (error) {
|
||
console.error('❌ 取消录音失败:', error);
|
||
}
|
||
},
|
||
|
||
// 🔥 发送语音消息(使用NIM SDK,新录制的语音)
|
||
async sendVoiceMessageWithNIM(filePath, duration) {
|
||
try {
|
||
console.log('📤 准备发送语音消息 (NIM直接调用):', {
|
||
filePath,
|
||
duration,
|
||
filePathType: typeof filePath,
|
||
filePathLength: filePath?.length,
|
||
targetId: this.data.targetId,
|
||
chatType: this.data.chatType
|
||
});
|
||
|
||
// 🔥 严格检查参数
|
||
if (!filePath || typeof filePath !== 'string' || filePath.trim() === '') {
|
||
console.error('❌ 语音文件路径无效:', filePath);
|
||
throw new Error('语音文件路径不能为空');
|
||
}
|
||
|
||
// 🔥 确保有会话ID(首次进入可能未设置)
|
||
let conversationId = this.data.conversationId;
|
||
if (!conversationId && this.data.targetId) {
|
||
conversationId = this.buildNIMConversationId(this.data.targetId, this.data.chatType);
|
||
this.setData({ conversationId });
|
||
console.log('✅ 自动生成会话ID:', conversationId);
|
||
}
|
||
|
||
if (!conversationId) {
|
||
console.error('❌ 会话ID为空');
|
||
throw new Error('会话ID不能为空');
|
||
}
|
||
|
||
wx.showLoading({
|
||
title: '发送中...'
|
||
});
|
||
|
||
// 🔥 获取当前用户的NIM账号ID
|
||
const currentNimAccountId = this.data.userInfo?.neteaseIMAccid || this.data.userInfo?.user?.neteaseIMAccid || '';
|
||
if (!currentNimAccountId) {
|
||
console.error('❌ 无法获取NIM账号ID');
|
||
throw new Error('无法获取当前用户的NIM账号ID');
|
||
}
|
||
|
||
console.log('✅ 参数检查通过,准备创建临时消息');
|
||
|
||
// 🔥 创建临时语音消息
|
||
const timestamp = Date.now();
|
||
const tempMessage = {
|
||
messageId: 'temp_voice_' + timestamp,
|
||
senderId: currentNimAccountId,
|
||
targetId: this.data.targetId,
|
||
content: {
|
||
duration: duration,
|
||
url: filePath // 临时使用本地路径
|
||
},
|
||
msgType: 'voice',
|
||
sendTime: timestamp,
|
||
timestamp: timestamp,
|
||
isSelf: true,
|
||
senderName: '我',
|
||
senderAvatar: this.data.userAvatar || '',
|
||
deliveryStatus: 3, // 发送中 (V2NIM_MESSAGE_SENDING_STATE_SENDING)
|
||
bubbleTime: this.formatBubbleTime(timestamp)
|
||
};
|
||
|
||
// 添加到消息列表
|
||
const messages = this.data.messages.concat([tempMessage]);
|
||
this.setMessagesWithDate(messages);
|
||
|
||
// 滚动到底部
|
||
setTimeout(() => this.scrollToBottom(), 100);
|
||
|
||
console.log('调用 NIM SDK 发送语音消息:', {
|
||
conversationId: conversationId,
|
||
targetId: this.data.targetId,
|
||
filePath: filePath,
|
||
duration: duration,
|
||
isGroup: this.data.chatType === 1
|
||
});
|
||
|
||
// 🔥 使用 NIM SDK 发送语音消息(使用 conversationId 而不是 targetId)
|
||
const result = await nimConversationManager.sendAudioMessage(
|
||
conversationId, // 🔥 修复:使用 conversationId 而不是 targetId
|
||
filePath,
|
||
duration, // 已经是毫秒
|
||
{
|
||
isGroup: this.data.chatType === 1
|
||
}
|
||
);
|
||
|
||
wx.hideLoading();
|
||
|
||
console.log('✅ 语音消息发送成功:', result);
|
||
|
||
// 🔥 格式化并替换临时消息
|
||
if (result && result.message) {
|
||
const sentMessage = await this.formatMessage(result.message);
|
||
sentMessage.deliveryStatus = 1; // 发送成功 (V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED)
|
||
|
||
// 替换临时消息
|
||
const tempIndex = this.data.messages.findIndex(msg => msg.messageId === tempMessage.messageId);
|
||
if (tempIndex !== -1) {
|
||
const updatedMessages = [...this.data.messages];
|
||
updatedMessages[tempIndex] = sentMessage;
|
||
this.setMessagesWithDate(updatedMessages);
|
||
console.log('✅ 语音消息UI更新成功');
|
||
}
|
||
}
|
||
|
||
wx.showToast({
|
||
title: '发送成功',
|
||
icon: 'success'
|
||
});
|
||
|
||
} catch (error) {
|
||
wx.hideLoading();
|
||
console.error('❌ 发送语音消息失败:', error);
|
||
|
||
// 更新临时消息状态为失败
|
||
const messages = this.data.messages.map(msg => {
|
||
if (msg.messageId.startsWith('temp_voice_')) {
|
||
return { ...msg, deliveryStatus: 1 }; // 发送失败
|
||
}
|
||
return msg;
|
||
});
|
||
this.setMessagesWithDate(messages);
|
||
|
||
wx.showToast({
|
||
title: '发送失败: ' + error.message,
|
||
icon: 'none'
|
||
});
|
||
}
|
||
},
|
||
|
||
// 关闭表情面板的辅助方法
|
||
closeEmojiPanel() {
|
||
if (this.data.showEmojiPanel) {
|
||
this.setData({
|
||
showEmojiPanel: false,
|
||
emojiPanelHeight: 0
|
||
});
|
||
}
|
||
},
|
||
|
||
// 收起输入法并重置相关状态
|
||
hideInputMethod() {
|
||
try { wx.hideKeyboard(); } catch (e) { /* 忽略异常 */ }
|
||
this.setData({
|
||
keyboardHeight: 0,
|
||
inputFocus: false,
|
||
showEmojiPanel: false,
|
||
showMorePanel: false,
|
||
emojiPanelHeight: 0
|
||
});
|
||
},
|
||
|
||
// 切换表情面板
|
||
toggleEmojiPanel() {
|
||
const willShow = !this.data.showEmojiPanel;
|
||
// 调用通用方法收起输入法
|
||
this.hideInputMethod();
|
||
this.setData({
|
||
showEmojiPanel: willShow,
|
||
showMorePanel: false
|
||
}, () => {
|
||
if (willShow) {
|
||
// 表情面板展开后,测量其高度
|
||
wx.createSelectorQuery()
|
||
.select('.emoji-panel')
|
||
.boundingClientRect(rect => {
|
||
if (rect && rect.height) {
|
||
console.log('😀 表情面板高度:', rect.height, rect.height - 10);
|
||
this.setData({
|
||
emojiPanelHeight: rect.height - 10 // 减去一些间距
|
||
}, () => {
|
||
// 滚动到底部,确保最后的消息可见
|
||
this.scrollToBottom(true);
|
||
});
|
||
}
|
||
})
|
||
.exec();
|
||
} else {
|
||
// 表情面板关闭
|
||
this.setData({
|
||
emojiPanelHeight: 0
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
// 切换更多面板
|
||
toggleMorePanel() {
|
||
const willShow = !this.data.showMorePanel;
|
||
|
||
// 关闭表情面板
|
||
this.closeEmojiPanel();
|
||
// 调用通用方法收起输入法
|
||
this.hideInputMethod();
|
||
|
||
this.setData({
|
||
showMorePanel: willShow
|
||
});
|
||
},
|
||
|
||
// 阻止事件冒泡
|
||
stopPropagation() {
|
||
// 阻止事件冒泡,防止点击面板内容时关闭面板
|
||
},
|
||
|
||
// 选择表情
|
||
selectEmoji(e) {
|
||
const emoji = e.currentTarget.dataset.emoji;
|
||
const inputText = this.data.inputText + emoji;
|
||
this.setData({ inputText });
|
||
},
|
||
|
||
// 选择文件
|
||
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);
|
||
|
||
// 限制大小 200MB
|
||
if (fileSize > 200 * 1024 * 1024) {
|
||
wx.showToast({ title: '文件不能超过200MB', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
// 获取当前用户的NIM账号ID
|
||
const currentNimAccountId = this.data.userInfo?.neteaseIMAccid || this.data.userInfo?.user?.neteaseIMAccid || '';
|
||
if (!currentNimAccountId) {
|
||
wx.showToast({ title: 'NIM账号错误', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
// 创建临时消息
|
||
const timestamp = Date.now();
|
||
const clientTempId = 'temp_' + timestamp;
|
||
const tempMessage = {
|
||
messageId: clientTempId,
|
||
senderId: currentNimAccountId, // 🔥 使用NIM账号ID
|
||
targetId: this.data.targetId,
|
||
content: { url: '' },
|
||
msgType: 'file',
|
||
fileName: fileName,
|
||
fileSize: readableSize,
|
||
sendTime: timestamp,
|
||
timestamp: timestamp,
|
||
isSelf: true,
|
||
senderName: '我',
|
||
senderAvatar: this.data.userAvatar || '',
|
||
deliveryStatus: 3, // V2NIM_MESSAGE_SENDING_STATE_SENDING
|
||
bubbleTime: this.formatBubbleTime(timestamp)
|
||
};
|
||
|
||
this.setMessagesWithDate(this.data.messages.concat([tempMessage]));
|
||
setTimeout(() => this.scrollToBottom(), 100);
|
||
|
||
wx.showLoading({ title: '发送中...' });
|
||
|
||
try {
|
||
// 🔥 使用 NIM SDK 发送文件消息(SDK会自动上传)
|
||
const result = await nimConversationManager.sendFileMessage(
|
||
this.data.conversationId,
|
||
filePath
|
||
);
|
||
|
||
wx.hideLoading();
|
||
|
||
if (result && result.message) {
|
||
// 🔥 格式化并替换临时消息
|
||
const sentMessage = await this.formatMessage(result.message);
|
||
sentMessage.deliveryStatus = 1; // 发送成功 (V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED)
|
||
|
||
const updatedMessages = this.data.messages.map(msg =>
|
||
msg.messageId === tempMessage.messageId ? sentMessage : msg
|
||
);
|
||
this.setMessagesWithDate(updatedMessages);
|
||
console.log('✅ 文件消息UI更新成功');
|
||
} else {
|
||
throw new Error('NIM SDK 发送文件失败');
|
||
}
|
||
} catch (err) {
|
||
wx.hideLoading();
|
||
console.error('❌ 发送文件失败:', err);
|
||
// 更新为失败
|
||
const failedMessages = this.data.messages.map(msg => {
|
||
if (msg.messageId === tempMessage.messageId) {
|
||
return { ...msg, deliveryStatus: 1 }; // V2NIM_MESSAGE_SENDING_STATE_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 {
|
||
|
||
const { name, address, latitude, longitude } = res || {};
|
||
|
||
// 基本校验
|
||
if (latitude == null || longitude == null) {
|
||
wx.showToast({ title: '位置信息无效', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
// 获取当前用户的NIM账号ID
|
||
const currentNimAccountId = this.data.userInfo?.neteaseIMAccid || this.data.userInfo?.user?.neteaseIMAccid || '';
|
||
if (!currentNimAccountId) {
|
||
wx.showToast({ title: 'NIM账号错误', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
// 构造临时消息
|
||
const timestamp = Date.now();
|
||
const clientTempId = 'temp_' + timestamp;
|
||
const tempMsg = {
|
||
messageId: clientTempId,
|
||
senderId: currentNimAccountId, // 🔥 使用NIM账号ID
|
||
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: 3, // V2NIM_MESSAGE_SENDING_STATE_SENDING
|
||
bubbleTime: this.formatBubbleTime(timestamp)
|
||
};
|
||
|
||
this.setMessagesWithDate(this.data.messages.concat([tempMsg]));
|
||
setTimeout(() => this.scrollToBottom(), 100);
|
||
|
||
// 使用 NIM SDK 发送位置消息
|
||
try {
|
||
const result = await nimConversationManager.sendLocationMessage(
|
||
this.data.conversationId,
|
||
latitude,
|
||
longitude,
|
||
address || '',
|
||
name || '位置'
|
||
);
|
||
|
||
if (result && result.message) {
|
||
// 🔥 替换临时消息为正式消息
|
||
console.log('✅ 位置消息发送成功,准备更新UI');
|
||
|
||
const sentMessage = await this.formatMessage(result.message);
|
||
|
||
// 🔥 强制设置为成功状态
|
||
sentMessage.deliveryStatus = 1; // V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED
|
||
|
||
// 🔥 使用临时消息的索引位置来替换
|
||
const tempIndex = this.data.messages.findIndex(msg => msg.messageId === clientTempId);
|
||
|
||
if (tempIndex !== -1) {
|
||
const updatedMessages = [...this.data.messages];
|
||
updatedMessages[tempIndex] = sentMessage;
|
||
this.setMessagesWithDate(updatedMessages);
|
||
} else {
|
||
const updatedMessages = this.data.messages.concat([sentMessage]);
|
||
this.setMessagesWithDate(updatedMessages);
|
||
}
|
||
} else {
|
||
throw new Error('NIM SDK 发送位置失败');
|
||
}
|
||
} catch (sendErr) {
|
||
this.updateMessageDeliveryStatus(clientTempId, 1); // V2NIM_MESSAGE_SENDING_STATE_FAILED
|
||
throw sendErr;
|
||
}
|
||
|
||
} catch (err) {
|
||
console.error('❌ 发送位置失败:', err);
|
||
// 标记失败
|
||
if (clientTempId) this.updateMessageDeliveryStatus(clientTempId, 1); // V2NIM_MESSAGE_SENDING_STATE_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;
|
||
|
||
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;
|
||
|
||
// TODO: 实现音频播放
|
||
wx.showToast({
|
||
title: '音频播放功能开发中',
|
||
icon: 'none'
|
||
});
|
||
},
|
||
|
||
// 显示位置
|
||
showLocation(e) {
|
||
const message = e.currentTarget.dataset.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) {
|
||
return;
|
||
}
|
||
|
||
// 普通消息的菜单选项
|
||
if (message.msgType === 'text') {
|
||
itemList.push('复制');
|
||
}
|
||
|
||
if (message.isSelf) {
|
||
// 检查是否可以撤回(2分钟内)
|
||
const canRecall = this.canRecallMessage(message);
|
||
if (canRecall) {
|
||
itemList.push('撤回');
|
||
}
|
||
}
|
||
|
||
// 如果没有可用操作,直接返回
|
||
if (itemList.length === 0) {
|
||
return;
|
||
}
|
||
|
||
wx.showActionSheet({
|
||
itemList: itemList,
|
||
success: (res) => {
|
||
const action = itemList[res.tapIndex];
|
||
switch (action) {
|
||
case '复制':
|
||
this.copyMessage(message);
|
||
break;
|
||
case '撤回':
|
||
this.recallMessage(message);
|
||
break;
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
// 复制消息
|
||
copyMessage(message) {
|
||
if (message.msgType === 'text') {
|
||
wx.setClipboardData({
|
||
data: message.content,
|
||
success: () => {
|
||
wx.showToast({
|
||
title: '已复制',
|
||
icon: 'success'
|
||
});
|
||
}
|
||
});
|
||
}
|
||
},
|
||
|
||
// 撤回消息
|
||
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
|
||
});
|
||
|
||
// message._rawMessage 是保存的 NIM SDK 原始消息对象
|
||
const rawMessage = message._rawMessage;
|
||
if (!rawMessage) {
|
||
throw new Error('消息对象不完整,无法撤回');
|
||
}
|
||
|
||
const result = await nimConversationManager.recallMessage(
|
||
this.data.conversationId,
|
||
rawMessage // 传入 NIM SDK 原始消息对象
|
||
);
|
||
|
||
if (result) {
|
||
// 撤回成功,本地立即更新消息状态为已撤回
|
||
this.updateMessageToRecalled(message.messageId);
|
||
|
||
wx.showToast({
|
||
title: '已撤回',
|
||
icon: 'success'
|
||
});
|
||
} else {
|
||
throw new Error('撤回失败');
|
||
}
|
||
} 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) {
|
||
const recallText = msg.isSelf ? '你撤回了一条消息' : '对方撤回了一条消息';
|
||
|
||
return {
|
||
...msg,
|
||
isRecalled: true,
|
||
content: recallText,
|
||
originalContent: msg.content,
|
||
recallTime: Date.now(),
|
||
msgType: 'text'
|
||
};
|
||
}
|
||
return msg;
|
||
});
|
||
|
||
this.setMessagesWithDate(messages);
|
||
|
||
// 同步撤回提示到本地,供会话列表页读取
|
||
try {
|
||
const convId = this.data.conversationId;
|
||
if (convId) {
|
||
const hints = wx.getStorageSync('recallHints') || {};
|
||
// 使用实际的撤回消息文本,而不是固定的"你撤回了一条消息"
|
||
const revokedMsg = messages.find(m => m.messageId === messageId);
|
||
const previewText = revokedMsg?.content || '撤回了一条消息';
|
||
hints[convId] = { preview: previewText, updatedAt: Date.now() };
|
||
wx.setStorageSync('recallHints', hints);
|
||
}
|
||
} catch (e) {
|
||
console.warn('写入撤回提示失败', e);
|
||
}
|
||
},
|
||
|
||
// 重发消息
|
||
async resendMessage(e) {
|
||
const message = e.currentTarget.dataset.message;
|
||
if (!message) return;
|
||
console.log('🔁 重发消息 (NIM SDK):', message);
|
||
|
||
// 标记为发送中
|
||
this.updateMessageDeliveryStatus(message.messageId, 2); // V2NIM_MESSAGE_SENDING_STATE_SENDING
|
||
|
||
try {
|
||
let result = null;
|
||
const conversationId = this.data.conversationId;
|
||
|
||
console.log('🔁 重发消息使用会话ID:', conversationId);
|
||
|
||
switch (message.msgType) {
|
||
case 'text':
|
||
result = await nimConversationManager.sendTextMessage(
|
||
conversationId,
|
||
message.content
|
||
);
|
||
break;
|
||
case 'image':
|
||
case 'video':
|
||
case 'voice':
|
||
case 'file':
|
||
// ⚠️ 图片/视频/语音/文件消息重发需要本地文件路径
|
||
// 但发送失败的消息已经没有本地路径,无法重发
|
||
wx.showToast({
|
||
title: '媒体消息暂不支持重发',
|
||
icon: 'none'
|
||
});
|
||
this.updateMessageDeliveryStatus(message.messageId, 1);
|
||
return;
|
||
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 || '位置';
|
||
result = await nimConversationManager.sendLocationMessage(
|
||
conversationId,
|
||
lat,
|
||
lng,
|
||
address,
|
||
name
|
||
);
|
||
break;
|
||
}
|
||
default:
|
||
throw new Error('不支持的消息类型');
|
||
}
|
||
|
||
if (result && result.message) {
|
||
// 替换为新消息 - 使用索引定位避免ID不匹配
|
||
const sentMessage = await this.formatMessage(result.message);
|
||
|
||
// 🔥 强制设置为成功状态
|
||
sentMessage.deliveryStatus = 1; // V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED
|
||
|
||
const msgIndex = this.data.messages.findIndex(msg => msg.messageId === message.messageId);
|
||
|
||
if (msgIndex !== -1) {
|
||
const updatedMessages = [...this.data.messages];
|
||
updatedMessages[msgIndex] = sentMessage;
|
||
console.log('✅ 重发消息替换成功:', {
|
||
原消息ID: message.messageId,
|
||
新消息ID: sentMessage.messageId,
|
||
索引位置: msgIndex
|
||
});
|
||
this.setMessagesWithDate(updatedMessages);
|
||
} else {
|
||
console.warn('⚠️ 未找到原消息,直接添加重发消息');
|
||
this.setMessagesWithDate([...this.data.messages, sentMessage]);
|
||
}
|
||
} else {
|
||
throw new Error('发送失败');
|
||
}
|
||
|
||
} catch (err) {
|
||
console.error('❌ 重发失败:', err);
|
||
this.updateMessageDeliveryStatus(message.messageId, 1); // V2NIM_MESSAGE_SENDING_STATE_FAILED
|
||
wx.showToast({ title: '发送失败', icon: 'none' });
|
||
}
|
||
},
|
||
|
||
// 将消息置为失败并可选记录原因
|
||
markMessageFailed(messageId, reason) {
|
||
const messages = this.data.messages.map(m => m.messageId === messageId ? { ...m, deliveryStatus: 1, errorReason: reason || '' } : m); // V2NIM_MESSAGE_SENDING_STATE_FAILED
|
||
this.setData({ messages });
|
||
},
|
||
|
||
// 🔥 加载更多消息 - 仅在用户向上滚动时触发
|
||
loadMoreMessages() {
|
||
console.log('🔄 用户向上滚动,触发加载更多历史消息');
|
||
console.log('🔍 当前状态检查:', {
|
||
hasMore: this.data.hasMore,
|
||
loadingMessages: this.data.loadingMessages,
|
||
lastMessageTime: this.data.lastMessageTime,
|
||
messagesCount: this.data.messages.length
|
||
});
|
||
|
||
// 🔥 检查是否还有更多消息
|
||
if (!this.data.hasMore) {
|
||
|
||
return;
|
||
}
|
||
|
||
// 🔥 检查是否正在加载中
|
||
if (this.data.loadingMessages) {
|
||
|
||
return;
|
||
}
|
||
|
||
// 🔥 检查是否有lastMessageTime用于分页
|
||
if (!this.data.lastMessageTime) {
|
||
console.log('⚠️ 缺少lastMessageTime,无法加载更多');
|
||
this.setData({ hasMore: false });
|
||
return;
|
||
}
|
||
|
||
console.log('✅ 开始加载更多历史消息');
|
||
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;
|
||
|
||
// 🔥 参考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);
|
||
},
|
||
|
||
// 下载文件
|
||
downloadFile(e) {
|
||
const message = e.currentTarget.dataset.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: () => {
|
||
|
||
},
|
||
fail: (err) => {
|
||
console.error('❌ 文件打开失败:', err);
|
||
wx.showToast({
|
||
title: '文件打开失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
});
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
console.error('❌ 文件下载失败:', err);
|
||
wx.showToast({
|
||
title: '文件下载失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
});
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
});
|
||
|
||
|