| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570 |
- import TIM from 'tim-wx-sdk';
- class TIMManager {
- constructor() {
- this.tim = null;
- this.isLogin = false;
- this.userId = null;
- this.messageCallbacks = [];
- this.messageReadCallbacks = [];
- // 新增:缓存 store 实例,避免重复获取
- this.store = null;
- }
- /**
- * 初始化 TIM SDK
- * @param {Number} sdkAppID - 腾讯云 IM 的 SDKAppID
- */
- init(sdkAppID) {
- // 创建 SDK 实例
- this.tim = TIM.create({
- SDKAppID: sdkAppID
- });
- // 设置日志级别(开发时设为 0,生产设为 1)
- this.tim.setLogLevel(0);
- // 注册监听事件
- this.registerEvents();
- // 初始化时提前尝试获取 store(提升后续获取成功率)
- this.initStore();
- }
- /**
- * 初始化 store 实例(核心修复:提前获取+缓存)
- */
- initStore() {
- try {
- // 方式1:从全局 App 实例获取
- const app = getApp();
- if (app && app.$store) {
- this.store = app.$store;
- console.log('✅ 从全局 App 获取 store 成功');
- return;
- }
- // 方式2:直接导入 store(备用方案,注意路径正确性)
- const storeModule = require('../store/index.js');
- if (storeModule && storeModule.default) {
- this.store = storeModule.default;
- console.log('✅ 直接导入 store 成功');
- return;
- }
- console.warn('⚠️ 初始化时未获取到 store,将在需要时重试');
- } catch (error) {
- console.warn('⚠️ 初始化 store 失败:', error.message);
- }
- }
- /**
- * 安全获取 store 实例(带重试逻辑)
- * @returns {Promise<Object>} store 实例
- */
- async getStoreSafely() {
- // 缓存中有则直接返回
- if (this.store) {
- return this.store;
- }
- // 无缓存时,重试获取(最多等 3 秒,每 100ms 检查一次)
- return new Promise((resolve, reject) => {
- let retryCount = 0;
- const maxRetry = 30; // 30 * 100ms = 3 秒
- const timer = setInterval(() => {
- retryCount++;
- // 尝试从全局 App 获取
- const app = getApp();
- if (app && app.$store) {
- clearInterval(timer);
- this.store = app.$store;
- resolve(this.store);
- return;
- }
- // 尝试直接导入
- try {
- const storeModule = require('../store/index.js');
- if (storeModule && storeModule.default) {
- clearInterval(timer);
- this.store = storeModule.default;
- resolve(this.store);
- return;
- }
- } catch (e) {}
- // 超过重试次数
- if (retryCount >= maxRetry) {
- clearInterval(timer);
- reject(new Error('获取 store 超时(3秒)'));
- }
- }, 100);
- });
- }
- /**
- * 注册事件监听
- */
- registerEvents() {
- // SDK 进入 ready 状态
- this.tim.on(TIM.EVENT.SDK_READY, this.onSdkReady.bind(this));
-
- // SDK 未 ready
- this.tim.on(TIM.EVENT.SDK_NOT_READY, this.onSdkNotReady.bind(this));
-
- // 收到新消息
- this.tim.on(TIM.EVENT.MESSAGE_RECEIVED, this.onMessageReceived.bind(this));
-
- // 会话列表更新(核心修复)
- this.tim.on(TIM.EVENT.CONVERSATION_LIST_UPDATED, this.onConversationListUpdated.bind(this));
-
- // 消息已读回执(对方已读我发送的消息)
- this.tim.on(TIM.EVENT.MESSAGE_READ_BY_PEER, this.onMessageReadByPeer.bind(this));
-
- // 被踢下线
- this.tim.on(TIM.EVENT.KICKED_OUT, this.onKickedOut.bind(this));
-
- // 网络状态变化
- this.tim.on(TIM.EVENT.NET_STATE_CHANGE, this.onNetStateChange.bind(this));
- }
- /**
- * SDK Ready
- */
- onSdkReady() {
- this.isLogin = true;
- }
- /**
- * SDK Not Ready
- */
- onSdkNotReady() {
- this.isLogin = false;
- }
- /**
- * 收到新消息
- */
- onMessageReceived(event) {
- const messageList = event.data;
-
- // 同步接收到的消息到MySQL(双重保障)
- messageList.forEach(message => {
- this.syncReceivedMessageToMySQL(message);
- });
-
- // 触发回调
- this.messageCallbacks.forEach(callback => {
- callback(messageList);
- });
- }
-
- /**
- * 同步接收到的消息到MySQL
- */
- async syncReceivedMessageToMySQL(timMessage) {
- try {
- // 获取消息类型
- const getMessageType = (msg) => {
- const typeMap = {
- 'TIMTextElem': 1,
- 'TIMImageElem': 2,
- 'TIMSoundElem': 3,
- 'TIMVideoFileElem': 4,
- 'TIMFileElem': 5
- };
-
- // 处理自定义消息(我们的语音消息)
- if (msg.type === 'TIMCustomElem' && msg.payload) {
- try {
- const customData = JSON.parse(msg.payload.data);
- if (customData.type === 'voice') {
- return 3; // 语音消息
- }
- } catch (e) {
- console.error('解析自定义消息类型失败:', e);
- }
- }
-
- return typeMap[msg.type] || 1;
- };
-
- // 获取消息内容
- const getMessageContent = (msg) => {
- switch (msg.type) {
- case 'TIMTextElem':
- return msg.payload.text || '';
- case 'TIMImageElem':
- return '[图片]';
- case 'TIMSoundElem':
- return '[语音]';
- case 'TIMVideoFileElem':
- return '[视频]';
- case 'TIMFileElem':
- return '[文件]';
- case 'TIMCustomElem':
- // 处理自定义消息
- try {
- const customData = JSON.parse(msg.payload.data);
- if (customData.type === 'voice') {
- return '[语音]';
- }
- } catch (e) {
- console.error('解析自定义消息内容失败:', e);
- }
- return '[自定义消息]';
- default:
- return '[未知消息]';
- }
- };
-
- // 使用V2接口,支持红娘消息(TIM用户ID可能是 m_xxx 格式)
- const syncData = {
- messageId: timMessage.ID,
- fromTimUserId: String(timMessage.from), // 保持字符串格式,支持 m_xxx
- toTimUserId: String(timMessage.to), // 保持字符串格式,支持 m_xxx
- messageType: getMessageType(timMessage),
- content: getMessageContent(timMessage),
- sendTime: timMessage.time
- };
-
- // 如果是语音消息,添加语音信息
- if (timMessage.type === 'TIMSoundElem' && timMessage.payload) {
- syncData.mediaUrl = timMessage.payload.url || timMessage.payload.remoteAudioUrl;
- syncData.duration = timMessage.payload.second || 0;
- syncData.mediaSize = timMessage.payload.size || 0;
- }
-
- // 如果是自定义消息(我们的语音消息)
- if (timMessage.type === 'TIMCustomElem' && timMessage.payload) {
- try {
- const customData = JSON.parse(timMessage.payload.data);
- if (customData.type === 'voice') {
- syncData.mediaUrl = customData.url;
- syncData.duration = customData.duration;
- syncData.mediaSize = customData.size || 0;
- }
- } catch (e) {
- console.error('解析自定义消息失败:', e);
- }
- }
-
- // 如果是图片消息,添加图片信息
- if (timMessage.type === 'TIMImageElem' && timMessage.payload.imageInfoArray) {
- const imageInfo = timMessage.payload.imageInfoArray[0];
- syncData.mediaUrl = imageInfo.imageUrl;
- syncData.thumbnailUrl = imageInfo.imageUrl;
- }
-
- // 调用后端同步接口(V2版本,支持红娘消息)
- const res = await uni.request({
- url: 'https://api.zhongruanke.cn/api/chat/syncTIMMessageV2',
- method: 'POST',
- data: syncData,
- header: {
- 'Content-Type': 'application/json'
- }
- });
-
- if (res[1].data.code === 200) {
- const msgType = res[1].data.type === 'matchmaker' ? '红娘消息' : '用户消息';
- }
- } catch (error) {
- console.error('❌ 同步接收消息失败:', error);
- // 同步失败不影响主流程
- }
- }
- /**
- * 消息已读回执(对方已读我发送的消息)
- */
- onMessageReadByPeer(event) {
- // 触发所有已读回执回调
- this.messageReadCallbacks.forEach(callback => {
- try {
- callback(event);
- } catch (error) {
- console.error('❌ 已读回执回调执行失败:', error);
- }
- });
- }
- /**
- * 会话列表更新(核心修复:异步安全获取 store)
- */
- async onConversationListUpdated(event) {
- // 计算总未读数
- const conversationList = event.data;
- let totalUnread = 0;
-
- conversationList.forEach(conversation => {
- totalUnread += conversation.unreadCount || 0;
- });
-
- try {
- // 核心修复:异步安全获取 store
- const store = await this.getStoreSafely();
- if (store) {
- await store.dispatch('updateTotalUnread', totalUnread);
- console.log('✅ 全局未读数更新成功:', totalUnread);
- }
-
- // 兜底方案:触发自定义事件,确保页面能收到更新
- uni.$emit('conversationUnreadUpdate', totalUnread);
- } catch (error) {
- console.error('❌ 更新全局未读数失败:', error.message);
- // 即使失败,仍触发自定义事件
- uni.$emit('conversationUnreadUpdate', totalUnread);
- }
- }
- /**
- * 被踢下线
- */
- onKickedOut(event) {
- uni.showModal({
- title: '下线通知',
- content: '您的账号在其他设备登录',
- showCancel: false
- });
- }
- /**
- * 网络状态变化
- */
- onNetStateChange(event) {}
- /**
- * 登录
- */
- async login(userID, userSig) {
- try {
- const res = await this.tim.login({
- userID: String(userID),
- userSig: userSig
- });
-
- this.userId = userID;
- return res;
- } catch (error) {
- console.error('❌ TIM 登录失败:', error);
- throw error;
- }
- }
- /**
- * 登出
- */
- async logout() {
- try {
- await this.tim.logout();
-
- this.isLogin = false;
- this.userId = null;
- } catch (error) {
- console.error('❌ TIM 登出失败:', error);
- }
- }
- /**
- * 发送文本消息
- */
- async sendTextMessage(toUserId, text) {
- try {
- // 验证参数
- if (!toUserId || toUserId === 'undefined' || toUserId === 'null') {
- console.error('❌ 接收者ID无效:', toUserId);
- throw new Error('接收者ID无效: ' + toUserId);
- }
-
- if (!this.userId || this.userId === 'undefined' || this.userId === 'null') {
- console.error('❌ 发送者ID无效:', this.userId);
- throw new Error('发送者ID未登录或无效');
- }
-
- const toUserIdStr = String(toUserId);
-
- // 创建文本消息
- const message = this.tim.createTextMessage({
- to: toUserIdStr,
- conversationType: TIM.TYPES.CONV_C2C, // 单聊
- payload: {
- text: text
- }
- });
- // 发送消息
- const res = await this.tim.sendMessage(message);
-
- return res.data.message;
- } catch (error) {
- console.error(' - 错误代码:', error.code || 'N/A');
- throw error;
- }
- }
- /**
- * 发送图片消息
- */
- async sendImageMessage(toUserId, filePath) {
- try {
- const message = this.tim.createImageMessage({
- to: String(toUserId),
- conversationType: TIM.TYPES.CONV_C2C,
- payload: {
- file: filePath
- },
- onProgress: (percent) => {}
- });
- const res = await this.tim.sendMessage(message);
-
- return res.data.message;
- } catch (error) {
- console.error('❌ 图片发送失败:', error);
- throw error;
- }
- }
- /**
- * 发送语音消息(通过自定义消息携带MinIO URL)
- * @param {String} toUserId 接收者ID
- * @param {String} voiceUrl MinIO语音文件URL
- * @param {Number} duration 语音时长(秒)
- * @param {Number} fileSize 文件大小(字节)
- */
- async sendVoiceMessage(toUserId, voiceUrl, duration, fileSize) {
- try {
- // 使用自定义消息发送语音信息
- const message = this.tim.createCustomMessage({
- to: String(toUserId),
- conversationType: TIM.TYPES.CONV_C2C,
- payload: {
- data: JSON.stringify({
- type: 'voice',
- url: voiceUrl,
- duration: duration,
- size: fileSize
- }),
- description: '[语音]',
- extension: voiceUrl
- }
- });
- const res = await this.tim.sendMessage(message);
-
- return res.data.message;
- } catch (error) {
- console.error('❌ 语音发送失败:', error);
- throw error;
- }
- }
- /**
- * 获取会话列表
- */
- async getConversationList() {
- try {
- const res = await this.tim.getConversationList();
-
- return res.data.conversationList;
- } catch (error) {
- console.error('❌ 获取会话列表失败:', error);
- throw error;
- }
- }
- /**
- * 获取聊天记录
- */
- async getMessageList(conversationID, count = 15) {
- try {
- const res = await this.tim.getMessageList({
- conversationID: conversationID,
- count: count
- });
-
- return res.data.messageList;
- } catch (error) {
- console.error('❌ 获取聊天记录失败:', error);
- throw error;
- }
- }
- /**
- * 将消息设为已读
- */
- async setMessageRead(conversationID) {
- try {
- await this.tim.setMessageRead({ conversationID });
- } catch (error) {
- console.error('❌ 设置已读失败:', error);
- }
- }
- /**
- * 撤回消息
- */
- async revokeMessage(message) {
- try {
- await this.tim.revokeMessage(message);
- } catch (error) {
- console.error('❌ 撤回失败:', error);
- throw error;
- }
- }
- /**
- * 监听消息
- */
- onMessage(callback) {
- this.messageCallbacks.push(callback);
- }
- /**
- * 移除监听
- */
- offMessage(callback) {
- const index = this.messageCallbacks.indexOf(callback);
- if (index > -1) {
- this.messageCallbacks.splice(index, 1);
- }
- }
- /**
- * 监听已读回执
- */
- onMessageRead(callback) {
- this.messageReadCallbacks.push(callback);
- }
- /**
- * 移除已读回执监听
- */
- offMessageRead(callback) {
- const index = this.messageReadCallbacks.indexOf(callback);
- if (index > -1) {
- this.messageReadCallbacks.splice(index, 1);
- }
- }
- /**
- * 获取 TIM 实例
- */
- getTim() {
- return this.tim;
- }
- /**
- * 获取当前登录的 IM 用户ID
- */
- getCurrentUserId() {
- return this.userId ? String(this.userId) : null;
- }
- }
- // 导出单例
- const timManager = new TIMManager();
- export default timManager;
|