第一次提交:微信小程序前端最新完整代码
This commit is contained in:
400
utils/chat-core.js
Normal file
400
utils/chat-core.js
Normal file
@@ -0,0 +1,400 @@
|
||||
// utils/chat-core.js — 全局 IM 连接、监听、角标(稳定版)
|
||||
import GoEasy from '../static/lib/goeasy-2.13.24.esm.min';
|
||||
import { resolveAvatarUrl } from './avatar.js';
|
||||
import { getLocalImUserId, getLocalImIdentity } from './im-user.js';
|
||||
|
||||
let _convListener = null;
|
||||
let _privateListener = null;
|
||||
let _groupListener = null;
|
||||
let _notificationHandler = null;
|
||||
let _heartbeatTimer = null;
|
||||
let _listenersBoundForUser = null;
|
||||
let _lastGroupSubscribeAt = 0;
|
||||
const _notifiedMessageIds = new Map();
|
||||
const NOTIFY_MAX_AGE_MS = 15000;
|
||||
|
||||
function shouldShowNotification(message) {
|
||||
if (!message) return false;
|
||||
const id = message.messageId || `${message.senderId}_${message.timestamp}`;
|
||||
if (_notifiedMessageIds.has(id)) return false;
|
||||
if (message.timestamp && Date.now() - message.timestamp > NOTIFY_MAX_AGE_MS) return false;
|
||||
_notifiedMessageIds.set(id, Date.now());
|
||||
if (_notifiedMessageIds.size > 400) {
|
||||
const cutoff = Date.now() - 60000;
|
||||
_notifiedMessageIds.forEach((t, k) => {
|
||||
if (t < cutoff) _notifiedMessageIds.delete(k);
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function bindGlobalListenersIfNeeded(app, userId) {
|
||||
if (_listenersBoundForUser === userId && _convListener) return;
|
||||
registerGlobalListeners(app);
|
||||
registerNotification(app);
|
||||
_listenersBoundForUser = userId;
|
||||
}
|
||||
|
||||
function initGlobalMessageSystem(app) {
|
||||
app.ensureConnection = () => ensureConnection(app);
|
||||
app.connectForCurrentRole = () => ensureConnection(app);
|
||||
app.loadConversations = () => loadConversations(app);
|
||||
app.disconnectGoEasy = () => disconnectGoEasy(app);
|
||||
app.switchRoleAndReconnect = (newRole) => switchRoleAndReconnect(app, newRole);
|
||||
app.updateUnreadCount = (count) => updateTabBarBadge(app, count || 0);
|
||||
app.reregisterNotification = () => registerNotification(app);
|
||||
app.startImWhenReady = () => startImWhenReady(app);
|
||||
|
||||
if (!app.globalData.messageManager) {
|
||||
app.globalData.messageManager = {};
|
||||
}
|
||||
try {
|
||||
const muted = wx.getStorageSync('notificationMuted');
|
||||
if (muted !== undefined) app.globalData.messageManager.notificationMuted = muted;
|
||||
} catch (e) {}
|
||||
|
||||
initAppStateListener(app);
|
||||
initNetworkListener(app);
|
||||
startHeartbeat(app);
|
||||
startImWhenReady(app);
|
||||
}
|
||||
|
||||
/** GoEasy 未就绪时轮询,登录后尽早建立监听 */
|
||||
function startImWhenReady(app, maxAttempts = 40) {
|
||||
let attempts = 0;
|
||||
const tick = () => {
|
||||
if (!wx.getStorageSync('uid')) return;
|
||||
attempts += 1;
|
||||
if (!wx.goEasy) {
|
||||
if (attempts < maxAttempts) setTimeout(tick, 250);
|
||||
return;
|
||||
}
|
||||
ensureConnection(app);
|
||||
};
|
||||
tick();
|
||||
}
|
||||
|
||||
function getCurrentUserId() {
|
||||
return (wx.goEasy && wx.goEasy.im && wx.goEasy.im.userId) || null;
|
||||
}
|
||||
|
||||
function getConnectionStatus() {
|
||||
if (!wx.goEasy || !wx.goEasy.getConnectionStatus) return 'disconnected';
|
||||
return wx.goEasy.getConnectionStatus();
|
||||
}
|
||||
|
||||
function isConnected() {
|
||||
const s = getConnectionStatus();
|
||||
return s === 'connected' || s === 'reconnected';
|
||||
}
|
||||
|
||||
/** 统一会话数据结构,避免不同回调格式导致列表空白 */
|
||||
function normalizeConversationsPayload(result) {
|
||||
if (!result) return { conversations: [], unreadTotal: 0 };
|
||||
const content = result.content || result;
|
||||
const conversations = content.conversations || result.conversations || [];
|
||||
const unreadTotal = content.unreadTotal ?? result.unreadTotal ?? 0;
|
||||
return { conversations, unreadTotal };
|
||||
}
|
||||
|
||||
function emitConversationsUpdated(app, result) {
|
||||
const payload = normalizeConversationsPayload(result);
|
||||
updateTabBarBadge(app, payload.unreadTotal);
|
||||
app.emitEvent('conversationsUpdated', {
|
||||
content: payload,
|
||||
unreadTotal: payload.unreadTotal,
|
||||
raw: result,
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureConnection(app) {
|
||||
const uid = wx.getStorageSync('uid');
|
||||
if (!uid || !wx.goEasy) return;
|
||||
|
||||
const targetUserId = getLocalImUserId(app);
|
||||
const identityType = getLocalImIdentity(app);
|
||||
if (!targetUserId) return;
|
||||
|
||||
if (isConnected()) {
|
||||
const currentUserId = getCurrentUserId();
|
||||
if (currentUserId === targetUserId) {
|
||||
bindGlobalListenersIfNeeded(app, targetUserId);
|
||||
loadConversations(app);
|
||||
const now = Date.now();
|
||||
if (now - _lastGroupSubscribeAt > 60000) {
|
||||
_lastGroupSubscribeAt = now;
|
||||
await subscribeAllGroups(app);
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
wx.goEasy.disconnect();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
connectGoEasy(app, targetUserId, identityType);
|
||||
}
|
||||
|
||||
function connectGoEasy(app, userId, identityType) {
|
||||
const defAvatar = resolveAvatarUrl('', app);
|
||||
const tx = wx.getStorageSync('touxiang') || '';
|
||||
const uid = wx.getStorageSync('uid') || '';
|
||||
const nick = wx.getStorageSync('nicheng') || app.globalData.nicheng || app.globalData.dashouNicheng || '';
|
||||
const name = nick || ('用户' + (uid || '').substring(0, 6));
|
||||
const avatar = resolveAvatarUrl(tx, app) || defAvatar;
|
||||
|
||||
wx.goEasy.connect({
|
||||
id: userId,
|
||||
data: {
|
||||
name,
|
||||
avatar,
|
||||
identityType: identityType === 'normal' ? 'boss' : identityType,
|
||||
},
|
||||
onSuccess: async () => {
|
||||
try {
|
||||
wx.setStorageSync('savedGoEasyConnection', JSON.stringify({
|
||||
userId,
|
||||
identityType,
|
||||
lastConnectTime: Date.now(),
|
||||
status: 'connected',
|
||||
}));
|
||||
} catch (e) {}
|
||||
bindGlobalListenersIfNeeded(app, userId);
|
||||
_lastGroupSubscribeAt = Date.now();
|
||||
await subscribeAllGroups(app);
|
||||
loadConversations(app);
|
||||
},
|
||||
onFailed: (error) => {
|
||||
console.error('GoEasy连接失败', error);
|
||||
if (error.code === 408) {
|
||||
bindGlobalListenersIfNeeded(app, userId);
|
||||
loadConversations(app);
|
||||
} else {
|
||||
wx.showToast({ title: '连接失败,请重试', icon: 'none' });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function subscribeAllGroups(app) {
|
||||
return new Promise((resolve) => {
|
||||
if (!wx.goEasy || !wx.goEasy.im) return resolve();
|
||||
wx.goEasy.im.latestConversations({
|
||||
onSuccess: (result) => {
|
||||
const payload = normalizeConversationsPayload(result);
|
||||
const groupIds = payload.conversations
|
||||
.filter((c) => c.type === 'group' && c.groupId)
|
||||
.map((c) => c.groupId);
|
||||
if (groupIds.length && wx.goEasy.im.subscribeGroup) {
|
||||
wx.goEasy.im.subscribeGroup({
|
||||
groupIds,
|
||||
onSuccess: () => resolve(),
|
||||
onFailed: () => resolve(),
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
onFailed: () => resolve(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function registerGlobalListeners(app) {
|
||||
if (!wx.goEasy || !wx.goEasy.im) return;
|
||||
|
||||
if (_convListener) {
|
||||
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, _convListener);
|
||||
}
|
||||
if (_privateListener) {
|
||||
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, _privateListener);
|
||||
}
|
||||
if (_groupListener) {
|
||||
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, _groupListener);
|
||||
}
|
||||
|
||||
_convListener = (result) => {
|
||||
emitConversationsUpdated(app, result);
|
||||
};
|
||||
wx.goEasy.im.on(wx.GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, _convListener);
|
||||
|
||||
_privateListener = () => {
|
||||
loadConversations(app);
|
||||
};
|
||||
wx.goEasy.im.on(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, _privateListener);
|
||||
|
||||
_groupListener = () => {
|
||||
loadConversations(app);
|
||||
};
|
||||
wx.goEasy.im.on(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, _groupListener);
|
||||
}
|
||||
|
||||
function loadConversations(app) {
|
||||
if (!wx.goEasy || !wx.goEasy.im) return;
|
||||
if (!getCurrentUserId()) return;
|
||||
|
||||
wx.goEasy.im.latestConversations({
|
||||
onSuccess: (result) => {
|
||||
emitConversationsUpdated(app, result);
|
||||
subscribeAllGroups(app);
|
||||
},
|
||||
onFailed: (err) => {
|
||||
console.error('加载会话失败', err);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function updateTabBarBadge(app, unreadCount) {
|
||||
const badgeText = unreadCount > 0 ? (unreadCount > 99 ? '99+' : String(unreadCount)) : '';
|
||||
if (!app.globalData.messageManager) app.globalData.messageManager = {};
|
||||
app.globalData.messageManager.unreadTotal = unreadCount;
|
||||
app.globalData.messageManager.tabBarBadgeText = badgeText;
|
||||
app.emitEvent('tabBarBadgeChanged', { badgeText, showRedDot: unreadCount > 0 });
|
||||
|
||||
try {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length) {
|
||||
const page = pages[pages.length - 1];
|
||||
const tabBar = page.selectComponent('#custom-tab-bar');
|
||||
if (tabBar && tabBar.setData) tabBar.setData({ badgeText });
|
||||
if (page.getTabBar) {
|
||||
const tb = page.getTabBar();
|
||||
if (tb && tb.setData) tb.setData({ badgeText });
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
wx.setStorageSync('messageUnreadTotal', unreadCount);
|
||||
}
|
||||
|
||||
function registerNotification(app) {
|
||||
if (!wx.goEasy || !wx.goEasy.im) return;
|
||||
|
||||
if (_notificationHandler) {
|
||||
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, _notificationHandler);
|
||||
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, _notificationHandler);
|
||||
}
|
||||
|
||||
_notificationHandler = (message) => {
|
||||
if (app.globalData.messageManager && app.globalData.messageManager.notificationMuted) return;
|
||||
if (!shouldShowNotification(message)) return;
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length) {
|
||||
const curPage = pages[pages.length - 1];
|
||||
const route = curPage.route || '';
|
||||
if (route === 'pages/liaotian/liaotian' || route === 'pages/qunliaotian/qunliaotian') return;
|
||||
}
|
||||
const senderName = message.senderData?.name || (message.senderId || '用户');
|
||||
let content = '';
|
||||
if (message.type === 'text') content = message.payload?.text || '[文本]';
|
||||
else if (message.type === 'image') content = '[图片]';
|
||||
else if (message.type === 'audio') content = '[语音]';
|
||||
else content = '[新消息]';
|
||||
const avatar = resolveAvatarUrl(message.senderData?.avatar, app);
|
||||
|
||||
if (app.globalData.globalNotification && app.globalData.globalNotification.show) {
|
||||
app.globalData.globalNotification.show({
|
||||
senderName,
|
||||
content,
|
||||
avatar,
|
||||
message,
|
||||
notificationType: message.groupId ? 'group' : 'private',
|
||||
groupId: message.groupId,
|
||||
orderId: message.orderId,
|
||||
});
|
||||
} else {
|
||||
wx.showToast({ title: `${senderName}: ${content}`, icon: 'none', duration: 2000 });
|
||||
}
|
||||
};
|
||||
wx.goEasy.im.on(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, _notificationHandler);
|
||||
wx.goEasy.im.on(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, _notificationHandler);
|
||||
}
|
||||
|
||||
function detachGlobalListeners() {
|
||||
if (!wx.goEasy || !wx.goEasy.im) return;
|
||||
if (_convListener) {
|
||||
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, _convListener);
|
||||
_convListener = null;
|
||||
}
|
||||
if (_privateListener) {
|
||||
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, _privateListener);
|
||||
_privateListener = null;
|
||||
}
|
||||
if (_groupListener) {
|
||||
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, _groupListener);
|
||||
_groupListener = null;
|
||||
}
|
||||
if (_notificationHandler) {
|
||||
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, _notificationHandler);
|
||||
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, _notificationHandler);
|
||||
_notificationHandler = null;
|
||||
}
|
||||
_listenersBoundForUser = null;
|
||||
}
|
||||
|
||||
function disconnectGoEasy(app) {
|
||||
stopHeartbeat(app);
|
||||
detachGlobalListeners();
|
||||
if (wx.goEasy && wx.goEasy.disconnect) {
|
||||
wx.goEasy.disconnect({ onSuccess: () => {}, onFailed: () => {} });
|
||||
}
|
||||
wx.removeStorageSync('savedGoEasyConnection');
|
||||
}
|
||||
|
||||
async function switchRoleAndReconnect(app, newRole) {
|
||||
disconnectGoEasy(app);
|
||||
app.globalData.currentRole = newRole;
|
||||
wx.setStorageSync('currentRole', newRole);
|
||||
if (app.globalData.primaryRole !== newRole && ['normal', 'dashou', 'shangjia'].includes(newRole)) {
|
||||
app.globalData.primaryRole = newRole;
|
||||
wx.setStorageSync('primaryRole', newRole);
|
||||
}
|
||||
startHeartbeat(app);
|
||||
await ensureConnection(app);
|
||||
return true;
|
||||
}
|
||||
|
||||
function startHeartbeat(app) {
|
||||
stopHeartbeat(app);
|
||||
_heartbeatTimer = setInterval(() => {
|
||||
if (!wx.getStorageSync('uid')) return;
|
||||
if (isConnected()) {
|
||||
loadConversations(app);
|
||||
} else {
|
||||
ensureConnection(app);
|
||||
}
|
||||
}, 45000);
|
||||
}
|
||||
|
||||
function stopHeartbeat(app) {
|
||||
if (_heartbeatTimer) {
|
||||
clearInterval(_heartbeatTimer);
|
||||
_heartbeatTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function initAppStateListener(app) {
|
||||
wx.onAppShow(() => {
|
||||
setTimeout(() => {
|
||||
if (!wx.getStorageSync('uid')) return;
|
||||
if (!isConnected()) {
|
||||
ensureConnection(app);
|
||||
} else {
|
||||
loadConversations(app);
|
||||
setTimeout(() => {
|
||||
const unread = (app.globalData.messageManager && app.globalData.messageManager.unreadTotal) || 0;
|
||||
updateTabBarBadge(app, unread);
|
||||
}, 300);
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
function initNetworkListener(app) {
|
||||
wx.onNetworkStatusChange((res) => {
|
||||
if (res.isConnected) {
|
||||
setTimeout(() => ensureConnection(app), 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { initGlobalMessageSystem };
|
||||
Reference in New Issue
Block a user