miniprogramme/pages/message/message.js

1399 lines
42 KiB
JavaScript
Raw Normal View History

2025-09-12 16:08:17 +08:00
// 消息页面逻辑 - 基于Flutter实现
const app = getApp();
const apiClient = require('../../utils/api-client.js');
const wsManager = require('../../utils/websocket-manager-v2.js');
const chatAPI = require('../../utils/chat-api.js');
// 移除未使用的 friendAPI 依赖,避免无意义的打包与阅读负担
const { initPageSystemInfo } = require('../../utils/system-info-modern.js');
Page({
data: {
searchKeyword: '',
refreshing: false,
loading: false,
// 会话数据
conversations: [],
pinnedConversations: [],
normalConversations: [],
totalUnreadCount: 0,
// 过滤后的数据
filteredConversations: [],
// WebSocket状态
wsConnected: false,
// 系统适配信息
systemInfo: {},
statusBarHeight: 0,
menuButtonHeight: 0,
menuButtonTop: 0,
navBarHeight: 0,
windowHeight: 0,
safeAreaBottom: 0,
showSearch: false, // 新增:控制搜索栏的显示/隐藏
themeClass: 'theme-dark',
isThemeTransitioning: false,
// 主题切换径向遮罩
showThemeOverlay: false,
overlayPlaying: false,
overlayTo: 'theme-light',
overlayX: 0,
overlayY: 0,
overlayDiameter: 0,
overlayLeft: 0,
overlayTop: 0,
// 本地模糊搜索增强:抽样最近消息文本
searching: false,
conversationSamples: {}, // { [conversationId]: string[] }
sampleLoading: {}, // { [conversationId]: boolean }
lastSearchKeyword: ''
},
// 统一时间戳规范化:支持 number/纯数字字符串/ISO 字符串
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);
// 假定 10 位为秒13 位为毫秒
return s.length === 10 ? n * 1000 : n;
}
// ISO 或其他可被 Date 解析的格式
const d = Date.parse(s);
return isNaN(d) ? 0 : d;
}
// 其他类型直接尝试 Date
try {
const d = new Date(ts).getTime();
return isNaN(d) ? 0 : d;
} catch (_) {
return 0;
}
},
// 页面加载
onLoad: function (options) {
console.log('消息页面加载');
this.initSystemInfo();
this.checkAuthAndInit();
// 🔥 动态设置导航栏标题
wx.setNavigationBarTitle({
title: '消息'
});
},
// 检查认证状态并初始化
async checkAuthAndInit() {
try {
console.log('🔍 开始检查用户认证状态...');
// 检查多种token来源
const userInfo = wx.getStorageSync('userInfo');
const directToken = wx.getStorageSync('token');
const apiToken = apiClient.getToken();
console.log('🔍 Token检查结果:', {
hasUserInfo: !!userInfo,
hasUserInfoToken: !!(userInfo?.token),
hasDirectToken: !!directToken,
hasApiToken: !!apiToken,
userInfoTokenLength: userInfo?.token?.length || 0,
directTokenLength: directToken?.length || 0,
apiTokenLength: apiToken?.length || 0
});
// 确保API客户端能获取到token
const currentToken = apiToken || userInfo?.token || directToken;
if (!currentToken) {
console.error('❌ 用户未登录,准备跳转到登录页');
this.redirectToLogin('checkAuthAndInit');
return;
}
console.log('✅ 消息页面认证检查通过,开始初始化');
// 确保API客户端有token
if (!apiToken && currentToken) {
apiClient.setToken(currentToken);
console.log('🔑 已设置API客户端token');
}
// 初始化WebSocket和加载数据
this.initWebSocket();
this.loadConversations();
} catch (error) {
console.error('❌ 消息页面认证检查失败:', error);
this.redirectToLogin('exception');
}
},
// 🔥 统一的登录页跳转方法 - 使用退出登录的成功模式
redirectToLogin(source) {
// 防止重复跳转
if (this.redirecting) {
console.log(`⚠️ 正在跳转中,忽略来自${source}的跳转请求`);
return;
}
this.redirecting = true;
console.log(`🔄 来自${source}的跳转请求,执行跳转...`);
// 🔥 使用退出登录的成功模式固定1000ms延迟 + 简单的reLaunch
setTimeout(() => {
wx.reLaunch({
url: '/pages/login/login',
success: () => {
console.log('✅ 跳转到登录页成功');
this.redirecting = false;
},
fail: (error) => {
console.error('❌ 跳转到登录页失败:', error);
this.redirecting = false;
}
});
}, 1000); // 🔥 使用与退出登录相同的1000ms延迟
},
// 初始化系统信息
initSystemInfo() {
const pageSystemInfo = initPageSystemInfo();
console.log('消息页面系统适配信息:', {
statusBarHeight: pageSystemInfo.statusBarHeight,
menuButtonHeight: pageSystemInfo.menuButtonHeight,
menuButtonTop: pageSystemInfo.menuButtonTop,
navBarHeight: pageSystemInfo.navBarHeight,
windowHeight: pageSystemInfo.windowHeight,
safeAreaBottom: pageSystemInfo.safeAreaBottom
});
this.setData({
systemInfo: pageSystemInfo.systemInfo,
statusBarHeight: pageSystemInfo.statusBarHeight,
menuButtonHeight: pageSystemInfo.menuButtonHeight,
menuButtonTop: pageSystemInfo.menuButtonTop,
navBarHeight: pageSystemInfo.navBarHeight,
windowHeight: pageSystemInfo.windowHeight,
safeAreaBottom: pageSystemInfo.safeAreaBottom
});
},
// 初始化WebSocket连接
initWebSocket() {
try {
console.log('初始化WebSocket连接');
// 获取用户token - 使用更可靠的方式
let userInfo = app.globalData.userInfo;
let token = null;
if (userInfo && userInfo.token) {
token = userInfo.token;
} else {
// 尝试从本地存储获取
try {
const storedUserInfo = wx.getStorageSync('userInfo');
if (storedUserInfo && storedUserInfo.token) {
token = storedUserInfo.token;
userInfo = storedUserInfo;
// 更新全局数据
app.globalData.userInfo = userInfo;
app.globalData.isLoggedIn = true;
}
} catch (error) {
console.error('从本地存储获取用户信息失败:', error);
}
}
if (!token) {
console.error('WebSocket连接失败: 缺少认证token');
// 不直接return而是设置一个延迟重试
setTimeout(() => {
this.initWebSocket();
}, 2000);
return;
}
// 设置token并连接
wsManager.setToken(token);
// 注册消息处理器
wsManager.on('new_message', (message) => {
console.log('收到新消息:', message);
this.handleNewMessage(message.data);
});
wsManager.on('conversation_update', (message) => {
console.log('会话更新:', message);
this.handleConversationUpdate(message.data);
});
wsManager.on('friend_request', (message) => {
console.log('收到好友请求:', message);
this.handleFriendRequest(message.data);
});
wsManager.on('global_connected', () => {
console.log('WebSocket连接成功');
this.setData({ wsConnected: true });
});
wsManager.on('global_disconnected', () => {
console.log('WebSocket连接断开');
this.setData({ wsConnected: false });
});
// 🔥 注册聊天功能处理器
wsManager.onChatMessage((message) => {
console.log('🔥 收到新聊天消息:', message);
this.handleNewMessage(message.data);
});
wsManager.onUnreadCountUpdate((message) => {
console.log('🔥 未读数更新:', message);
this.updateUnreadCount(message.data);
});
// 🔥 注册好友功能处理器
wsManager.onNotification((message) => {
console.log('🔥 收到通知:', message);
this.handleNotification(message.data);
});
wsManager.onFriendRequest((message) => {
console.log('🔥 收到好友请求:', message);
this.handleFriendRequest(message.data);
});
// 🔥 注册在线状态事件presence
const handleOnline = (msg) => {
console.log('👤 用户上线:', msg);
this.updatePresenceStatus(msg?.data || msg, true);
};
const handleOffline = (msg) => {
console.log('👤 用户离线:', msg);
this.updatePresenceStatus(msg?.data || msg, false);
};
const handlePresence = (msg) => {
const data = msg?.data || msg;
const online = typeof data?.online === 'boolean' ? data.online : undefined;
console.log('👤 在线状态更新:', data);
if (online !== undefined) {
this.updatePresenceStatus(data, online);
}
};
this._presenceHandlers = { handleOnline, handleOffline, handlePresence };
wsManager.on('user_online', handleOnline);
wsManager.on('user_offline', handleOffline);
wsManager.on('presence_update', handlePresence);
// 开始连接
wsManager.connect();
} catch (error) {
console.error('初始化WebSocket失败:', error);
}
},
// 处理新消息
handleNewMessage(messageData) {
console.log('处理新消息:', messageData);
// 保护如果没有会话ID或消息为空直接返回
if (!messageData) return;
// 复制列表以防直接修改原数组
const conversations = Array.isArray(this.data.conversations) ? [...this.data.conversations] : [];
// 更健壮的匹配:支持 conversationId、id、targetId或 senderId等字段
const incomingConvId = messageData.conversationId || messageData.conversation_id || messageData.id || '';
const incomingTargetId = messageData.targetId || messageData.receiverId || messageData.to || messageData.senderId || '';
let foundIndex = conversations.findIndex(conv => {
return conv.conversationId === incomingConvId || conv.id === incomingConvId ||
conv.targetId === incomingTargetId || conv.targetId === (messageData.senderId || messageData.from);
});
if (foundIndex >= 0) {
// 更新现有会话
const conv = conversations[foundIndex];
const ts = Number(messageData.sendTime || Date.now());
conversations[foundIndex] = {
...conv,
lastMessage: this.getMessagePreview(messageData),
lastMessageType: messageData.msgType || 'text',
lastMessageTime: this.formatMessageTime(ts),
lastMsgTime: ts,
// 只有当消息不是自己发出的情况下才增加未读
unreadCount: (conv.unreadCount || 0) + ((messageData.senderId && (messageData.senderId !== (this.data.userInfo?.user?.customId || ''))) ? 1 : 0)
};
console.log('更新会话index=' + foundIndex + ')未读数为:', conversations[foundIndex].unreadCount);
} else {
// 未找到会话:创建一个最小化的会话记录并插入到列表顶部
const newConvId = incomingConvId || `conv_${Date.now()}`;
const senderName = messageData.senderName || messageData.fromName || '陌生人';
const ts = Number(messageData.sendTime || Date.now());
const newConversation = {
id: newConvId,
conversationId: newConvId,
type: messageData.chatType || 0,
name: senderName,
avatar: messageData.senderAvatar || '',
isOnline: false,
isPinned: false,
isMuted: false,
unreadCount: 1,
lastMessage: this.getMessagePreview(messageData),
lastMessageType: messageData.msgType || 'text',
lastMessageTime: this.formatMessageTime(ts),
lastMsgTime: ts,
messageStatus: 'received',
targetId: incomingTargetId || messageData.senderId || ''
};
conversations.unshift(newConversation);
console.log('新增会话以显示未读提示:', newConversation);
}
// 写回并统一处理排序/角标等
this.setData({ conversations });
this.processConversations(conversations);
// 播放提示音
this.playMessageSound();
// 更新tab角标processConversations 内也会调用,但这里保证即时性)
this.updateTabBadge();
},
// 获取消息预览
getMessagePreview(message) {
switch (message.msgType) {
case 'text':
return message.content || '[文本消息]';
case 'image':
return '[图片]';
case 'audio':
return '[语音]';
case 'video':
return '[视频]';
case 'file':
return '[文件]';
case 'location':
return '[位置]';
default:
return '[消息]';
}
},
// 处理会话更新
handleConversationUpdate(updateData) {
console.log('会话更新:', updateData);
// 重新加载会话列表
this.loadConversations();
},
// 处理好友请求(精简:保留一个实现)
handleFriendRequest(requestData) {
console.log('收到好友请求:', requestData);
wx.showToast({ title: '收到新的好友请求', icon: 'none', duration: 2000 });
},
// 页面显示
onShow: function () {
console.log('消息页面显示');
// 初始化主题
const savedTheme = wx.getStorageSync('theme') || 'theme-dark';
this.setData({ themeClass: savedTheme });
// 🔥 在刷新前检查登录状态
const currentToken = apiClient.getToken();
if (!currentToken) {
console.warn('⚠️ onShow检测到用户未登录跳转到登录页');
this.redirectToLogin('onShow');
return;
}
this.refreshConversations();
// 启动定时刷新
if (this._refreshTimer) clearInterval(this._refreshTimer);
this._refreshTimer = setInterval(() => {
console.log('定时刷新会话列表...');
this.loadConversations();
}, 10000); // 每10秒刷新一次
},
// 页面隐藏
onHide: function () {
console.log('消息页面隐藏');
if (this._refreshTimer) {
clearInterval(this._refreshTimer);
this._refreshTimer = null;
}
},
// 页面卸载
onUnload: function () {
console.log('消息页面卸载');
if (this._refreshTimer) {
clearInterval(this._refreshTimer);
this._refreshTimer = null;
}
// 移除WebSocket监听器
wsManager.off('new_message');
wsManager.off('conversation_update');
wsManager.off('friend_request');
wsManager.off('global_connected');
wsManager.off('global_disconnected');
if (this._presenceHandlers) {
wsManager.off('user_online', this._presenceHandlers.handleOnline);
wsManager.off('user_offline', this._presenceHandlers.handleOffline);
wsManager.off('presence_update', this._presenceHandlers.handlePresence);
this._presenceHandlers = null;
}
},
// 🔥 在线状态更新:根据 userId/targetId 更新对应会话的 isOnline
updatePresenceStatus(data, isOnline) {
try {
const userId = data?.userId || data?.targetId || data?.uid || data?.id || data?.customId;
if (!userId) {
console.warn('⚠️ presence 事件缺少用户ID:', data);
return;
}
const conversations = (this.data.conversations || []).map(conv => {
// 单聊targetId 等于该用户;群聊忽略
if ((conv.type === 'single' || conv.chatType === 0) && (conv.targetId == userId || String(conv.targetId) === String(userId))) {
return { ...conv, isOnline: !!isOnline };
}
return conv;
});
// 仅当有变化时写回
const changed = JSON.stringify(conversations) !== JSON.stringify(this.data.conversations || []);
if (changed) {
this.setData({ conversations });
this.processConversations(conversations);
}
} catch (err) {
console.error('❌ 更新在线状态失败:', err);
}
},
// 加载会话列表
async loadConversations() {
if (this.data.loading) return;
// 🔥 在加载前检查登录状态
const currentToken = apiClient.getToken();
if (!currentToken) {
console.warn('⚠️ loadConversations检测到用户未登录停止加载');
return;
}
try {
this.setData({ loading: true });
console.log('🔥 开始加载会话列表');
const response = await chatAPI.getConversations();
console.log('会话列表API响应:', response);
if (response && response.code === 0) {
const conversations = response.data || [];
console.log('处理会话数据:', conversations);
// 🔥 处理会话数据格式,过滤掉无效会话
const processedConversations = conversations
.map(conv => this.processConversationData(conv))
.filter(conv => conv !== null); // 过滤掉null值
console.log('处理后的会话数量:', processedConversations.length);
this.processConversations(processedConversations);
} else {
throw new Error(response?.message || '获取会话列表失败');
}
} catch (error) {
console.error('加载会话失败:', error);
wx.showToast({
title: '加载失败',
icon: 'none'
});
} finally {
this.setData({ loading: false });
}
},
// 处理会话数据格式
processConversationData(conv) {
// 🔥 完全使用后端返回的会话ID不再前端生成
const conversationId = conv.id || conv.conversationId;
if (!conversationId) {
console.error('会话缺少ID跳过该会话:', conv);
return null; // 如果没有会话ID跳过该会话
}
console.log('使用后端会话ID:', conversationId);
// 统一提取原始时间戳
const candidates = [conv.lastMsgTime, conv.lastMessageTimeTs, conv.updatedAt, conv.lastActiveAt];
let rawTs = 0;
for (const c of candidates) {
const v = this.normalizeTimestamp(c);
if (v) { rawTs = v; break; }
}
return {
id: conversationId,
conversationId: conversationId,
type: conv.type === 1 ? 'group' : 'single', // 使用正确的type字段
name: conv.targetName || conv.name || '未知',
avatar: conv.targetAvatar || '',
isOnline: conv.isOnline || false,
isPinned: conv.isTop || false,
isMuted: conv.isMuted || false,
unreadCount: conv.unreadCount || 0,
lastMessage: conv.lastMsgContent || conv.lastMessage || '',
lastMessageType: conv.lastMsgType || 'text',
lastMessageTime: this.formatMessageTime(rawTs),
lastMsgTime: rawTs,
messageStatus: conv.messageStatus || 'sent',
targetId: conv.targetId || conv.userId || '',
chatType: conv.type || conv.chatType || 0 // 使用正确的type字段
};
},
// 格式化消息时间Telegram 风格)
formatMessageTime(timestamp) {
if (!timestamp) return '';
const now = new Date();
const msg = new Date(timestamp);
// 今天HH:mm
if (now.toDateString() === msg.toDateString()) {
const hh = msg.getHours().toString().padStart(2, '0');
const mm = msg.getMinutes().toString().padStart(2, '0');
return `${hh}:${mm}`;
}
// 昨天
const y = new Date(now);
y.setDate(y.getDate() - 1);
if (y.toDateString() === msg.toDateString()) {
return '昨天';
}
// 更早YYYY/MM/DD
const year = msg.getFullYear();
const month = (msg.getMonth() + 1).toString().padStart(2, '0');
const day = msg.getDate().toString().padStart(2, '0');
return `${year}/${month}/${day}`;
},
// 播放消息提示音
playMessageSound() {
try {
// 简单的提示,小程序没有播放背景音频的能力
wx.vibrateShort();
} catch (error) {
console.log('提示异常:', error);
}
},
// 更新Tab角标
updateTabBadge() {
const totalUnread = this.data.totalUnreadCount;
if (totalUnread > 0) {
wx.setTabBarBadge({
index: 1, // 消息tab的索引
text: totalUnread > 99 ? '99+' : totalUnread.toString()
});
} else {
wx.removeTabBarBadge({
index: 1
});
}
},
// 处理会话数据
processConversations(conversations) {
// 过滤已删除项,并统一关键字段
const validList = (conversations || [])
.filter(it => it && !it.deleted && !it._deleted)
.map(it => ({
...it,
// 统一置顶字段isTop 与 isPinned 兼容)
isPinned: !!(it.isPinned || it.isTop),
// 统一时间字段用于排序(降序)
_ts: this.normalizeTimestamp(it.lastMsgTime || it.lastMessageTimeTs || it.lastActiveAt || it.updatedAt)
}));
// 置顶/非置顶分组并组内按时间降序
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);
const sortedAll = [...pinnedConversations, ...normalConversations];
const totalUnreadCount = validList.reduce((sum, item) => sum + (item.unreadCount || 0), 0);
this.setData({
conversations: sortedAll,
pinnedConversations,
normalConversations,
totalUnreadCount,
filteredConversations: sortedAll
});
// 更新角标
this.updateTabBadge();
},
// 刷新会话列表
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.setData({ searching: false });
this.processConversations(this.data.conversations);
return;
}
// 立即本地模糊过滤
this.fuzzyFilterConversations(keyword);
// 异步预取若干可能相关会话的最近消息样本
this.prefetchTopConversationSamples(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); },
// 本地模糊过滤:名称/最后一条消息/抽样最近消息(文本)
fuzzyFilterConversations(keyword) {
if (!keyword) {
this.processConversations(this.data.conversations);
return;
}
const q = (keyword || '').toLowerCase();
const subseqScore = (text, query) => {
if (!text || !query) return 0;
const s = String(text).toLowerCase();
let pos = 0, score = 0, streak = 0;
for (const ch of query) {
const idx = s.indexOf(ch, pos);
if (idx === -1) return score;
score += 1;
streak = (idx === pos) ? streak + 1 : 1;
score += Math.min(streak, 3) * 0.5;
pos = idx + 1;
}
if (s.startsWith(query)) score += 2;
if (s.includes(query)) score += 1;
return score;
};
const samples = this.data.conversationSamples || {};
const list = this.data.conversations || [];
const withScore = list.map(item => {
const nameScore = subseqScore(item.name || '', q);
const lastScore = subseqScore(item.lastMessage || '', q);
const sampleTexts = samples[item.conversationId] || [];
let sampleScore = 0;
for (const t of sampleTexts) sampleScore = Math.max(sampleScore, subseqScore(t, q));
const score = nameScore * 2 + Math.max(sampleScore * 1.5, lastScore);
return { item, score };
}).filter(x => x.score > 0);
withScore.sort((a, b) => {
if (b.score !== a.score) return b.score - a.score;
const at = this.normalizeTimestamp(a.item.lastMsgTime || a.item.lastMessageTimeTs);
const bt = this.normalizeTimestamp(b.item.lastMsgTime || b.item.lastMessageTimeTs);
return bt - at;
});
let filtered = withScore.map(x => x.item);
if (filtered.length === 0) {
// 兜底:简单包含匹配
filtered = list.filter(item => {
const name = String(item.name || '').toLowerCase();
const msg = String(item.lastMessage || '').toLowerCase();
return name.includes(q) || msg.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 || [];
// 粗筛:名称/最后消息包含命中的取前8
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;
if (!cid) continue;
if ((this.data.conversationSamples||{})[cid]) continue;
if ((this.data.sampleLoading||{})[cid]) continue;
tasks.push(this.fetchConversationSample(conv));
if (tasks.length >= 4) break; // 控制并发与数量
}
if (tasks.length) {
await Promise.allSettled(tasks);
// 关键词未变化时刷新一次
if ((this.data.lastSearchKeyword||'') === (keyword||'')) {
this.fuzzyFilterConversations(keyword);
}
}
},
async fetchConversationSample(conv) {
const cid = conv.conversationId;
try {
const loading = { ...(this.data.sampleLoading||{}) };
loading[cid] = true;
this.setData({ sampleLoading: loading });
const params = {
receiverId: conv.targetId,
chatType: conv.chatType !== undefined ? conv.chatType : (conv.type === 'group' ? 1 : 0),
limit: 20,
direction: 'before'
};
const resp = await apiClient.get('/api/v1/chat/history', params);
let texts = [];
if (resp && (resp.code === 0 || resp.code === 200)) {
const arr = Array.isArray(resp.data) ? resp.data : (Array.isArray(resp.data?.list) ? resp.data.list : []);
texts = arr
.map(m => {
const c = (typeof m?.content === 'string' && m.content) ? m.content
: (typeof m?.text === 'string' ? m.text : (typeof m?.summary === 'string' ? m.summary : ''));
return c;
})
.filter(s => typeof s === 'string' && s.trim())
.slice(0, 20);
}
const samples = { ...(this.data.conversationSamples||{}) };
samples[cid] = texts;
const loadingDone = { ...(this.data.sampleLoading||{}) };
delete loadingDone[cid];
this.setData({ conversationSamples: samples, sampleLoading: loadingDone });
} catch (e) {
const loadingDone = { ...(this.data.sampleLoading||{}) };
delete loadingDone[cid];
this.setData({ sampleLoading: loadingDone });
}
},
// 移除旧版 filterConversations已被 fuzzyFilterConversations 取代
// 打开聊天
openChat(e) {
const conversation = e.currentTarget.dataset.conversation;
console.log('打开聊天:', conversation);
// 检查必要参数
if (!conversation.targetId) {
console.error('缺少targetId无法打开聊天');
wx.showToast({
title: '聊天信息错误',
icon: 'none'
});
return;
}
// 标记为已读
if (conversation.unreadCount > 0) {
this.markConversationRead(conversation.conversationId);
}
// 🔥 确保chatType有默认值
const chatType = conversation.chatType !== undefined ? conversation.chatType : 0;
console.log('确定的chatType:', chatType, '原始值:', conversation.chatType);
// 跳转到聊天页面
const params = {
conversationId: conversation.conversationId,
targetId: conversation.targetId,
name: encodeURIComponent(conversation.name),
chatType: chatType
};
// 若当前存在搜索关键词,则作为锚点关键词传递,用于在聊天页内定位
const kw = (this.data.searchKeyword || '').trim();
if (kw) {
params.keyword = encodeURIComponent(kw);
}
const queryString = Object.keys(params)
.map(key => `${key}=${params[key]}`)
.join('&');
console.log('聊天页面参数:', params);
wx.navigateTo({
url: `/pages/message/chat/chat?${queryString}`
});
},
// 显示会话选项
showConversationOptions(e) {
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.deleteConversation(conversation.conversationId);
break;
}
}
});
},
// 标记会话已读
async markConversationRead(conversationId) {
try {
// TODO: 调用API标记已读
await apiClient.markMessagesRead(conversationId);
// 更新本地数据
const conversations = this.data.conversations.map(conv => {
if (conv.conversationId === conversationId) {
return { ...conv, unreadCount: 0 };
}
return conv;
});
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;
try {
// 1) 立即本地清零,给用户即时反馈
const conversations = (this.data.conversations || []).map(conv => ({
...conv,
unreadCount: 0
}));
this.processConversations(conversations);
wx.showToast({ title: '全部已读', icon: 'success' });
// 2) 后台逐会话标记已读(并发执行)
await Promise.allSettled(unreadIds.map(id => chatAPI.markAllRead(id)));
// 3) 刷新总未读徽章(防止后端有残留)
try {
const resp = await chatAPI.getTotalUnreadCount();
const total = (resp && (resp.code === 0 || resp.code === 200) && (resp.data?.total || resp.total)) ? (resp.data?.total || resp.total) : 0;
this.setData({ totalUnreadCount: total });
if (total > 0) {
wx.setTabBarBadge({ index: 1, text: total > 99 ? '99+' : String(total) });
} else {
wx.removeTabBarBadge({ index: 1 });
}
} catch (_) { /* 忽略统计刷新失败 */ }
} catch (error) {
console.error('全部标记已读失败:', error);
wx.showToast({ title: '操作失败', icon: 'none' });
} finally {
this._markingAll = false;
}
}
});
},
// 发起聊天入口:统一打开顶部菜单(创建群聊/添加好友/扫一扫/设置)
startNewChat() { this.showMenu(); },
// 🔥 ===== 新增的聊天功能处理方法 =====
// 更新未读数
updateUnreadCount(data) {
try {
if (data.conversationId) {
// 更新特定会话的未读数
const conversations = this.data.conversations.map(conv => {
if (conv.conversationId === data.conversationId) {
return {
...conv,
unreadCount: data.unreadCount || 0
};
}
return conv;
});
this.setData({ conversations });
this.processConversations(conversations);
}
if (data.totalUnreadCount !== undefined) {
// 更新总未读数
this.setData({ totalUnreadCount: data.totalUnreadCount });
// 更新tabBar徽章
if (data.totalUnreadCount > 0) {
wx.setTabBarBadge({
index: 1, // 消息tab的索引
text: data.totalUnreadCount > 99 ? '99+' : data.totalUnreadCount.toString()
});
} else {
wx.removeTabBarBadge({ index: 1 });
}
}
} catch (error) {
console.error('更新未读数失败:', error);
}
},
// 处理通知
handleNotification(data) {
try {
console.log('处理通知:', data);
// 根据通知类型进行不同处理
switch (data.type) {
case 'new_friend_request':
this.handleFriendRequestNotification(data);
break;
case 'friend_accepted':
this.handleFriendAcceptedNotification(data);
break;
case 'friend_rejected':
this.handleFriendRejectedNotification(data);
break;
default:
console.log('未知通知类型:', data.type);
}
} catch (error) {
console.error('处理通知失败:', error);
}
},
// 移除重复的 handleFriendRequest 定义(上方已保留)
// 处理好友请求通知
handleFriendRequestNotification(data) {
wx.showToast({
title: `${data.senderName} 想和您成为好友`,
icon: 'none',
duration: 2000
});
},
// 处理好友请求被接受通知
handleFriendAcceptedNotification(data) {
wx.showToast({
title: `${data.senderName} 接受了您的好友请求`,
icon: 'success',
duration: 2000
});
},
// 处理好友请求被拒绝通知
handleFriendRejectedNotification(data) {
wx.showToast({
title: '好友请求被拒绝',
icon: 'none',
duration: 2000
});
},
// 🔥 ===== 会话操作增强 =====
// 置顶/取消置顶会话
async toggleConversationTop(conversationId) {
try {
const conversation = this.data.conversations.find(conv => conv.conversationId === conversationId);
if (!conversation) return;
const newTopStatus = !conversation.isTop;
await chatAPI.updateConversation(conversationId, {
isTop: newTopStatus
});
// 更新本地数据
const conversations = this.data.conversations.map(conv => {
if (conv.conversationId === conversationId) {
return { ...conv, isTop: newTopStatus };
}
return conv;
});
this.setData({ conversations });
this.processConversations(conversations);
wx.showToast({
title: newTopStatus ? '已置顶' : '已取消置顶',
icon: 'success'
});
} catch (error) {
console.error('切换置顶状态失败:', error);
wx.showToast({
title: '操作失败',
icon: 'none'
});
}
},
// 静音/取消静音会话
async toggleConversationMute(conversationId) {
try {
const conversation = this.data.conversations.find(conv => conv.conversationId === conversationId);
if (!conversation) return;
const newMuteStatus = !conversation.isMuted;
await chatAPI.updateConversation(conversationId, {
isMuted: newMuteStatus
});
// 更新本地数据
const conversations = this.data.conversations.map(conv => {
if (conv.conversationId === conversationId) {
return { ...conv, isMuted: newMuteStatus };
}
return conv;
});
this.setData({ conversations });
this.processConversations(conversations);
wx.showToast({
title: newMuteStatus ? '已静音' : '已取消静音',
icon: 'success'
});
} catch (error) {
console.error('切换静音状态失败:', error);
wx.showToast({
title: '操作失败',
icon: 'none'
});
}
},
// 删除会话
async deleteConversation(conversationId) {
try {
await chatAPI.deleteConversation(conversationId);
// 从本地数据中移除
const conversations = this.data.conversations.filter(conv => conv.conversationId !== conversationId);
this.setData({ conversations });
this.processConversations(conversations);
wx.showToast({
title: '已删除',
icon: 'success'
});
} catch (error) {
console.error('删除会话失败:', error);
wx.showToast({
title: '删除失败',
icon: 'none'
});
}
},
// 显示菜单
showMenu() {
console.log('📋 显示菜单');
const itemList = ['创建群聊', '添加好友', '扫一扫', '设置'];
wx.showActionSheet({
itemList: 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;
default:
console.log('选择菜单项:', res.tapIndex);
}
}
});
},
// 创建群聊
createGroup() {
console.log('👥 创建群聊');
wx.navigateTo({
url: '/pages/group/create-group/create-group'
});
},
// 添加好友
addFriend() {
console.log('👤 添加好友');
wx.navigateTo({
url: '/pages/social/search/search'
});
},
// 扫一扫
scanQRCode() {
console.log('📷 扫一扫');
wx.scanCode({
success: (res) => {
console.log('扫码结果:', res);
// 处理扫码结果
this.handleScanResult(res);
},
fail: (error) => {
console.error('❌ 扫码失败:', error);
wx.showToast({
title: '扫码失败',
icon: 'none'
});
}
});
},
// 处理扫码结果
handleScanResult(result) {
const { result: scanResult } = result;
try {
// 尝试解析为JSON
const data = JSON.parse(scanResult);
if (data.type === 'user') {
// 用户二维码
wx.navigateTo({
url: `/pages/social/user-profile/user-profile?userId=${data.userId}`
});
} else if (data.type === 'group') {
// 群聊二维码
wx.showModal({
title: '加入群聊',
content: `确定要加入群聊"${data.groupName}"吗?`,
success: (res) => {
if (res.confirm) {
this.joinGroupByQR(data.groupId);
}
}
});
} else {
// 其他类型
wx.showToast({
title: '不支持的二维码类型',
icon: 'none'
});
}
} catch (error) {
// 不是JSON格式可能是URL或其他格式
if (scanResult.startsWith('http')) {
// 网址
wx.showModal({
title: '打开网址',
content: scanResult,
success: (res) => {
if (res.confirm) {
// 复制到剪贴板
wx.setClipboardData({
data: scanResult
});
}
}
});
} else {
// 其他文本
wx.showModal({
title: '扫码结果',
content: scanResult,
showCancel: false
});
}
}
},
// 通过二维码加入群聊
async joinGroupByQR(groupId) {
try {
wx.showLoading({
title: '加入群聊中...'
});
const response = await apiClient.request({
url: `/api/v1/groups/${groupId}/join`,
method: 'POST'
});
wx.hideLoading();
if (response.success) {
wx.showToast({
title: '加入群聊成功',
icon: 'success'
});
// 刷新会话列表
this.loadConversations();
} else {
wx.showToast({
title: response.error || '加入群聊失败',
icon: 'none'
});
}
} catch (error) {
wx.hideLoading();
console.error('❌ 加入群聊失败:', error);
wx.showToast({
title: '加入群聊失败',
icon: 'none'
});
}
},
// 打开设置
openSettings() {
console.log('⚙️ 打开设置');
wx.navigateTo({
url: '/pages/profile/profile'
});
},
// 切换主题(带过渡动画)
toggleTheme() {
const next = this.data.themeClass === 'theme-dark' ? 'theme-light' : 'theme-dark';
const query = wx.createSelectorQuery();
query.select('.theme-toggle').boundingClientRect();
query.selectViewport().boundingClientRect();
query.exec((res) => {
const btnRect = res && res[0];
const vpRect = res && res[1];
// 兜底:左上角区域,和按钮在左侧的位置一致
let cx = 24;
let cy = (this.data.statusBarHeight || 0) + 16;
if (btnRect) {
cx = btnRect.left + btnRect.width / 2;
cy = btnRect.top + btnRect.height / 2;
}
const w = vpRect ? vpRect.width : (this.data.systemInfo?.windowWidth || 375);
const h = vpRect ? vpRect.height : (this.data.systemInfo?.windowHeight || this.data.windowHeight || 667);
const dx = Math.max(cx, w - cx);
const dy = Math.max(cy, h - cy);
const radius = Math.ceil(Math.sqrt(dx * dx + dy * dy)) + 12;
const diameter = radius * 2;
const left = Math.round(cx - radius);
const top = Math.round(cy - radius);
this.setData({
isThemeTransitioning: true,
showThemeOverlay: true,
overlayPlaying: false,
overlayTo: next,
overlayX: cx,
overlayY: cy,
overlayDiameter: diameter,
overlayLeft: left,
overlayTop: top
});
setTimeout(() => {
this.setData({ overlayPlaying: true, themeClass: next });
try { wx.setStorageSync('theme', next); } catch (e) { console.warn('保存主题失败', e); }
}, 16);
setTimeout(() => {
this.setData({ showThemeOverlay: false, overlayPlaying: false, isThemeTransitioning: false });
}, 420);
});
}
});