1290 lines
36 KiB
JavaScript
1290 lines
36 KiB
JavaScript
// 消息页面逻辑 - 完全基于 NIM SDK
|
||
const app = getApp();
|
||
const apiClient = require('../../utils/api-client.js');
|
||
const nimConversationManager = require('../../utils/nim-conversation-manager.js');
|
||
const nimPresenceManager = require('../../utils/nim-presence-manager.js');
|
||
const { initPageSystemInfo } = require('../../utils/system-info-modern.js');
|
||
|
||
Page({
|
||
data: {
|
||
//模拟数据
|
||
store:[{
|
||
avatar:'../../images/group/testImg.jpg',
|
||
nickname:'Jianying Liu',
|
||
text:'Heyyy',
|
||
tiem:'16:30'
|
||
}],
|
||
searchKeyword: '',
|
||
refreshing: false,
|
||
loading: false,
|
||
longPressing: false, // 长按标记,防止长按后触发单击
|
||
|
||
|
||
// 会话数据
|
||
conversations: [],
|
||
pinnedConversations: [],
|
||
normalConversations: [],
|
||
totalUnreadCount: 0,
|
||
filteredConversations: [],
|
||
|
||
// NIM SDK 状态
|
||
nimReady: false,
|
||
|
||
// 系统适配信息
|
||
systemInfo: {},
|
||
statusBarHeight: 0,
|
||
menuButtonHeight: 0,
|
||
menuButtonTop: 0,
|
||
navBarHeight: 0,
|
||
windowHeight: 0,
|
||
safeAreaBottom: 0,
|
||
showSearch: false,
|
||
|
||
// 本地模糊搜索增强
|
||
searching: false,
|
||
conversationSamples: {},
|
||
sampleLoading: {},
|
||
lastSearchKeyword: ''
|
||
},
|
||
|
||
// 统一时间戳规范化
|
||
normalizeTimestamp(ts) {
|
||
if (!ts && ts !== 0) return 0;
|
||
if (typeof ts === 'number' && isFinite(ts)) return ts;
|
||
if (typeof ts === 'string') {
|
||
const s = ts.trim();
|
||
if (!s) return 0;
|
||
if (/^\d+$/.test(s)) {
|
||
const n = parseInt(s, 10);
|
||
return s.length === 10 ? n * 1000 : n;
|
||
}
|
||
const d = Date.parse(s);
|
||
return isNaN(d) ? 0 : d;
|
||
}
|
||
try {
|
||
const d = new Date(ts).getTime();
|
||
return isNaN(d) ? 0 : d;
|
||
} catch (_) {
|
||
return 0;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 从会话数据中提取真实的用户ID
|
||
* @param {Object} conv - 会话对象
|
||
* @returns {string} - 用户ID
|
||
*/
|
||
extractUserIdFromConversation(conv) {
|
||
let userId = conv.targetId || conv.userId || '';
|
||
|
||
// 如果 targetId 是 conversationId 格式(包含|符号),则解析
|
||
if (userId && typeof userId === 'string' && userId.includes('|')) {
|
||
const parts = userId.split('|');
|
||
if (parts.length === 3) {
|
||
userId = parts[2]; // 提取目标用户ID
|
||
}
|
||
}
|
||
|
||
// 如果还是没有,尝试从 conversationId 解析
|
||
if (!userId && conv.conversationId && typeof conv.conversationId === 'string') {
|
||
const parts = conv.conversationId.split('|');
|
||
if (parts.length === 3) {
|
||
userId = parts[2];
|
||
}
|
||
}
|
||
|
||
return userId || '';
|
||
},
|
||
|
||
// 页面加载
|
||
onLoad: function (options) {
|
||
const { store } = this.data;
|
||
// 生成10次重复的数组(fill+flat 一行实现)
|
||
const repeatedArr = Array(20).fill(store).flat();
|
||
// 响应式更新:必须用 setData,页面才会渲染新数组
|
||
this.setData({ repeatedArr });
|
||
|
||
|
||
this.initSystemInfo();
|
||
this.checkAuthAndInit();
|
||
|
||
wx.setNavigationBarTitle({ title: '消息' });
|
||
},
|
||
|
||
// 检查认证状态并初始化
|
||
async checkAuthAndInit() {
|
||
try {
|
||
const currentToken = apiClient.getToken();
|
||
const app = getApp();
|
||
const isLoggedIn = app?.globalData?.isLoggedIn || false;
|
||
|
||
console.log('检查认证状态:', { hasToken: !!currentToken, isLoggedIn });
|
||
|
||
if (!currentToken || !isLoggedIn) {
|
||
console.warn('⚠️ 用户未登录,跳转到登录页');
|
||
this.redirectToLogin('checkAuthAndInit');
|
||
return;
|
||
}
|
||
|
||
// 初始化 NIM 模式
|
||
await this.initNIMMode();
|
||
} catch (error) {
|
||
console.error('认证检查失败:', error);
|
||
wx.showToast({
|
||
title: `NIM初始化失败:${error}`,
|
||
icon: 'none'
|
||
});
|
||
}
|
||
},
|
||
|
||
// 统一的登录页跳转方法
|
||
redirectToLogin(source) {
|
||
if (this.redirecting) {
|
||
console.log('已在跳转中,跳过');
|
||
return;
|
||
}
|
||
|
||
this.redirecting = true;
|
||
console.log(`🔄 来自${source}的跳转请求,执行跳转...`);
|
||
|
||
setTimeout(() => {
|
||
wx.reLaunch({
|
||
url: '/pages/login/login',
|
||
success: () => {
|
||
console.log('✅ 跳转登录页成功');
|
||
this.redirecting = false;
|
||
},
|
||
fail: (err) => {
|
||
console.error('❌ 跳转登录页失败:', err);
|
||
this.redirecting = false;
|
||
}
|
||
});
|
||
}, 1000);
|
||
},
|
||
|
||
// 初始化系统信息
|
||
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
|
||
});
|
||
},
|
||
|
||
// 初始化 NIM 模式
|
||
initNIMMode() {
|
||
try {
|
||
console.log('📡 初始化 NIM 模式');
|
||
|
||
const app = getApp();
|
||
if (!app.globalData.nim) {
|
||
console.error('❌ NIM 实例未初始化');
|
||
wx.showToast({
|
||
title: 'NIM未初始化',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 注册 NIM 事件监听器
|
||
nimConversationManager.on('conversation_created', this.handleNIMConversationCreated.bind(this));
|
||
nimConversationManager.on('conversation_deleted', this.handleNIMConversationDeleted.bind(this));
|
||
nimConversationManager.on('conversation_changed', this.handleNIMConversationChanged.bind(this));
|
||
nimConversationManager.on('new_message', this.handleNIMNewMessages.bind(this));
|
||
nimConversationManager.on('message_revoked', this.handleNIMMessageRevoked.bind(this));
|
||
|
||
// 🔥 注册在线状态变化监听
|
||
nimPresenceManager.on('presence_changed', this.handlePresenceChanged.bind(this));
|
||
|
||
this.setData({ nimReady: true });
|
||
|
||
// 加载会话列表
|
||
this.loadConversations();
|
||
|
||
console.log('✅ NIM 模式初始化完成');
|
||
} catch (error) {
|
||
console.error('❌ 初始化 NIM 模式失败:', error);
|
||
wx.showToast({
|
||
title: '初始化 NIM 模式失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
},
|
||
|
||
// 🔥 检查并重连 NIM(页面级别)
|
||
async checkAndReconnectNim() {
|
||
try {
|
||
const app = getApp();
|
||
|
||
// 如果 NIM 未就绪,尝试初始化
|
||
if (!this.data.nimReady) {
|
||
console.log('⚠️ 页面 NIM 未就绪,尝试初始化');
|
||
await this.checkAuthAndInit();
|
||
return;
|
||
}
|
||
|
||
// 触发全局 NIM 重连检查
|
||
if (app && typeof app.checkAndReconnectNim === 'function') {
|
||
await app.checkAndReconnectNim();
|
||
}
|
||
|
||
// 重新初始化会话管理器(确保事件监听器有效)
|
||
if (app.globalData.nim) {
|
||
const nim = app.globalData.nim;
|
||
|
||
// 检查会话管理器是否已初始化
|
||
if (!nimConversationManager.getInitStatus()) {
|
||
console.log('🔄 重新初始化会话管理器');
|
||
nimConversationManager.init(nim);
|
||
}
|
||
|
||
// 检查在线状态管理器是否已初始化
|
||
if (!nimPresenceManager.getInitStatus()) {
|
||
console.log('🔄 重新初始化在线状态管理器');
|
||
nimPresenceManager.init(nim);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ 页面级 NIM 重连检查失败:', error);
|
||
}
|
||
},
|
||
|
||
// ===== NIM 事件处理方法 =====
|
||
|
||
// 处理 NIM 会话新增
|
||
handleNIMConversationCreated(conversation) {
|
||
console.log('📝 处理NIM会话新增:', conversation);
|
||
const conversations = [conversation, ...this.data.conversations];
|
||
this.processConversations(conversations);
|
||
this.playMessageSound();
|
||
},
|
||
|
||
// 处理 NIM 会话删除
|
||
handleNIMConversationDeleted(conversationIds) {
|
||
console.log('🗑️ 处理NIM会话删除:', conversationIds);
|
||
const conversations = this.data.conversations.filter(
|
||
conv => !conversationIds.includes(conv.conversationId)
|
||
);
|
||
this.processConversations(conversations);
|
||
},
|
||
|
||
// 处理 NIM 会话更新
|
||
handleNIMConversationChanged(changedConversations) {
|
||
console.log('🔄 处理NIM会话更新,数量:', changedConversations?.length || 0);
|
||
|
||
if (!Array.isArray(changedConversations) || changedConversations.length === 0) {
|
||
return;
|
||
}
|
||
|
||
// 🔥 增量更新:只更新变化的会话,而不是替换整个列表
|
||
let conversations = [...this.data.conversations];
|
||
let hasChanges = false;
|
||
|
||
changedConversations.forEach(changedConv => {
|
||
const index = conversations.findIndex(
|
||
conv => conv.conversationId === changedConv.conversationId
|
||
);
|
||
|
||
if (index >= 0) {
|
||
// 检查是否真的有变化(避免不必要的更新)
|
||
const oldConv = conversations[index];
|
||
const hasRealChange =
|
||
oldConv.unreadCount !== changedConv.unreadCount ||
|
||
oldConv.lastMessage !== changedConv.lastMessage ||
|
||
oldConv.isPinned !== changedConv.isPinned ||
|
||
oldConv.isMuted !== changedConv.isMuted;
|
||
|
||
if (hasRealChange) {
|
||
// 🔥 特殊处理:如果旧会话有未读数,新会话未读数为0,但最后消息没变
|
||
// 说明这可能是标记已读导致的更新,不应该覆盖可能正在到来的新消息未读数
|
||
if (oldConv.unreadCount > 0 && changedConv.unreadCount === 0 &&
|
||
oldConv.lastMessage === changedConv.lastMessage) {
|
||
console.log('⚠️ 检测到可能的标记已读事件,保留当前未读数');
|
||
changedConv.unreadCount = oldConv.unreadCount;
|
||
}
|
||
conversations[index] = changedConv;
|
||
hasChanges = true;
|
||
}
|
||
} else {
|
||
// 新会话,添加到列表
|
||
conversations.unshift(changedConv);
|
||
hasChanges = true;
|
||
}
|
||
});
|
||
|
||
// 只有真正有变化时才更新 UI
|
||
if (hasChanges) {
|
||
this.processConversations(conversations);
|
||
} else {
|
||
console.log('⚠️ 会话数据无实质变化,跳过更新');
|
||
}
|
||
},
|
||
|
||
// 处理 NIM 新消息
|
||
async handleNIMNewMessages(messages) {
|
||
console.log('📨 处理NIM新消息:', messages);
|
||
|
||
if (!Array.isArray(messages)) return;
|
||
|
||
let conversations = [...this.data.conversations];
|
||
let hasChanges = false;
|
||
let needReloadConversations = false;
|
||
|
||
messages.forEach(message => {
|
||
const conversationId = message.conversationId;
|
||
if (!conversationId) return;
|
||
|
||
const index = conversations.findIndex(
|
||
conv => conv.conversationId === conversationId
|
||
);
|
||
|
||
if (index >= 0) {
|
||
// 更新现有会话
|
||
// 🔥 使用消息的 createTime,如果没有则保持原时间
|
||
const messageTime = message.createTime || conversations[index].lastMsgTime || 0;
|
||
conversations[index] = {
|
||
...conversations[index],
|
||
lastMessage: nimConversationManager.getMessagePreview(message),
|
||
lastMessageType: nimConversationManager.getMessageType(message),
|
||
lastMessageTime: nimConversationManager.formatMessageTime(messageTime),
|
||
lastMsgTime: messageTime,
|
||
unreadCount: (conversations[index].unreadCount || 0) + 1
|
||
};
|
||
hasChanges = true;
|
||
} else {
|
||
// 新会话,需要重新加载
|
||
console.log('⚠️ 收到新会话消息,需要重新加载会话列表');
|
||
needReloadConversations = true;
|
||
}
|
||
});
|
||
|
||
if (hasChanges) {
|
||
this.processConversations(conversations);
|
||
this.playMessageSound();
|
||
}
|
||
|
||
if (needReloadConversations) {
|
||
this.loadConversations();
|
||
}
|
||
},
|
||
|
||
// 处理 NIM 消息撤回
|
||
handleNIMMessageRevoked(revokeNotifications) {
|
||
console.log('↩️ 处理NIM消息撤回:', revokeNotifications);
|
||
|
||
if (!Array.isArray(revokeNotifications)) return;
|
||
|
||
let conversations = [...this.data.conversations];
|
||
let hasChanges = false;
|
||
|
||
revokeNotifications.forEach(notification => {
|
||
const conversationId = notification.conversationId;
|
||
if (!conversationId) return;
|
||
|
||
const index = conversations.findIndex(
|
||
conv => conv.conversationId === conversationId
|
||
);
|
||
|
||
if (index >= 0) {
|
||
conversations[index] = {
|
||
...conversations[index],
|
||
lastMessage: '已撤回'
|
||
};
|
||
hasChanges = true;
|
||
}
|
||
});
|
||
|
||
if (hasChanges) {
|
||
this.processConversations(conversations);
|
||
}
|
||
},
|
||
|
||
// 🔥 处理用户在线状态变化
|
||
handlePresenceChanged(data) {
|
||
const { userId, isOnline } = data;
|
||
|
||
// 更新会话列表中对应用户的在线状态
|
||
let conversations = [...this.data.conversations];
|
||
let hasChanges = false;
|
||
|
||
conversations.forEach(conv => {
|
||
// 判断是否为单聊会话
|
||
const isSingleChat = conv.type === 'single' || conv.type === 0 || conv.chatType === 0;
|
||
|
||
if (!isSingleChat) {
|
||
return;
|
||
}
|
||
|
||
// 提取真实的用户ID进行匹配
|
||
let targetId = conv.targetId;
|
||
|
||
// 如果 targetId 包含 | 符号,说明是 conversationId 格式,需要解析
|
||
if (targetId && typeof targetId === 'string' && targetId.includes('|')) {
|
||
const parts = targetId.split('|');
|
||
if (parts.length === 3) {
|
||
targetId = parts[2];
|
||
conv.targetId = targetId; // 更新为纯用户ID
|
||
}
|
||
}
|
||
|
||
if (targetId === userId) {
|
||
conv.isOnline = isOnline;
|
||
hasChanges = true;
|
||
}
|
||
});
|
||
|
||
if (hasChanges) {
|
||
this.processConversations(conversations);
|
||
}
|
||
},
|
||
|
||
// 加载会话列表
|
||
async loadConversations() {
|
||
if (this.data.loading) return;
|
||
|
||
const currentToken = apiClient.getToken();
|
||
if (!currentToken) {
|
||
console.warn('⚠️ Token 丢失,无法加载会话列表');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
this.setData({ loading: true });
|
||
|
||
console.log('📋 从 NIM SDK 加载会话列表');
|
||
const conversations = await nimConversationManager.getConversationList(0, 100);
|
||
|
||
console.log('✅ 加载会话列表成功,数量:', conversations.length);
|
||
|
||
this.processConversations(conversations);
|
||
} catch (error) {
|
||
console.error('❌ 加载会话列表失败:', error);
|
||
wx.showToast({
|
||
title: `加载会话列表失败:${error}`,
|
||
icon: 'none'
|
||
});
|
||
} finally {
|
||
this.setData({ loading: false });
|
||
}
|
||
},
|
||
|
||
// 播放消息提示音
|
||
playMessageSound() {
|
||
try {
|
||
const innerAudioContext = wx.createInnerAudioContext();
|
||
innerAudioContext.src = '/images/message-sound.mp3';
|
||
innerAudioContext.play();
|
||
} catch (error) {
|
||
console.error('播放提示音失败:', error);
|
||
}
|
||
},
|
||
|
||
// 更新Tab角标
|
||
updateTabBadge() {
|
||
const totalUnread = this.data.totalUnreadCount;
|
||
if (totalUnread > 0) {
|
||
wx.setTabBarBadge({
|
||
index: 1,
|
||
text: totalUnread > 99 ? '99+' : totalUnread.toString()
|
||
});
|
||
} else {
|
||
wx.removeTabBarBadge({ index: 1 });
|
||
}
|
||
|
||
// 自定义TabBar同步
|
||
try {
|
||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||
this.getTabBar().setMessagesBadge(totalUnread);
|
||
}
|
||
} catch (_) { }
|
||
},
|
||
|
||
// 处理会话数据
|
||
async processConversations(conversations) {
|
||
// 过滤已删除项,并统一关键字段
|
||
const validList = (conversations || [])
|
||
.filter(it => it && !it.deleted && !it._deleted)
|
||
.map(it => ({
|
||
...it,
|
||
isPinned: !!(it.isPinned || it.isTop),
|
||
_ts: this.normalizeTimestamp(it.lastMsgTime || it.lastMessageTimeTs || it.lastActiveAt || it.updatedAt)
|
||
}));
|
||
|
||
// 🔥 订阅会话中单聊用户的在线状态
|
||
try {
|
||
await nimPresenceManager.subscribeConversations(validList);
|
||
} catch (error) {
|
||
console.error('订阅在线状态失败:', error);
|
||
}
|
||
|
||
// 🔥 从缓存中更新在线状态
|
||
validList.forEach(conv => {
|
||
// 判断是否为单聊
|
||
const isSingleChat = conv.type === 'single' || conv.type === 0 || conv.chatType === 0;
|
||
|
||
if (isSingleChat) {
|
||
// 提取真实的用户ID(处理可能的 conversationId 格式)
|
||
let targetId = conv.targetId;
|
||
|
||
// 如果 targetId 包含 | 符号,说明是 conversationId 格式,需要解析
|
||
if (targetId && typeof targetId === 'string' && targetId.includes('|')) {
|
||
const parts = targetId.split('|');
|
||
if (parts.length === 3) {
|
||
targetId = parts[2];
|
||
conv.targetId = targetId; // 更新为纯用户ID
|
||
}
|
||
}
|
||
|
||
if (!targetId) {
|
||
return;
|
||
}
|
||
|
||
const presence = nimPresenceManager.getUserPresence(targetId);
|
||
|
||
if (presence) {
|
||
conv.isOnline = presence.online;
|
||
} else {
|
||
conv.isOnline = false; // 默认离线
|
||
}
|
||
}
|
||
});
|
||
|
||
// 置顶/非置顶分组并组内按时间降序
|
||
const pinnedConversations = validList
|
||
.filter(item => item.isPinned)
|
||
.sort((a, b) => b._ts - a._ts);
|
||
const normalConversations = validList
|
||
.filter(item => !item.isPinned)
|
||
.sort((a, b) => b._ts - a._ts);
|
||
|
||
let sortedAll = [...pinnedConversations, ...normalConversations];
|
||
|
||
// 应用来自聊天页的撤回提示
|
||
sortedAll = this.applyRecallHints(sortedAll);
|
||
|
||
// 基于隐藏集合过滤呈现列表
|
||
const hiddenSet = this._getHiddenConversationSet();
|
||
const filteredSorted = (sortedAll || []).filter(item =>
|
||
!hiddenSet.has(String(item.conversationId || item.id || ''))
|
||
);
|
||
|
||
const totalUnreadCount = filteredSorted.reduce((sum, item) => sum + (item.unreadCount || 0), 0);
|
||
|
||
this.setData({
|
||
conversations: sortedAll,
|
||
pinnedConversations: filteredSorted.filter(i => i.isPinned),
|
||
normalConversations: filteredSorted.filter(i => !i.isPinned),
|
||
totalUnreadCount,
|
||
filteredConversations: filteredSorted
|
||
});
|
||
|
||
// 更新角标
|
||
this.updateTabBadge();
|
||
},
|
||
|
||
// 应用撤回提示(来自 chat 页存储),TTL=2分钟
|
||
applyRecallHints(list) {
|
||
try {
|
||
const store = wx.getStorageSync('recallHints') || {};
|
||
const now = Date.now();
|
||
const TTL = 2 * 60 * 1000; // 2分钟
|
||
|
||
return list.map(conv => {
|
||
const cid = conv.conversationId || conv.id;
|
||
if (!cid) return conv;
|
||
|
||
const hint = store[cid];
|
||
if (hint && (now - hint.timestamp < TTL)) {
|
||
return {
|
||
...conv,
|
||
lastMessage: '已撤回',
|
||
lastMessageType: 'system'
|
||
};
|
||
}
|
||
return conv;
|
||
});
|
||
} catch (e) {
|
||
console.error('应用撤回提示失败:', e);
|
||
return list;
|
||
}
|
||
},
|
||
|
||
// 刷新会话列表
|
||
async refreshConversations() {
|
||
this.setData({ refreshing: true });
|
||
await this.loadConversations();
|
||
this.setData({ refreshing: false });
|
||
},
|
||
|
||
// 下拉刷新
|
||
onRefresh() {
|
||
this.refreshConversations();
|
||
},
|
||
|
||
// 搜索输入
|
||
onSearchInput(e) {
|
||
const keyword = (e.detail.value || '').trim();
|
||
this.setData({
|
||
searchKeyword: keyword,
|
||
lastSearchKeyword: keyword,
|
||
searching: !!keyword
|
||
});
|
||
|
||
if (!keyword) {
|
||
// 清空搜索结果
|
||
this.processConversations(this.data.conversations);
|
||
return;
|
||
}
|
||
|
||
// 🔥 本地会话列表过滤(快速响应)
|
||
this.fuzzyFilterConversations(keyword);
|
||
},
|
||
|
||
// 清除搜索
|
||
clearSearch() {
|
||
this.setData({
|
||
searchKeyword: '',
|
||
lastSearchKeyword: '',
|
||
searching: false
|
||
});
|
||
this.processConversations(this.data.conversations);
|
||
},
|
||
|
||
// 显示/隐藏搜索栏
|
||
showSearchBar() {
|
||
this.setData({ showSearch: true });
|
||
},
|
||
|
||
hideSearchBar() {
|
||
this.setData({
|
||
showSearch: false,
|
||
searchKeyword: '',
|
||
lastSearchKeyword: '',
|
||
searching: false
|
||
});
|
||
this.processConversations(this.data.conversations);
|
||
},
|
||
|
||
// 🔥 带搜索关键词打开聊天
|
||
openChatWithSearchKeyword(conversation, searchKeyword) {
|
||
if (!conversation.targetId) {
|
||
wx.showToast({
|
||
title: '会话信息不完整',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
const chatType = conversation.chatType !== undefined ? conversation.chatType : 0;
|
||
|
||
const params = {
|
||
conversationId: conversation.conversationId,
|
||
targetId: conversation.targetId,
|
||
name: encodeURIComponent(conversation.name),
|
||
chatType: chatType
|
||
};
|
||
|
||
const kw = (searchKeyword || '').trim();
|
||
if (kw) {
|
||
params.searchKeyword = encodeURIComponent(kw);
|
||
}
|
||
|
||
const queryString = Object.keys(params)
|
||
.map(key => `${key}=${params[key]}`)
|
||
.join('&');
|
||
|
||
wx.navigateTo({
|
||
url: `/pages/message/chat/chat?${queryString}`
|
||
});
|
||
},
|
||
|
||
// 本地模糊过滤
|
||
fuzzyFilterConversations(keyword) {
|
||
if (!keyword) {
|
||
this.processConversations(this.data.conversations);
|
||
return;
|
||
}
|
||
|
||
const q = (keyword || '').toLowerCase();
|
||
|
||
const subseqScore = (text, query) => {
|
||
if (!text || !query) return 0;
|
||
text = text.toLowerCase();
|
||
let ti = 0, qi = 0, matches = 0;
|
||
while (ti < text.length && qi < query.length) {
|
||
if (text[ti] === query[qi]) {
|
||
matches++;
|
||
qi++;
|
||
}
|
||
ti++;
|
||
}
|
||
return qi === query.length ? matches / text.length : 0;
|
||
};
|
||
|
||
const samples = this.data.conversationSamples || {};
|
||
const list = this.data.conversations || [];
|
||
|
||
const withScore = list.map(item => {
|
||
const nameScore = subseqScore(item.name || '', q) * 3;
|
||
const lastMsgScore = subseqScore(item.lastMessage || '', q) * 2;
|
||
const sampleTexts = samples[item.conversationId] || [];
|
||
const sampleScore = sampleTexts.reduce((acc, txt) => Math.max(acc, subseqScore(txt, q)), 0);
|
||
const totalScore = nameScore + lastMsgScore + sampleScore;
|
||
return { item, score: totalScore };
|
||
}).filter(x => x.score > 0);
|
||
|
||
withScore.sort((a, b) => {
|
||
if (Math.abs(a.score - b.score) < 0.01) {
|
||
return b.item._ts - a.item._ts;
|
||
}
|
||
return b.score - a.score;
|
||
});
|
||
|
||
let filtered = withScore.map(x => x.item);
|
||
if (filtered.length === 0) {
|
||
filtered = list.filter(item => {
|
||
const n = (item.name || '').toLowerCase();
|
||
const m = (item.lastMessage || '').toLowerCase();
|
||
return n.includes(q) || m.includes(q);
|
||
});
|
||
}
|
||
|
||
const pinnedFiltered = filtered.filter(i => i.isPinned);
|
||
const normalFiltered = filtered.filter(i => !i.isPinned);
|
||
|
||
this.setData({
|
||
pinnedConversations: pinnedFiltered,
|
||
normalConversations: normalFiltered,
|
||
filteredConversations: filtered
|
||
});
|
||
},
|
||
|
||
// 预取会话样本
|
||
async prefetchTopConversationSamples(keyword) {
|
||
const q = (keyword || '').toLowerCase();
|
||
const list = this.data.conversations || [];
|
||
|
||
const rough = list
|
||
.map(item => ({
|
||
item,
|
||
s: (String(item.name || '').toLowerCase().includes(q) ? 2 : 0) +
|
||
(String(item.lastMessage || '').toLowerCase().includes(q) ? 1 : 0)
|
||
}))
|
||
.sort((a, b) => b.s - a.s)
|
||
.slice(0, 8)
|
||
.map(x => x.item);
|
||
|
||
const tasks = [];
|
||
for (const conv of rough) {
|
||
const cid = conv.conversationId;
|
||
const loading = this.data.sampleLoading[cid];
|
||
const existing = this.data.conversationSamples[cid];
|
||
|
||
if (!loading && !existing) {
|
||
tasks.push(this.fetchConversationSample(conv));
|
||
}
|
||
}
|
||
|
||
if (tasks.length) {
|
||
await Promise.allSettled(tasks);
|
||
this.fuzzyFilterConversations(this.data.lastSearchKeyword);
|
||
}
|
||
},
|
||
|
||
async fetchConversationSample(conv) {
|
||
const cid = conv.conversationId;
|
||
try {
|
||
this.setData({
|
||
[`sampleLoading.${cid}`]: true
|
||
});
|
||
|
||
// 这里可以调用 NIM SDK 获取历史消息
|
||
// 暂时留空,等待实现
|
||
|
||
this.setData({
|
||
[`conversationSamples.${cid}`]: [],
|
||
[`sampleLoading.${cid}`]: false
|
||
});
|
||
} catch (e) {
|
||
console.error('抓取会话样本失败:', e);
|
||
this.setData({
|
||
[`sampleLoading.${cid}`]: false
|
||
});
|
||
}
|
||
},
|
||
|
||
// 打开聊天
|
||
openChat(e) {
|
||
// 如果正在长按,忽略单击事件
|
||
if (this.data.longPressing) {
|
||
console.log('长按中,忽略单击事件');
|
||
this.setData({ longPressing: false });
|
||
return;
|
||
}
|
||
|
||
const conversation = e.currentTarget.dataset.conversation;
|
||
|
||
if (!conversation.targetId) {
|
||
wx.showToast({
|
||
title: '会话信息不完整',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
const chatType = conversation.chatType !== undefined ? conversation.chatType : 0;
|
||
|
||
const params = {
|
||
conversationId: conversation.conversationId,
|
||
targetId: conversation.targetId,
|
||
name: encodeURIComponent(conversation.name),
|
||
chatType: chatType
|
||
};
|
||
|
||
const kw = (this.data.searchKeyword || '').trim();
|
||
if (kw) {
|
||
params.searchKeyword = encodeURIComponent(kw);
|
||
}
|
||
|
||
const queryString = Object.keys(params)
|
||
.map(key => `${key}=${params[key]}`)
|
||
.join('&');
|
||
|
||
wx.navigateTo({
|
||
url: `/pages/message/chat/chat?${queryString}`
|
||
});
|
||
},
|
||
|
||
// 显示会话选项
|
||
showConversationOptions(e) {
|
||
// 设置长按标记,防止长按后触发单击
|
||
this.setData({ longPressing: true });
|
||
|
||
const conversation = e.currentTarget.dataset.conversation;
|
||
|
||
const itemList = [
|
||
(conversation.isPinned || conversation.isTop) ? '取消置顶' : '置顶会话',
|
||
conversation.isMuted ? '取消免打扰' : '免打扰',
|
||
'不显示该聊天',
|
||
'删除会话'
|
||
];
|
||
|
||
wx.showActionSheet({
|
||
itemList: itemList,
|
||
success: (res) => {
|
||
switch (res.tapIndex) {
|
||
case 0:
|
||
this.toggleConversationTop(conversation.conversationId);
|
||
break;
|
||
case 1:
|
||
this.toggleConversationMute(conversation.conversationId);
|
||
break;
|
||
case 2:
|
||
this.hideConversation(conversation.conversationId);
|
||
break;
|
||
case 3:
|
||
this.deleteConversation(conversation.conversationId);
|
||
break;
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
// 标记会话已读
|
||
async markConversationRead(conversationId) {
|
||
try {
|
||
await nimConversationManager.markConversationRead(conversationId);
|
||
|
||
const conversations = [...this.data.conversations];
|
||
const index = conversations.findIndex(c => c.conversationId === conversationId);
|
||
|
||
if (index >= 0) {
|
||
conversations[index].unreadCount = 0;
|
||
this.processConversations(conversations);
|
||
}
|
||
} catch (error) {
|
||
console.error('标记已读失败:', error);
|
||
}
|
||
},
|
||
|
||
// 全部标记为已读
|
||
markAllRead() {
|
||
if (this._markingAll) return;
|
||
|
||
const unreadIds = (this.data.conversations || [])
|
||
.filter(c => (c.unreadCount || 0) > 0)
|
||
.map(c => c.conversationId)
|
||
.filter(Boolean);
|
||
|
||
if (!unreadIds.length) {
|
||
wx.showToast({ title: '没有未读消息', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
wx.showModal({
|
||
title: '确认操作',
|
||
content: '将所有会话标记为已读?',
|
||
success: async (res) => {
|
||
if (!res.confirm) return;
|
||
|
||
this._markingAll = true;
|
||
wx.showLoading({ title: '处理中...' });
|
||
|
||
try {
|
||
const tasks = unreadIds.map(id => this.markConversationRead(id));
|
||
await Promise.allSettled(tasks);
|
||
|
||
wx.hideLoading();
|
||
wx.showToast({ title: '已全部标记为已读', icon: 'success' });
|
||
} catch (err) {
|
||
console.error('批量标记已读失败:', err);
|
||
wx.hideLoading();
|
||
wx.showToast({ title: '操作失败', icon: 'none' });
|
||
} finally {
|
||
this._markingAll = false;
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
// 发起聊天入口
|
||
startNewChat() {
|
||
this.showMenu();
|
||
},
|
||
|
||
// ===== 隐藏会话 =====
|
||
_hiddenStorageKey: 'hiddenConversationIds',
|
||
|
||
_getHiddenConversationSet() {
|
||
try {
|
||
const arr = wx.getStorageSync(this._hiddenStorageKey) || [];
|
||
return new Set(arr);
|
||
} catch (_) {
|
||
return new Set();
|
||
}
|
||
},
|
||
|
||
_saveHiddenConversationSet(set) {
|
||
try {
|
||
wx.setStorageSync(this._hiddenStorageKey, Array.from(set));
|
||
} catch (_) { }
|
||
},
|
||
|
||
hideConversation(conversationId) {
|
||
const cid = String(conversationId || '');
|
||
if (!cid) return;
|
||
|
||
const set = this._getHiddenConversationSet();
|
||
if (!set.has(cid)) set.add(cid);
|
||
this._saveHiddenConversationSet(set);
|
||
|
||
this.processConversations(this.data.conversations || []);
|
||
wx.showToast({ title: '已隐藏该聊天', icon: 'none' });
|
||
},
|
||
|
||
_unhideConversationId(conversationId) {
|
||
const cid = String(conversationId || '');
|
||
if (!cid) return;
|
||
|
||
const set = this._getHiddenConversationSet();
|
||
if (set.has(cid)) {
|
||
set.delete(cid);
|
||
this._saveHiddenConversationSet(set);
|
||
}
|
||
},
|
||
|
||
// ===== 会话操作 =====
|
||
|
||
// 置顶/取消置顶会话
|
||
async toggleConversationTop(conversationId) {
|
||
const conversations = [...this.data.conversations];
|
||
const conv = conversations.find(c => c.conversationId === conversationId);
|
||
|
||
if (!conv) return;
|
||
|
||
const willPin = !conv.isPinned;
|
||
|
||
try {
|
||
wx.showLoading({ title: willPin ? '置顶中...' : '取消中...' });
|
||
|
||
await nimConversationManager.setConversationStickTop(conversationId, willPin);
|
||
|
||
conv.isPinned = willPin;
|
||
conv.isTop = willPin;
|
||
|
||
this.processConversations(conversations);
|
||
|
||
wx.hideLoading();
|
||
wx.showToast({
|
||
title: willPin ? '已置顶' : '已取消置顶',
|
||
icon: 'success'
|
||
});
|
||
} catch (error) {
|
||
console.error('置顶操作失败:', error);
|
||
wx.hideLoading();
|
||
wx.showToast({
|
||
title: '操作失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
},
|
||
|
||
// 静音/取消静音会话
|
||
async toggleConversationMute(conversationId) {
|
||
const conversations = [...this.data.conversations];
|
||
const conv = conversations.find(c => c.conversationId === conversationId);
|
||
|
||
if (!conv) return;
|
||
|
||
const willMute = !conv.isMuted;
|
||
conv.isMuted = willMute;
|
||
|
||
this.processConversations(conversations);
|
||
|
||
wx.showToast({
|
||
title: willMute ? '已设置免打扰' : '已取消免打扰',
|
||
icon: 'success'
|
||
});
|
||
},
|
||
|
||
// 删除会话
|
||
async deleteConversation(conversationId) {
|
||
try {
|
||
wx.showLoading({ title: '删除中...' });
|
||
|
||
await nimConversationManager.deleteConversation(conversationId);
|
||
|
||
const conversations = this.data.conversations.filter(
|
||
c => c.conversationId !== conversationId
|
||
);
|
||
this.processConversations(conversations);
|
||
|
||
wx.hideLoading();
|
||
wx.showToast({ title: '已删除', icon: 'success' });
|
||
} catch (error) {
|
||
console.error('删除会话失败:', error);
|
||
wx.hideLoading();
|
||
wx.showToast({ title: '删除失败', icon: 'none' });
|
||
}
|
||
},
|
||
|
||
// 显示菜单
|
||
showMenu() {
|
||
wx.showActionSheet({
|
||
itemList: ['创建群聊', '添加好友', '扫一扫', '设置'],
|
||
success: (res) => {
|
||
switch (res.tapIndex) {
|
||
case 0: this.createGroup(); break;
|
||
case 1: this.addFriend(); break;
|
||
case 2: this.scanQRCode(); break;
|
||
case 3: this.openSettings(); break;
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
// 创建群聊
|
||
createGroup() {
|
||
wx.navigateTo({ url: '/subpackages/group/create-group/create-group' });
|
||
},
|
||
|
||
// 添加好友
|
||
addFriend() {
|
||
wx.navigateTo({ url: '/subpackages/social/search/search' });
|
||
},
|
||
|
||
// 扫一扫
|
||
scanQRCode() {
|
||
wx.scanCode({
|
||
success: (res) => {
|
||
this.handleScanResult(res.result);
|
||
},
|
||
fail: (err) => {
|
||
console.error('扫码失败:', err);
|
||
if (err.errMsg && err.errMsg.includes('cancel')) {
|
||
// 用户取消扫码,不显示错误提示
|
||
return;
|
||
}
|
||
wx.showToast({
|
||
title: '扫码失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
// 处理扫码结果
|
||
handleScanResult(result) {
|
||
console.log('📱 扫码结果:', result);
|
||
|
||
try {
|
||
// 处理 FindMe 用户二维码格式: FINDME:{customId}
|
||
if (result && result.startsWith('FINDME:')) {
|
||
const customId = result.split(':')[1];
|
||
|
||
if (!customId) {
|
||
wx.showToast({
|
||
title: '无效的用户二维码',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 检查是否是自己
|
||
const app = getApp();
|
||
const currentUser = app.globalData.userInfo?.user;
|
||
|
||
if (currentUser && currentUser.customId === customId) {
|
||
wx.showToast({
|
||
title: '这是你自己的二维码',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 跳转到用户预览页(该页面会自动处理好友/非好友状态)
|
||
wx.navigateTo({
|
||
url: `/subpackages/social/user-preview/user-preview?customId=${customId}`,
|
||
fail: (err) => {
|
||
console.error('跳转用户预览页失败:', err);
|
||
wx.showToast({
|
||
title: '打开用户资料失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 尝试解析URL格式的二维码(兼容旧格式)
|
||
try {
|
||
const url = new URL(result);
|
||
const type = url.searchParams.get('type');
|
||
const id = url.searchParams.get('id');
|
||
|
||
if (type === 'user' && id) {
|
||
// 检查是否是自己
|
||
const app = getApp();
|
||
const currentUser = app.globalData.userInfo?.user;
|
||
|
||
if (currentUser && currentUser.customId === id) {
|
||
wx.showToast({
|
||
title: '这是你自己的二维码',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
wx.navigateTo({
|
||
url: `/subpackages/social/user-preview/user-preview?customId=${id}`
|
||
});
|
||
} else if (type === 'group' && id) {
|
||
this.joinGroupByQR(id);
|
||
} else {
|
||
wx.showToast({
|
||
title: '无效的二维码',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
} catch (urlError) {
|
||
// 不是URL格式,显示无效二维码提示
|
||
wx.showToast({
|
||
title: '无法识别的二维码格式',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('处理扫码结果失败:', error);
|
||
wx.showToast({
|
||
title: '处理二维码失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
},
|
||
|
||
// 通过二维码加入群聊
|
||
async joinGroupByQR(groupId) {
|
||
try {
|
||
wx.showLoading({ title: '加入中...' });
|
||
|
||
// 调用加入群组API
|
||
// await groupAPI.joinGroup(groupId);
|
||
|
||
wx.hideLoading();
|
||
wx.showToast({ title: '已加入群聊', icon: 'success' });
|
||
|
||
setTimeout(() => {
|
||
wx.navigateTo({
|
||
url: `/subpackages/group/group-info/group-info?groupId=${groupId}`
|
||
});
|
||
}, 1500);
|
||
} catch (error) {
|
||
console.error('加入群聊失败:', error);
|
||
wx.hideLoading();
|
||
wx.showToast({
|
||
title: '加入失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
},
|
||
|
||
// 打开设置
|
||
openSettings() {
|
||
wx.navigateTo({ url: '/subpackages/settings/settingss/settingss' });
|
||
},
|
||
|
||
// 页面显示
|
||
onShow: function () {
|
||
console.log('消息页面显示');
|
||
|
||
// 设置tabBar选中状态为"聊天"(索引2)
|
||
try {
|
||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||
this.getTabBar().setData({ selected: 2 });
|
||
}
|
||
} catch (_) {}
|
||
|
||
const currentToken = apiClient.getToken();
|
||
const app = getApp();
|
||
const isLoggedIn = app?.globalData?.isLoggedIn || false;
|
||
|
||
if (!currentToken) {
|
||
console.warn('⚠️ onShow检测到用户未登录');
|
||
if (isLoggedIn) {
|
||
this.redirectToLogin('onShow');
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 🔥 检查并重连 NIM(防止从后台恢复后 NIM 断开)
|
||
this.checkAndReconnectNim();
|
||
|
||
// 刷新会话列表
|
||
this.refreshConversations();
|
||
},
|
||
|
||
// 页面隐藏
|
||
onHide: function () {
|
||
console.log('消息页面隐藏');
|
||
},
|
||
|
||
// 页面卸载
|
||
onUnload: function () {
|
||
console.log('消息页面卸载');
|
||
|
||
// 移除 NIM 监听器
|
||
if (this.data.nimReady) {
|
||
nimConversationManager.off('conversation_created');
|
||
nimConversationManager.off('conversation_deleted');
|
||
nimConversationManager.off('conversation_changed');
|
||
nimConversationManager.off('new_message');
|
||
nimConversationManager.off('message_revoked');
|
||
}
|
||
}
|
||
});
|