ykt-wxapp/utils/tim-chat.js

2788 lines
82 KiB
JavaScript
Raw Normal View History

2026-01-20 13:21:50 +08:00
// 引入腾讯IM SDK
import TIM from 'tim-wx-sdk'
import TIMUploadPlugin from 'tim-upload-plugin'
2026-01-22 15:13:26 +08:00
import api from './api.js'
2026-01-20 13:21:50 +08:00
const env = __VITE_ENV__;
// 腾讯IM配置 - SDKAppID 必须是 number 类型,使用 Number() 转换
const TIM_CONFIG = {
2026-01-22 15:13:26 +08:00
SDKAppID: Number(env.MP_TIM_SDK_APP_ID), // 患者端 IM SDKAppID
}
// 验证配置
if (!TIM_CONFIG.SDKAppID || isNaN(TIM_CONFIG.SDKAppID)) {
console.error('❌ TIM SDK配置错误: MP_TIM_SDK_APP_ID未定义或无效', {
envValue: env.MP_TIM_SDK_APP_ID,
parsedValue: TIM_CONFIG.SDKAppID
})
2026-01-20 13:21:50 +08:00
}
// IM连接配置常量
const IM_CONNECTION_CONFIG = {
MAX_RECONNECT_ATTEMPTS: 10, // 最大重连次数
RECONNECT_DELAYS: [2000, 4000, 8000, 16000, 30000], // 重连延迟(指数退避)
LOGIN_COOLDOWN: 5000, // 登录冷却时间
SDK_READY_TIMEOUT: 15000, // SDK就绪超时时间
LOGIN_CHECK_INTERVAL_STABLE: 60000, // 稳定状态检查间隔
LOGIN_CHECK_INTERVAL_UNSTABLE: 15000, // 不稳定状态检查间隔
LOGIN_CHECK_FIRST_DELAY: 30000, // 首次检查延迟
HEARTBEAT_INTERVAL: 60000, // 心跳间隔毫秒60秒
HEARTBEAT_MAX_FAIL: 3, // 心跳最大失败次数
NETWORK_RECONNECT_DELAY: 2000, // 网络恢复后延迟重连
MESSAGE_BATCH_COUNT: 20, // 每批消息数量
MAX_MESSAGE_REQUESTS: 50, // 最大消息请求次数
MAX_CACHE_SIZE: 1000, // 最大缓存消息数
TIM_INSTANCE_READY_CHECK_INTERVAL: 100 // TIM实例就绪检查间隔
}
class TimChatManager {
constructor() {
// TIM实例和会话
this.tim = null
this.conversation = null
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 用户信息
this.currentUserID = ''
this.currentUserSig = ''
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 消息管理
this.messageList = []
this.currentConversationID = null
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 分页加载相关状态
this.nextReqMessageID = ""
this.isCompleted = false
this.isLoadingMore = false
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 状态标志
this.isLoading = false
this.isLoggedIn = false
this.isInitializing = false
this.isLoggingIn = false
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 定时器
this.loginCheckInterval = null
this.heartbeatInterval = null
this.networkReconnectTimer = null
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 重连管理
this.reconnectAttempts = 0
this.maxReconnectAttempts = IM_CONNECTION_CONFIG.MAX_RECONNECT_ATTEMPTS
this.reconnectDelays = IM_CONNECTION_CONFIG.RECONNECT_DELAYS
this.lastLoginTime = 0
this.loginCooldown = IM_CONNECTION_CONFIG.LOGIN_COOLDOWN
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 心跳管理
this.heartbeatFailCount = 0
// 回调函数
this.callbacks = {
onMessageReceived: null,
onMessageSent: null,
onSDKReady: null,
onSDKNotReady: null,
onError: null,
onLoginStatusChanged: null,
onConversationListUpdated: null
}
2026-01-22 15:13:26 +08:00
// 绑定事件处理函数
this.boundEventHandlers = {
onSDKReady: this.onSDKReady.bind(this),
onSDKNotReady: this.onSDKNotReady.bind(this),
onMessageReceived: this.onMessageReceived.bind(this),
onMessageSentSucceeded: this.onMessageSentSucceeded.bind(this),
onMessageSentFailed: this.onMessageSentFailed.bind(this),
onConversationListUpdated: this.onConversationListUpdated.bind(this),
onNetStateChange: this.onNetStateChange.bind(this),
onKickedOut: this.onKickedOut.bind(this)
}
2026-01-20 13:21:50 +08:00
}
// ============== 资源管理方法 ==============
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 清理所有定时器
clearAllTimers() {
if (this.loginCheckInterval) {
clearTimeout(this.loginCheckInterval)
clearInterval(this.loginCheckInterval)
this.loginCheckInterval = null
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval)
this.heartbeatInterval = null
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
if (this.networkReconnectTimer) {
clearTimeout(this.networkReconnectTimer)
this.networkReconnectTimer = null
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
console.log('所有定时器已清理')
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 清理缓存(已弃用缓存功能)
cleanupCache() {
// 缓存功能已移除
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 重置所有状态
resetAllStates() {
this.isLoggedIn = false
this.isLoggingIn = false
this.isInitializing = false
this.reconnectAttempts = 0
this.lastLoginTime = 0
this.heartbeatFailCount = 0
}
// ============== 初始化方法 ==============
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 初始化腾讯IM
async initTIM(userID = null) {
if (this.isInitializing) {
console.log('IM正在初始化中跳过重复初始化')
return false
}
this.isInitializing = true
console.log('=== 开始初始化IM ===')
try {
// 重置重连次数,允许重新登录
this.reconnectAttempts = 0
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 如果存在旧的TIM实例先完整清理
if (this.tim) {
console.log('检测到旧的TIM实例开始清理...')
await this.cleanupOldInstance()
}
if (!TIM) {
throw new Error('TIM SDK 未正确导入')
}
2026-01-22 15:13:26 +08:00
// 验证 SDKAppID
if (!TIM_CONFIG.SDKAppID || isNaN(TIM_CONFIG.SDKAppID)) {
throw new Error(`TIM SDK配置错误: SDKAppID无效 (${TIM_CONFIG.SDKAppID}),请检查环境变量 MP_TIM_SDK_APP_ID`)
}
2026-01-20 13:21:50 +08:00
2026-01-22 15:13:26 +08:00
console.log('创建TIM实例SDKAppID:', TIM_CONFIG.SDKAppID)
2026-01-20 13:21:50 +08:00
this.tim = TIM.create({ SDKAppID: TIM_CONFIG.SDKAppID })
// 等待TIM实例初始化完成
await this.waitForTIMInstanceReady()
// 注册上传插件
this.tim.registerPlugin({ "tim-upload-plugin": TIMUploadPlugin })
console.log('上传插件已注册')
// 注册事件监听器
this.registerEventListeners()
// 设置日志级别
if (typeof this.tim.setLogLevel === 'function') {
this.tim.setLogLevel(0)
}
// 获取用户信息并登录
await this.getUserInfoAndLogin(userID)
// 等待SDK Ready
console.log('等待SDK Ready...')
await this.waitForSDKReady(IM_CONNECTION_CONFIG.SDK_READY_TIMEOUT)
console.log('=== IM初始化完成 ===')
return true
} catch (error) {
console.error('=== IM初始化失败 ===', error)
this.triggerCallback('onError', `初始化失败: ${error.message || error}`)
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 初始化失败时清理资源
await this.cleanupOldInstance()
return false
} finally {
this.isInitializing = false
}
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 清理旧的TIM实例
async cleanupOldInstance() {
try {
// 清理所有定时器
this.clearAllTimers()
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 移除事件监听器
this.removeEventListeners()
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 登出
if (this.tim && this.tim.isLoggedIn) {
try {
await this.tim.logout()
console.log('已登出旧实例')
} catch (err) {
console.warn('登出旧实例失败:', err)
}
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 清理实例
this.tim = null
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 重置状态
this.resetAllStates()
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
console.log('旧实例清理完成')
} catch (error) {
console.error('清理旧实例时出错:', error)
}
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 等待TIM实例准备就绪
waitForTIMInstanceReady() {
return new Promise((resolve, reject) => {
const startTime = Date.now()
const timeout = 10000 // 10秒超时
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
const checkReady = () => {
if (this.tim && typeof this.tim.on === 'function') {
console.log('TIM实例已就绪')
resolve()
} else if (Date.now() - startTime > timeout) {
reject(new Error('等待TIM实例就绪超时'))
} else {
setTimeout(checkReady, IM_CONNECTION_CONFIG.TIM_INSTANCE_READY_CHECK_INTERVAL)
}
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
checkReady()
})
}
// 等待SDK Ready优化版更好的超时处理
waitForSDKReady(timeout = IM_CONNECTION_CONFIG.SDK_READY_TIMEOUT) {
return new Promise((resolve, reject) => {
const startTime = Date.now()
const checkInterval = 1000 // 每秒检查一次
let checkCount = 0
const checkSDKReady = () => {
checkCount++
const elapsed = Date.now() - startTime
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
if (this.isLoggedIn) {
console.log(`✓ SDK已Ready耗时${elapsed}ms检查${checkCount}次)`)
resolve()
} else if (elapsed > timeout) {
const error = new Error(`等待SDK Ready超时${timeout}ms`)
console.error('✗', error.message)
// 超时不算致命错误,尝试继续
resolve()
} else {
2026-01-22 15:13:26 +08:00
console.log(`等待SDK Ready... ${Math.floor(elapsed / 1000)}/${Math.floor(timeout / 1000)}`)
2026-01-20 13:21:50 +08:00
setTimeout(checkSDKReady, checkInterval)
}
}
// 立即开始第一次检查
checkSDKReady()
})
}
// 注册事件监听器
registerEventListeners() {
if (!this.tim || !TIM.EVENT) return
try {
// 检查TIM实例是否已经初始化完成
if (!this.tim || typeof this.tim.on !== 'function') {
console.log('TIM实例未准备好延迟注册事件监听器')
setTimeout(() => this.registerEventListeners(), 100)
return
}
2026-01-22 15:13:26 +08:00
// 逐个注册事件监听器,如果某个注册失败则继续注册其他的
const events = [
{ event: TIM.EVENT.SDK_READY, handler: this.boundEventHandlers.onSDKReady },
{ event: TIM.EVENT.SDK_NOT_READY, handler: this.boundEventHandlers.onSDKNotReady },
{ event: TIM.EVENT.MESSAGE_RECEIVED, handler: this.boundEventHandlers.onMessageReceived },
{ event: TIM.EVENT.MESSAGE_SENT_SUCCEEDED, handler: this.boundEventHandlers.onMessageSentSucceeded },
{ event: TIM.EVENT.MESSAGE_SENT_FAILED, handler: this.boundEventHandlers.onMessageSentFailed },
{ event: TIM.EVENT.CONVERSATION_LIST_UPDATED, handler: this.boundEventHandlers.onConversationListUpdated },
{ event: TIM.EVENT.NET_STATE_CHANGE, handler: this.boundEventHandlers.onNetStateChange },
{ event: TIM.EVENT.KICKED_OUT, handler: this.boundEventHandlers.onKickedOut }
]
2026-01-20 13:21:50 +08:00
events.forEach(({ event, handler }) => {
if (event && handler && typeof this.tim.on === 'function') {
try {
this.tim.on(event, handler)
} catch (error) {
console.error(`注册事件监听器失败 ${event}:`, error)
}
}
})
console.log('TIM事件监听器注册完成')
} catch (error) {
console.error('注册TIM事件监听器失败:', error)
}
}
// 移除事件监听器
removeEventListeners() {
if (!this.tim || !TIM.EVENT) return
try {
// 检查TIM实例是否还有off方法
if (typeof this.tim.off !== 'function') return
// 逐个移除事件监听器,如果某个移除失败则继续移除其他的
const events = [
{ event: TIM.EVENT.SDK_READY, handler: this.boundEventHandlers.onSDKReady },
{ event: TIM.EVENT.SDK_NOT_READY, handler: this.boundEventHandlers.onSDKNotReady },
{ event: TIM.EVENT.MESSAGE_RECEIVED, handler: this.boundEventHandlers.onMessageReceived },
{ event: TIM.EVENT.MESSAGE_SENT_SUCCEEDED, handler: this.boundEventHandlers.onMessageSentSucceeded },
{ event: TIM.EVENT.MESSAGE_SENT_FAILED, handler: this.boundEventHandlers.onMessageSentFailed },
{ event: TIM.EVENT.CONVERSATION_LIST_UPDATED, handler: this.boundEventHandlers.onConversationListUpdated },
{ event: TIM.EVENT.NET_STATE_CHANGE, handler: this.boundEventHandlers.onNetStateChange }
]
events.forEach(({ event, handler }) => {
if (event && handler && typeof this.tim.off === 'function') {
try {
this.tim.off(event, handler)
} catch (error) {
console.error(`移除事件监听器失败 ${event}:`, error)
}
}
})
} catch (error) {
console.error('移除TIM事件监听器失败:', error)
}
}
// 获取用户信息并登录
async getUserInfoAndLogin(userID) {
try {
if (userID) {
this.currentUserID = userID
uni.setStorageSync('userInfo', { userID })
} else {
const userInfo = uni.getStorageSync('userInfo')
if (!userInfo?.userID) {
throw new Error('未找到用户信息,请先登录')
}
this.currentUserID = userInfo.userID
}
this.currentUserSig = await this.getUserSig(this.currentUserID)
await this.loginTIM()
} catch (error) {
console.error('获取用户信息失败:', error)
this.triggerCallback('onError', `登录失败: ${error.message || error}`)
throw error // 重新抛出错误,让调用者知道登录失败
}
}
// 获取 userSig
async getUserSig(userID) {
try {
2026-01-22 15:13:26 +08:00
const response = await api('getUserSig', { userId: userID })
if (response?.success && response?.data) {
return response.data
2026-01-20 13:21:50 +08:00
}
throw new Error('获取 userSig 失败: 接口返回数据格式错误')
} catch (error) {
console.error('获取UserSig失败:', error)
throw error
}
}
// 登录腾讯IM
loginTIM() {
return new Promise((resolve, reject) => {
if (this.isLoggingIn) {
reject(new Error('正在登录中'))
return
}
const now = Date.now()
if (now - this.lastLoginTime < this.loginCooldown) {
reject(new Error('登录冷却中'))
return
}
if (this.isLoggedIn) {
resolve()
return
}
this.isLoggingIn = true
this.lastLoginTime = now
console.log('开始登录腾讯IMuserID:', this.currentUserID)
this.tim.login({
userID: this.currentUserID,
userSig: this.currentUserSig
}).then(() => {
2026-01-22 16:35:05 +08:00
console.log('腾讯IM登录请求成功等待SDK_READY事件...')
2026-01-20 13:21:50 +08:00
this.isLoggingIn = false
2026-01-22 16:35:05 +08:00
// 不在这里设置 isLoggedIn = true等待 onSDKReady 事件
2026-01-20 13:21:50 +08:00
this.reconnectAttempts = 0
2026-01-22 15:13:26 +08:00
2026-01-22 16:35:05 +08:00
// 触发登录状态变化回调(登录中状态)
2026-01-20 13:21:50 +08:00
this.triggerCallback('onLoginStatusChanged', {
2026-01-22 16:35:05 +08:00
isLoggedIn: false,
2026-01-20 13:21:50 +08:00
userID: this.currentUserID,
2026-01-22 16:35:05 +08:00
reason: 'LOGIN_REQUESTED',
message: '登录请求成功等待SDK就绪...'
2026-01-20 13:21:50 +08:00
})
resolve()
}).catch(error => {
console.error('腾讯IM登录失败:', error)
this.isLoggingIn = false
this.triggerCallback('onLoginStatusChanged', {
isLoggedIn: false,
error,
reason: 'LOGIN_FAILED'
})
reject(error)
})
})
}
// ============== 连接监控方法 ==============
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 启动登录状态检测(优化版:使用配置常量)
startLoginStatusCheck() {
this.stopLoginStatusCheck()
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 根据连接稳定性动态调整检查间隔
const getCheckInterval = () => {
if (this.reconnectAttempts > 0) {
return IM_CONNECTION_CONFIG.LOGIN_CHECK_INTERVAL_UNSTABLE
}
return IM_CONNECTION_CONFIG.LOGIN_CHECK_INTERVAL_STABLE
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
const checkStatus = () => {
// 如果已达到最大重连次数(被踢下线),停止检查
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.log('⚠️ 已被踢下线或达到最大重连次数,停止登录状态检查')
return
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 如果正在登录中,跳过本次检查
if (this.isLoggingIn) {
this.loginCheckInterval = setTimeout(checkStatus, getCheckInterval())
return
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
const isLoggedIn = this.checkLoginStatus()
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 如果未登录且有用户ID尝试重连
if (!isLoggedIn && this.currentUserID && !this.isLoggingIn) {
console.log('📡 登录状态检测:发现未登录,尝试重连')
this.attemptReconnect()
} else if (isLoggedIn) {
// 登录正常,重置重连计数
if (this.reconnectAttempts > 0) {
console.log('✓ 登录状态检测:连接已恢复正常')
this.reconnectAttempts = 0
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 定期清理缓存
this.cleanupCache()
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 重新调度下一次检查(再次检查,防止在检查过程中被踢下线)
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.loginCheckInterval = setTimeout(checkStatus, getCheckInterval())
}
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 首次检查延迟
const firstDelay = IM_CONNECTION_CONFIG.LOGIN_CHECK_FIRST_DELAY
this.loginCheckInterval = setTimeout(checkStatus, firstDelay)
2026-01-22 15:13:26 +08:00
console.log(`🔍 登录状态检测已启动,首次检查将在${firstDelay / 1000}秒后进行`)
2026-01-20 13:21:50 +08:00
}
// 停止登录状态检测
stopLoginStatusCheck() {
if (this.loginCheckInterval) {
clearTimeout(this.loginCheckInterval)
clearInterval(this.loginCheckInterval)
this.loginCheckInterval = null
}
}
// 检查登录状态
checkLoginStatus() {
if (!this.tim) return false
try {
// 【修复】优先信任已设置的登录状态,只有在明确检测到未登录时才更新
// 如果已经标记为登录状态,不应该被轻易覆盖
if (this.isLoggedIn) {
// 只在已登录状态下,通过 SDK 再次验证
let sdkLoginStatus = true // 默认保持登录状态
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
if (typeof this.tim.getLoginStatus === 'function') {
try {
const status = this.tim.getLoginStatus()
// 只有明确检测到未登录状态才更新
if (status === TIM.TYPES.NOT_LOGGED_IN || status === TIM.TYPES.KICKED_OUT) {
sdkLoginStatus = false
console.warn('SDK 检测到未登录状态:', status)
}
} catch (e) {
console.error('获取 SDK 登录状态失败:', e)
// 出错时保持当前状态
}
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 只有 SDK 明确返回未登录时才更新状态
if (!sdkLoginStatus) {
this.isLoggedIn = false
this.triggerCallback('onLoginStatusChanged', {
isLoggedIn: false,
userID: this.currentUserID,
reason: 'SDK_STATUS_CHECK'
})
return false
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 保持登录状态
return true
}
// 如果当前是未登录状态,检查 SDK 是否已登录
let loginStatus = false
if (typeof this.tim.isLoggedIn === 'boolean') {
loginStatus = this.tim.isLoggedIn
} else if (typeof this.tim.getLoginStatus === 'function') {
const status = this.tim.getLoginStatus()
loginStatus = status === TIM.TYPES.LOGINED || status === TIM.TYPES.LOGGING_IN
}
// 只有在检测到登录状态变化时才更新
if (loginStatus && !this.isLoggedIn) {
this.isLoggedIn = true
this.triggerCallback('onLoginStatusChanged', {
isLoggedIn: true,
userID: this.currentUserID,
reason: 'SDK_STATUS_CHECK'
})
}
return loginStatus
} catch (error) {
console.error('检查IM登录状态失败:', error)
// 出错时返回当前状态,不改变状态
return this.isLoggedIn
}
}
// 尝试重新连接(优化版:更智能的重连策略)
async attemptReconnect() {
// 如果已达到最大重连次数(包括被踢下线的情况),停止重连
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.log('⚠️ 已达到最大重连次数或被踢下线,停止自动重连')
return false
}
if (this.isLoggingIn) {
console.log('正在登录中,跳过重连')
return false
}
// 先检查是否真的需要重连
if (this.tim && this.isLoggedIn) {
console.log('IM已登录无需重连')
this.reconnectAttempts = 0
return true
}
const now = Date.now()
const delayIndex = Math.min(this.reconnectAttempts, this.reconnectDelays.length - 1)
const reconnectDelay = this.reconnectDelays[delayIndex]
const timeSinceLastLogin = now - this.lastLoginTime
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 只有非首次重连才需要检查冷却时间
if (this.reconnectAttempts > 0 && timeSinceLastLogin < reconnectDelay) {
const remainingTime = reconnectDelay - timeSinceLastLogin
console.log(`重连冷却中,剩余时间:${Math.ceil(remainingTime / 1000)}`)
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 安排下次重连
setTimeout(() => {
if (!this.isLoggedIn && !this.isLoggingIn && this.reconnectAttempts < this.maxReconnectAttempts) {
this.attemptReconnect()
}
}, remainingTime)
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
return false
}
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('达到最大重连次数,停止自动重连')
this.triggerCallback('onError', 'IM连接失败请检查网络或手动刷新页面')
return false
}
this.reconnectAttempts++
console.log(`${this.reconnectAttempts}次重连尝试(最多${this.maxReconnectAttempts}次)`)
try {
if (this.tim && !this.isLoggedIn) {
console.log('TIM实例存在但未登录尝试登录...')
await this.loginTIM()
console.log('重连成功!')
this.reconnectAttempts = 0 // 重置重连次数
return true
}
if (!this.tim) {
console.log('TIM实例不存在重新初始化...')
await this.initTIM(this.currentUserID)
console.log('重新初始化并登录成功!')
this.reconnectAttempts = 0
return true
}
return false
} catch (error) {
console.error(`${this.reconnectAttempts}次重连失败:`, error)
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 如果还有重连机会,自动安排下一次重连
if (this.reconnectAttempts < this.maxReconnectAttempts) {
const nextDelay = this.reconnectDelays[Math.min(this.reconnectAttempts, this.reconnectDelays.length - 1)]
console.log(`${nextDelay / 1000}秒后进行第${this.reconnectAttempts + 1}次重连...`)
setTimeout(() => {
if (!this.isLoggedIn && !this.isLoggingIn && this.reconnectAttempts < this.maxReconnectAttempts) {
this.attemptReconnect()
}
}, nextDelay)
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
return false
}
}
// SDK Ready 事件
onSDKReady() {
console.log('腾讯IM SDK Ready')
this.isLoggedIn = true
this.triggerCallback('onSDKReady')
this.triggerCallback('onLoginStatusChanged', {
isLoggedIn: true,
reason: 'SDK_READY'
})
this.startLoginStatusCheck()
this.startHeartbeat() // 启动心跳检测
}
// SDK Not Ready 事件
onSDKNotReady() {
console.log('腾讯IM SDK Not Ready')
this.isLoggedIn = false
this.triggerCallback('onSDKNotReady')
this.triggerCallback('onLoginStatusChanged', {
isLoggedIn: false,
reason: 'SDK_NOT_READY'
})
this.stopLoginStatusCheck()
this.stopHeartbeat() // 停止心跳检测
}
// 收到新消息
onMessageReceived(event) {
event.data.forEach(message => {
const existingMessage = this.messageList.find(msg => msg.ID === message.ID)
if (existingMessage) return
if (!this.filterMessage(message)) return
const convertedMessage = this.convertMessageFormat(message)
if (!convertedMessage.conversationID) {
convertedMessage.conversationID = message.conversationID
}
// 获取消息所属的会话ID
const messageConversationID = convertedMessage.conversationID
console.log('收到新消息:', {
messageID: convertedMessage.ID,
messageConversationID: messageConversationID,
currentConversationID: this.currentConversationID,
messageType: convertedMessage.type,
from: convertedMessage.from
})
2026-01-22 15:13:26 +08:00
// 判断是否为当前会话的消息必须有currentConversationID且匹配才显示
const isCurrentConversation = this.currentConversationID &&
messageConversationID === this.currentConversationID
console.log('消息会话匹配检查:', {
isCurrentConversation,
hasCurrentConversationID: !!this.currentConversationID,
conversationIDMatch: messageConversationID === this.currentConversationID
})
if (isCurrentConversation) {
// 当前会话的消息,触发回调
console.log('✓ 消息属于当前会话,触发显示')
this.triggerCallback('onMessageReceived', convertedMessage)
2026-01-20 13:21:50 +08:00
// 处理已读状态
if (this.currentConversationID) {
this.markConversationAsRead(this.currentConversationID)
}
this.triggerCallback('onConversationListUpdated', {
reason: 'NEW_MESSAGE_RECEIVED_IN_CURRENT_CONVERSATION',
conversation: {
conversationID: messageConversationID || this.currentConversationID,
lastMessage: this.formatLastMessage(convertedMessage),
lastMessageTime: convertedMessage.lastTime,
unreadCount: 0,
messageType: convertedMessage.type,
messageFlow: convertedMessage.flow
},
message: convertedMessage
})
} else {
// 非当前会话的消息,只更新会话列表
console.log('✗ 消息不属于当前会话,已过滤不显示,仅更新会话列表', {
messageConversationID,
currentConversationID: this.currentConversationID
})
this.updateConversationListOnNewMessage(convertedMessage)
}
})
}
// 消息发送成功
onMessageSentSucceeded(event) {
const messageID = event.data.ID
const sentMessage = event.data.message
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 更新messageList中的消息状态
const message = this.messageList.find(msg => msg.ID === messageID)
if (message) {
message.status = 'success'
// 如果服务器返回了真实的消息ID更新它
if (sentMessage && sentMessage.ID && sentMessage.ID !== messageID) {
message.ID = sentMessage.ID
}
}
// 缓存功能已移除
this.triggerCallback('onMessageSent', {
messageID,
status: 'success'
})
}
// 消息发送失败
onMessageSentFailed(event) {
const messageID = event.data.ID
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 更新messageList中的消息状态
const message = this.messageList.find(msg => msg.ID === messageID)
if (message) {
message.status = 'failed'
}
// 缓存功能已移除
this.triggerCallback('onMessageSent', {
messageID,
status: 'failed'
})
this.triggerCallback('onError', `发送失败: ${event.data.error || '未知错误'}`)
}
// 会话列表更新
onConversationListUpdated(event) {
this.triggerCallback('onConversationListUpdated', event.data)
}
// 网络状态变化(优化版:更稳定的处理)
onNetStateChange(event) {
const netState = event.data.netState
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
console.log('🌐 网络状态变化:', netState)
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 清理之前的网络重连定时器
if (this.networkReconnectTimer) {
clearTimeout(this.networkReconnectTimer)
this.networkReconnectTimer = null
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
if (netState === TIM.TYPES.NET_STATE_CONNECTED) {
console.log('✓ 网络已连接延迟检查IM状态以确保稳定')
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 网络恢复后延迟再检查,避免网络还不稳定时立即重连
const delay = IM_CONNECTION_CONFIG.NETWORK_RECONNECT_DELAY
this.networkReconnectTimer = setTimeout(() => {
if (this.tim && !this.isLoggedIn && !this.isLoggingIn) {
console.log('🔄 网络已稳定,开始重连')
this.ensureIMConnection()
} else if (this.isLoggedIn) {
console.log('✓ 网络已稳定IM连接正常')
}
this.networkReconnectTimer = null
}, delay)
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 重置重连次数(网络恢复后给更多机会)
if (this.reconnectAttempts > 0) {
console.log(`重置重连次数(之前: ${this.reconnectAttempts}`)
this.reconnectAttempts = 0
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
} else if (netState === TIM.TYPES.NET_STATE_CONNECTING) {
console.log('🔗 网络连接中,等待稳定...')
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
} else if (netState === TIM.TYPES.NET_STATE_DISCONNECTED) {
console.log('⚠️ 网络断开(暂不标记为未登录,等待心跳检测判断)')
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 网络断开时不立即设置 isLoggedIn = false
// 因为可能只是短暂的网络波动,让心跳检测来判断是否真的断线
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
this.triggerCallback('onLoginStatusChanged', {
isLoggedIn: this.isLoggedIn,
reason: 'NETWORK_DISCONNECTED',
message: '网络波动,将自动重连'
})
}
}
// 被踢下线处理
onKickedOut(event) {
console.log('⚠️ 账号被踢下线:', event.data)
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 更新登录状态
this.isLoggedIn = false
this.isLoggingIn = false
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 停止所有定时器(防止自动重连)
this.clearAllTimers()
this.stopLoginStatusCheck()
this.stopHeartbeat()
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 清除重连相关状态,阻止重连
this.reconnectAttempts = this.maxReconnectAttempts // 设置为最大值,阻止重连
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 触发登录状态变化回调
this.triggerCallback('onLoginStatusChanged', {
isLoggedIn: false,
reason: 'KICKED_OUT',
message: '您的账号在其他设备登录'
})
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 显示提示弹框
uni.showModal({
title: '提示',
content: '您的账号在其他设备登录,您已被踢下线',
showCancel: false,
confirmText: '确定',
success: () => {
// 清理本地缓存并跳转到登录页
uni.removeStorageSync('token')
uni.removeStorageSync('refreshToken')
uni.removeStorageSync('account')
uni.removeStorageSync('openid')
uni.reLaunch({
2026-01-22 15:13:26 +08:00
url: '/pages-center/login/login'
2026-01-20 13:21:50 +08:00
})
}
})
}
// 手动检测并重连IM
async ensureIMConnection() {
if (this.isLoggingIn || !this.tim) return true
const isLoggedIn = this.checkLoginStatus()
if (!isLoggedIn && this.currentUserID) {
return await this.attemptReconnect()
}
return true
}
startHeartbeat() {
this.stopHeartbeat()
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 心跳失败计数器
this.heartbeatFailCount = 0
const MAX_HEARTBEAT_FAIL = IM_CONNECTION_CONFIG.HEARTBEAT_MAX_FAIL
const INTERVAL = IM_CONNECTION_CONFIG.HEARTBEAT_INTERVAL
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 定时心跳检测
this.heartbeatInterval = setInterval(() => {
// 如果已达到最大重连次数(被踢下线),停止心跳检测
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.log('⚠️ 已被踢下线或达到最大重连次数,停止心跳检测')
this.stopHeartbeat()
return
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 只在已登录状态下进行心跳检测
if (!this.tim || !this.isLoggedIn) {
console.log('⏸ 心跳检测:未登录,跳过检测')
return
}
2026-01-22 15:13:26 +08:00
2026-01-22 16:35:05 +08:00
// 确保方法存在
if (typeof this.tim.getConversationList !== 'function') {
console.log('⏸ 心跳检测SDK方法不可用跳过检测')
return
}
2026-01-20 13:21:50 +08:00
this.tim.getConversationList()
.then(() => {
if (this.heartbeatFailCount > 0) {
2026-01-22 16:35:05 +08:00
console.log(`<EFBFBD> 心跳恢复正常(之前失败${this.heartbeatFailCount}次)`)
2026-01-20 13:21:50 +08:00
}
this.heartbeatFailCount = 0 // 重置失败计数
})
.catch((error) => {
2026-01-22 16:35:05 +08:00
// 如果是SDK未就绪错误不计入失败次数这是临时状态
if (error && error.message && error.message.includes('sdk not ready')) {
console.log('⏸ 心跳检测SDK未就绪跳过本次检测')
return
}
2026-01-20 13:21:50 +08:00
this.heartbeatFailCount++
console.error(`💔 心跳失败 (${this.heartbeatFailCount}/${MAX_HEARTBEAT_FAIL}):`, error.message)
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 只有连续失败多次才认为真的断线
if (this.heartbeatFailCount >= MAX_HEARTBEAT_FAIL) {
console.log('❌ 心跳连续失败,标记为未登录并尝试重连')
this.isLoggedIn = false
this.heartbeatFailCount = 0
// 只有未被踢下线才尝试重连
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.attemptReconnect()
}
}
})
}, INTERVAL)
2026-01-22 15:13:26 +08:00
console.log(`💓 心跳检测已启动(间隔${INTERVAL / 1000}秒,最多失败${MAX_HEARTBEAT_FAIL}次)`)
2026-01-20 13:21:50 +08:00
}
// 停止心跳检测
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval)
this.heartbeatInterval = null
console.log('IM心跳检测已停止')
}
}
// 获取会话列表
getConversationList() {
if (!this.tim) {
console.error('TIM实例不存在无法获取会话列表')
return
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
if (!this.isLoggedIn) {
console.log('SDK未ready等待SDK初始化后再获取会话列表...')
// 等待SDK就绪
const checkSDKReady = () => {
if (this.isLoggedIn) {
console.log('SDK已ready开始获取会话列表')
this.getConversationList()
} else {
setTimeout(checkSDKReady, 500)
}
}
setTimeout(checkSDKReady, 500)
return
}
}
// 获取群聊列表
getGroupList() {
return new Promise((resolve, reject) => {
if (!this.tim) {
reject(new Error('TIM实例不存在'))
return
}
if (!this.isLoggedIn) {
console.log('SDK未ready等待SDK初始化...')
2026-01-22 15:13:26 +08:00
let waitTime = 0
const maxWaitTime = 30000 // 最多等待30秒
const checkInterval = 1000 // 每秒检查一次
2026-01-22 16:35:05 +08:00
let timeoutHandle = null
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
const checkSDKReady = () => {
if (this.isLoggedIn) {
console.log('SDK已ready开始获取群聊列表')
2026-01-22 16:35:05 +08:00
if (timeoutHandle) clearTimeout(timeoutHandle)
2026-01-20 13:21:50 +08:00
this.getGroupListInternal().then(resolve).catch(reject)
2026-01-22 15:13:26 +08:00
} else if (waitTime >= maxWaitTime) {
console.error('等待SDK就绪超时')
2026-01-22 16:35:05 +08:00
if (timeoutHandle) clearTimeout(timeoutHandle)
2026-01-22 15:13:26 +08:00
reject(new Error('SDK初始化超时请检查网络连接'))
2026-01-20 13:21:50 +08:00
} else {
2026-01-22 15:13:26 +08:00
waitTime += checkInterval
console.log(`等待SDK就绪... (${Math.floor(waitTime / 1000)}/${Math.floor(maxWaitTime / 1000)}秒)`)
2026-01-22 16:35:05 +08:00
timeoutHandle = setTimeout(checkSDKReady, checkInterval)
2026-01-20 13:21:50 +08:00
}
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
checkSDKReady()
return
}
this.getGroupListInternal().then(resolve).catch(reject)
})
}
// 内部获取群聊列表方法
getGroupListInternal() {
return new Promise((resolve, reject) => {
console.log('开始获取群聊列表')
2026-01-22 16:35:05 +08:00
// 直接调用SDK就绪检查已在getGroupList()中完成
this.tim.getConversationList()
.then(async (conversationResponse) => {
console.log('获取会话列表成功')
const groupConversations = conversationResponse.data.conversationList.filter(conversation => {
return conversation.conversationID && conversation.conversationID.startsWith('GROUP')
})
2026-01-22 15:13:26 +08:00
2026-01-22 16:35:05 +08:00
console.log('群聊会话列表数量:', groupConversations.length)
2026-01-20 13:21:50 +08:00
2026-01-22 16:35:05 +08:00
// 先获取一次群组列表,避免在循环中重复调用
let allGroups = []
try {
const groupListResponse = await this.tim.getGroupList()
allGroups = groupListResponse.data.groupList || []
} catch (error) {
console.error('获取群组列表失败:', error)
}
2026-01-20 13:21:50 +08:00
2026-01-22 16:35:05 +08:00
const groupsWithInfo = await Promise.all(
groupConversations.map(async (conversation) => {
2026-01-20 13:21:50 +08:00
try {
2026-01-22 16:35:05 +08:00
const groupName = conversation.groupProfile?.name || ''
const [doctorId, patientName] = groupName.split('|')
const groupID = conversation.conversationID.replace('GROUP', '')
// 从已获取的群组列表中查找
const group = allGroups.find(g => g.groupID === groupID)
const groupInfo = {
groupID: groupID,
name: group?.name || '问诊群聊',
avatar: '/static/home/avatar.svg',
memberCount: group?.memberCount || 0
2026-01-20 13:21:50 +08:00
}
2026-01-22 16:35:05 +08:00
const lastMessage = conversation.lastMessage
let lastMessageText = '暂无消息'
let lastMessageTime = Date.now()
if (lastMessage) {
if (lastMessage.type === 'TIMTextElem') {
lastMessageText = lastMessage.payload.text
} else if (lastMessage.type === 'TIMImageElem') {
lastMessageText = '[图片]'
} else if (lastMessage.type === 'TIMSoundElem') {
lastMessageText = '[语音]'
} else if (lastMessage.type === 'TIMCustomElem') {
lastMessageText = lastMessage.payload.data || '[自定义消息]'
} else {
lastMessageText = '[未知消息类型]'
}
lastMessageTime = (lastMessage.lastTime || lastMessage.time || 0) * 1000
2026-01-20 13:21:50 +08:00
}
2026-01-22 16:35:05 +08:00
return {
...groupInfo,
groupID: groupID,
doctorId,
patientName,
conversationID: conversation.conversationID,
lastMessage: lastMessageText,
lastMessageTime: lastMessageTime,
unreadCount: conversation.unreadCount || 0
}
} catch (error) {
console.error(`处理群聊会话失败:`, error)
return {
groupID: conversation.conversationID,
name: conversation.groupProfile?.name || '问诊群聊',
doctorId: '',
patientName: '',
avatar: '/static/home/avatar.svg',
lastMessage: '获取失败',
lastMessageTime: Date.now(),
unreadCount: conversation.unreadCount || 0,
memberCount: 0
2026-01-20 13:21:50 +08:00
}
}
2026-01-22 16:35:05 +08:00
})
)
2026-01-20 13:21:50 +08:00
2026-01-22 16:35:05 +08:00
console.log('处理后的群聊列表数量:', groupsWithInfo.length)
resolve({
success: true,
groupList: groupsWithInfo,
totalCount: groupsWithInfo.length,
data: conversationResponse.data
2026-01-20 13:21:50 +08:00
})
})
2026-01-22 16:35:05 +08:00
.catch((imError) => {
console.error('获取会话列表失败:', imError)
reject({
success: false,
error: imError
})
2026-01-20 13:21:50 +08:00
})
})
}
// 创建问诊群聊
createGroup({ type, name, groupID, memberList, timeout = 30000, retryCount = 2 }) {
return new Promise((resolve, reject) => {
if (!this.tim) {
reject(new Error('TIM实例不存在'))
return
}
if (!this.isLoggedIn) {
console.log('SDK未ready等待SDK初始化...')
const maxWaitTime = 30000
const startTime = Date.now()
const checkSDKReady = () => {
if (this.isLoggedIn) {
console.log('SDK已ready开始创建群聊')
this.createGroupInternal({ type, name, groupID, memberList, timeout, retryCount })
.then(resolve)
.catch(reject)
} else if (Date.now() - startTime > maxWaitTime) {
reject(new Error('等待SDK初始化超时'))
} else {
console.log('SDK仍未ready继续等待...')
setTimeout(checkSDKReady, 1000)
}
}
checkSDKReady()
return
}
this.createGroupInternal({ type, name, groupID, memberList, timeout, retryCount })
.then(resolve)
.catch(reject)
})
}
// 内部创建群组方法
async createGroupInternal({ type, name, groupID, memberList, timeout, retryCount }) {
let lastError = null
for (let attempt = 0; attempt <= retryCount; attempt++) {
try {
console.log(`创建群聊尝试 ${attempt + 1}/${retryCount + 1}:`, {
type,
name,
groupID,
memberCount: memberList ? memberList.length : 0
})
const result = await this.createGroupWithTimeout({ type, name, groupID, memberList, timeout })
console.log(`创建群聊成功,尝试次数: ${attempt + 1}`)
return result
} catch (error) {
lastError = error
console.error(`${attempt + 1} 次创建群聊失败:`, error)
if (attempt === retryCount) break
const retryDelay = Math.min(1000 * Math.pow(2, attempt), 5000)
console.log(`等待 ${retryDelay}ms 后重试...`)
await new Promise(resolve => setTimeout(resolve, retryDelay))
}
}
throw lastError
}
// 带超时的创建群组方法
createGroupWithTimeout({ type, name, groupID, memberList, timeout = 30000 }) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error(`创建群聊超时 (${timeout}ms)`))
}, timeout)
if (!groupID) {
clearTimeout(timeoutId)
reject(new Error('群聊ID不能为空'))
return
}
if (!name) {
clearTimeout(timeoutId)
reject(new Error('群聊名称不能为空'))
return
}
let groupType = TIM.TYPES.GRP_WORK
if (type === 'GRP_WORK') {
groupType = TIM.TYPES.GRP_WORK
} else if (type === 'GRP_PUBLIC') {
groupType = TIM.TYPES.GRP_PUBLIC
} else if (type === 'GRP_CHATROOM') {
groupType = TIM.TYPES.GRP_CHATROOM
} else if (type === 'GRP_AVCHATROOM') {
groupType = TIM.TYPES.GRP_AVCHATROOM
} else if (type === 'GRP_MEETING') {
groupType = TIM.TYPES.GRP_MEETING
}
const createGroupParams = {
type: groupType,
name: name,
groupID: groupID
}
if (memberList && memberList.length > 0) {
createGroupParams.memberList = memberList
}
console.log('创建群组参数:', createGroupParams)
this.tim.createGroup(createGroupParams).then(async (imResponse) => {
clearTimeout(timeoutId)
console.log('创建问诊群聊成功:', imResponse)
try {
2026-01-22 15:13:26 +08:00
await api('sendSystemMessage', { groupID, status: 'pending' })
2026-01-20 13:21:50 +08:00
console.log('pending系统消息发送成功')
} catch (error) {
console.error('pending系统消息发送失败:', error)
}
resolve({
success: true,
groupID: groupID,
data: imResponse.data
})
}).catch((imError) => {
clearTimeout(timeoutId)
const enhancedError = {
...imError,
originalError: imError,
groupID: groupID,
name: name,
type: type,
timestamp: new Date().toISOString()
}
console.error('创建问诊群聊失败:', enhancedError)
const errorMessage = enhancedError.message || enhancedError.originalError?.message || ''
if (errorMessage.includes('group id has been used') || errorMessage.includes('group id has been used!')) {
console.log('群聊ID已存在视为创建成功')
resolve({
success: true,
groupID: groupID,
data: { groupID: groupID },
message: '群聊已存在'
})
return
}
reject({
success: false,
error: enhancedError,
userMessage: this.formatErrorMessage(enhancedError)
})
})
})
}
// 格式化错误信息
formatErrorMessage(error) {
if (!error) return '创建群聊失败,请稍后重试'
const errorCode = error.code || (error.originalError && error.originalError.code)
const errorMessage = error.message || (error.originalError && error.originalError.message) || ''
if (errorMessage.includes('group id has been used') || errorMessage.includes('group id has been used!')) {
return 'SUCCESS_GROUP_EXISTS'
}
switch (errorCode) {
case 2801:
return '网络连接超时,请检查网络连接后重试'
case 2800:
return '网络连接异常,请检查网络设置'
case 2802:
return '服务器连接失败,请稍后重试'
case 2803:
return '请求失败,请检查网络连接'
case 2804:
return '服务暂时不可用,请稍后重试'
case 10004:
return '群聊ID已存在请使用其他ID'
case 10007:
return '群聊创建权限不足'
case 10010:
return '群聊数量已达上限'
default:
if (errorMessage.includes('超时') || errorMessage.includes('timeout')) {
return '操作超时,请检查网络连接后重试'
} else if (errorMessage.includes('网络') || errorMessage.includes('network')) {
return '网络连接异常,请检查网络设置'
} else if (errorMessage.includes('权限') || errorMessage.includes('permission')) {
return '操作权限不足,请联系管理员'
} else {
return `创建群聊失败:${errorMessage}`
}
}
}
// 设置当前会话ID
setConversationID(conversationID) {
this.currentConversationID = conversationID
console.log('设置当前会话ID:', conversationID)
}
// 进入会话
enterConversation(conversationID) {
console.log("【enterConversation】进入会话:", conversationID)
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 更新当前会话ID
this.currentConversationID = conversationID
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 清空当前消息列表
this.messageList = []
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 重置分页状态
this.nextReqMessageID = ""
this.isCompleted = false
this.isLoadingMore = false
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
console.log(" 会话ID已更新消息列表已清空分页状态已重置")
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 进入群聊会话默认加载20条消息
this.enterGroupConversation(conversationID, 20)
}
// 进入群聊会话
async enterGroupConversation(groupID, count = 20) {
console.log("【enterGroupConversation】进入群聊会话, groupID:", groupID, "count:", count)
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
let conversationID = groupID
let actualGroupID = groupID
if (groupID.startsWith('GROUP')) {
actualGroupID = groupID.replace('GROUP', '')
conversationID = groupID
} else {
conversationID = `GROUP${groupID}`
actualGroupID = groupID
}
console.log(" conversationID:", conversationID, "actualGroupID:", actualGroupID)
// 清空消息列表,准备加载新会话的消息
this.messageList = []
// 确保设置当前会话ID防止消息混淆
this.currentConversationID = conversationID
console.log('进入群聊会话设置currentConversationID:', conversationID)
this.conversation = {
conversationID: conversationID,
conversationType: TIM.TYPES.CONV_GROUP,
groupProfile: {
groupID: actualGroupID,
name: '问诊群聊',
type: TIM.TYPES.GRP_WORK
}
}
this.markConversationAsRead(conversationID)
// 首先从本地接口加载聊天记录
console.log(" 开始从本地接口加载聊天记录groupID:", actualGroupID)
await this.loadMessagesFromLocalAPI(actualGroupID, count)
}
// 从本地API加载聊天记录
async loadMessagesFromLocalAPI(groupID, count = 20, skip = 0, isPullUp = false) {
try {
console.log("【loadMessagesFromLocalAPI】开始从本地API加载聊天记录")
console.log(" groupID:", groupID, "count:", count, "skip:", skip, "isPullUp:", isPullUp)
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 调用本地接口获取聊天记录
2026-01-22 15:13:26 +08:00
const response = await api('getChatRecordsByGroupId', { groupID, count, skip })
2026-01-20 13:21:50 +08:00
if (response && response.success && response.data && response.data.records) {
const dbMessages = response.data.records
const hasMore = response.data.hasMore || false // 从后端获取hasMore字段
const total = response.data.total || 0
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
console.log(` 成功获取 ${dbMessages.length} 条聊天记录`)
console.log(` hasMore: ${hasMore}, total: ${total}`)
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 将数据库消息转换为IM消息格式
const convertedMessages = dbMessages
.map(dbMsg => this.convertDBMessageToIMFormat(dbMsg))
.filter(msg => msg !== null && this.filterMessage(msg))
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
console.log(` 转换后 ${convertedMessages.length} 条消息`)
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 按时间排序(从早到晚)
convertedMessages.sort((a, b) => a.lastTime - b.lastTime)
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 根据是否为上拉加载,决定如何更新消息列表
if (isPullUp) {
// 上拉加载更多:将新消息插入到列表前面(历史消息)
this.messageList.unshift(...convertedMessages)
// 再次排序确保顺序正确
this.messageList.sort((a, b) => a.lastTime - b.lastTime)
} else {
// 首次加载:直接替换消息列表
this.messageList = convertedMessages
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 设置分页状态 - 使用后端返回的 hasMore 字段
this.isCompleted = !hasMore
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
console.log(` 分页状态: isCompleted=${this.isCompleted}, hasMore=${hasMore}`)
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 触发回调
this.triggerCallback("onMessageListLoaded", {
messages: this.messageList,
isPullUp: isPullUp,
hasMore: hasMore,
isCompleted: this.isCompleted,
total: total,
})
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
return {
success: true,
count: convertedMessages.length,
hasMore: hasMore,
isCompleted: this.isCompleted,
messages: this.messageList,
total: total,
}
} else {
console.warn(" ⚠️ 本地API返回数据格式错误")
return {
success: false,
message: "本地API返回数据格式错误",
count: 0,
}
}
} catch (error) {
console.error(" ❌ 从本地API加载聊天记录失败:", error)
return {
success: false,
error: error,
message: "加载聊天记录失败",
count: 0,
}
}
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 将数据库消息转换为IM消息格式
convertDBMessageToIMFormat(dbMsg) {
try {
if (!dbMsg || !dbMsg.MsgBody || !Array.isArray(dbMsg.MsgBody) || dbMsg.MsgBody.length === 0) {
return null
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 获取第一个消息体(通常一个消息只有一个消息体)
const msgBody = dbMsg.MsgBody[0]
const msgType = msgBody.MsgType
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 确定消息的流向in/out
// 根据 From_Account 判断是否为当前用户发送的消息
const flow = dbMsg.From_Account === this.currentUserID ? 'out' : 'in'
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 计算时间戳(毫秒)
let lastTime = Date.now()
if (dbMsg.MsgTime) {
// MsgTime 是秒级时间戳
lastTime = dbMsg.MsgTime * 1000
} else if (dbMsg.createdAt) {
lastTime = new Date(dbMsg.createdAt).getTime()
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 构建基础消息对象
const message = {
ID: dbMsg.MsgSeq || dbMsg._id || `db_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
from: dbMsg.From_Account || '',
flow: flow,
type: msgType,
payload: this.convertDBPayloadToIMPayload(msgType, msgBody.MsgContent),
lastTime: lastTime,
status: 'success',
2026-01-22 16:35:05 +08:00
avatar: flow === 'in' ? '/static/home/avatar.svg' : '/static/center/user-avatar.png',
2026-01-20 13:21:50 +08:00
conversationID: this.currentConversationID,
MsgSeq: dbMsg.MsgSeq, // 保留 MsgSeq 用于分页
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
return message
} catch (error) {
console.error("转换数据库消息格式失败:", error, dbMsg)
return null
}
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 将数据库消息体转换为IM payload格式
convertDBPayloadToIMPayload(msgType, msgContent) {
try {
if (!msgContent) {
return {}
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
switch (msgType) {
case 'TIMTextElem':
return {
text: msgContent.Text || ''
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
case 'TIMImageElem':
return {
imageInfoArray: msgContent.ImageInfoArray || []
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
case 'TIMSoundElem':
return {
url: msgContent.Url || '',
second: msgContent.Second || 0,
downloadFlag: msgContent.Download_Flag || 2
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
case 'TIMCustomElem':
return {
data: msgContent.Data || '',
description: msgContent.Desc || '',
extension: msgContent.Ext || ''
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
case 'TIMVideoFileElem':
return {
videoUrl: msgContent.VideoUrl || '',
videoSize: msgContent.VideoSize || 0,
videoSecond: msgContent.VideoSecond || 0,
videoFormat: msgContent.VideoFormat || '',
videoDownloadFlag: msgContent.VideoDownloadFlag || 2,
thumbUrl: msgContent.ThumbUrl || '',
thumbSize: msgContent.ThumbSize || 0,
thumbWidth: msgContent.ThumbWidth || 0,
thumbHeight: msgContent.ThumbHeight || 0,
thumbFormat: msgContent.ThumbFormat || '',
thumbDownloadFlag: msgContent.ThumbDownloadFlag || 2
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
case 'TIMFileElem':
return {
url: msgContent.Url || '',
fileSize: msgContent.FileSize || 0,
fileName: msgContent.FileName || '',
downloadFlag: msgContent.Download_Flag || 2
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
default:
return msgContent
}
} catch (error) {
console.error("转换消息体失败:", error, msgType, msgContent)
return {}
}
}
// 分页加载消息(单次加载)
async loadMessagesPage(options = {}) {
if (!this.conversation) {
console.log("会话未初始化,无法加载消息")
return Promise.reject(new Error("会话未初始化"))
}
const count = options.count || 20
// 记录调用前是否已有nextReqMessageID用于判断是否为首次加载
const isLoadMore = !!(this.nextReqMessageID && this.nextReqMessageID !== "")
try {
// 构建请求参数
const requestParams = {
conversationID: this.conversation.conversationID,
count: Math.min(count, 15), // 腾讯IM SDK限制count在1-15之间
}
// 如果有nextReqMessageID说明是加载更多
if (isLoadMore) {
requestParams.nextReqMessageID = this.nextReqMessageID
console.log("加载更多消息nextReqMessageID:", this.nextReqMessageID)
} else {
console.log("首次加载消息")
}
console.log(" 📡 发送请求到腾讯IM:", requestParams)
const response = await this.tim.getMessageList(requestParams)
const messageList = response.data.messageList
const oldNextReqMessageID = this.nextReqMessageID
const oldIsCompleted = this.isCompleted
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
this.nextReqMessageID = response.data.nextReqMessageID
this.isCompleted = response.data.isCompleted
console.log(" 📥 收到腾讯IM响应:")
console.log(` 消息数量: ${messageList.length}`)
console.log(` nextReqMessageID: ${oldNextReqMessageID}${this.nextReqMessageID}`)
console.log(` isCompleted: ${oldIsCompleted}${this.isCompleted}`)
if (messageList && messageList.length > 0) {
// 过滤掉不需要显示的消息
const filteredMessages = messageList.filter((message) =>
this.filterMessage(message)
)
// 转换消息格式
const messages = filteredMessages.map((message) => {
return this.convertMessageFormat(message)
})
// 将新消息插入到列表前面(历史消息在前)
this.messageList.unshift(...messages)
// 按时间戳排序消息列表(从早到晚)
this.messageList.sort((a, b) => a.lastTime - b.lastTime)
}
// 触发回调 - 使用 isLoadMore 来判断是否为上拉加载
this.triggerCallback("onMessageListLoaded", {
messages: this.messageList,
isPullUp: isLoadMore, // 只有在加载更多时才是 true
hasMore: !this.isCompleted,
isCompleted: this.isCompleted,
})
return {
success: true,
count: messageList ? messageList.length : 0,
hasMore: !this.isCompleted,
isCompleted: this.isCompleted,
messages: this.messageList,
}
} catch (error) {
console.error("分页加载消息失败:", error)
return {
success: false,
error: error,
count: 0,
}
}
}
// 加载更多消息(供页面调用)
async loadMoreMessages() {
console.log("【loadMoreMessages】开始加载更多消息")
console.log(" 当前状态: isLoadingMore=", this.isLoadingMore)
console.log(" 当前状态: isCompleted=", this.isCompleted)
console.log(" 当前消息数量:", this.messageList.length)
if (this.isLoadingMore) {
console.log(" ⚠️ 正在加载中,跳过重复请求")
return { success: false, message: "正在加载中" }
}
if (this.isCompleted) {
console.log(" ✅ 已加载全部消息")
return { success: false, message: "已加载全部消息" }
}
this.isLoadingMore = true
console.log(" 🔄 设置 isLoadingMore = true")
try {
// 从本地数据库加载更多历史消息
// 使用当前消息列表的长度作为 skip 值
const skip = this.messageList.length
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 获取群组ID
let groupID = ''
if (this.conversation && this.conversation.groupProfile) {
groupID = this.conversation.groupProfile.groupID
} else if (this.currentConversationID) {
groupID = this.currentConversationID.replace('GROUP', '')
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
if (!groupID) {
console.error(" ❌ 无法获取群组ID")
this.isLoadingMore = false
return {
success: false,
message: "无法获取群组ID",
}
}
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
console.log(" 从本地数据库加载更多groupID:", groupID, "skip:", skip)
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 调用本地API加载更多消息
const result = await this.loadMessagesFromLocalAPI(groupID, 20, skip, true)
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
this.isLoadingMore = false
console.log(" 🔄 设置 isLoadingMore = false")
console.log(" 📥 loadMessagesFromLocalAPI 返回结果:", result)
if (result.success) {
console.log(" ✅ 加载成功,新增", result.count, "条消息")
return {
success: true,
count: result.count,
hasMore: result.hasMore,
isCompleted: result.isCompleted,
}
} else {
console.log(" ❌ 加载失败:", result.message || result.error)
return {
success: false,
error: result.error,
message: result.message || "加载失败",
}
}
} catch (error) {
console.error(" ❌ 加载更多异常:", error)
this.isLoadingMore = false
return {
success: false,
error: error,
message: "加载失败",
}
}
}
// 后台刷新服务器消息(已弃用缓存功能)
async refreshMessagesFromServer(conversationID, cachedMessages) {
try {
const result = await this.getMessages({ getAllMessages: true })
if (result.success) {
// 严格过滤:只保留属于当前会话的消息
const filteredMessages = result.messages.filter(msg => msg.conversationID === conversationID)
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
this.messageList = filteredMessages
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
console.log(`消息刷新完成,过滤后${filteredMessages.length}条消息`)
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 触发更新回调
this.triggerCallback('onMessageListLoaded', {
messages: this.messageList,
isPullUp: false,
hasMore: false,
isCompleted: true,
isRefresh: true // 标记这是刷新操作
})
}
} catch (error) {
console.error('后台刷新消息失败:', error)
}
}
// 合并消息(保留本地消息,避免覆盖)
mergeMessages(serverMessages, cachedMessages) {
const messageMap = new Map()
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 先添加服务器消息
serverMessages.forEach(msg => {
if (msg.ID) {
messageMap.set(msg.ID, msg)
}
})
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 再添加缓存消息(覆盖服务器消息,保留本地最新状态)
cachedMessages.forEach(msg => {
if (msg.ID) {
// 如果是本地消息ID以local_开头或者是发送中/失败的消息,保留缓存中的版本
if (msg.ID.startsWith('local_') || msg.status === 'sending' || msg.status === 'failed') {
messageMap.set(msg.ID, msg)
} else if (!messageMap.has(msg.ID)) {
// 服务器没有的消息也保留
messageMap.set(msg.ID, msg)
}
}
})
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 转换为数组并按时间排序
const mergedArray = Array.from(messageMap.values())
mergedArray.sort((a, b) => a.lastTime - b.lastTime)
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
console.log(`消息合并完成: 服务器${serverMessages.length}条, 缓存${cachedMessages.length}条, 合并后${mergedArray.length}`)
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
return mergedArray
}
// 获取消息列表
async getMessages(options = {}) {
if (!this.conversation) {
console.log('会话未初始化,跳过获取消息列表')
this.isLoading = false
return Promise.resolve({ success: false, error: '会话未初始化' })
}
this.isLoading = true
const defaultOptions = {
count: 20,
nextReqMessageID: '',
isPullUp: false,
getAllMessages: false
}
const requestOptions = { ...defaultOptions, ...options }
try {
if (requestOptions.getAllMessages) {
return await this.getAllMessages(requestOptions)
} else {
return await this.getMessagesBatch(requestOptions)
}
} catch (error) {
console.error('获取消息列表失败:', error)
this.isLoading = false
this.messageList = []
this.triggerCallback('onMessageListLoaded', {
messages: this.messageList,
isPullUp: requestOptions.isPullUp,
hasMore: false,
error: error
})
return { success: false, error }
}
}
// 获取单批消息
async getMessagesBatch(options) {
const requestParams = {
conversationID: this.conversation.conversationID,
count: options.count
}
if (options.nextReqMessageID) {
requestParams.nextReqMessageID = options.nextReqMessageID
}
console.log('请求消息列表参数:', requestParams)
const response = await this.tim.getMessageList(requestParams)
const messageList = response.data.messageList
const nextReqMessageID = response.data.nextReqMessageID
const isCompleted = response.data.isCompleted
console.log(`TIM返回消息: ${messageList.length}条, isCompleted: ${isCompleted}`)
// 处理消息
const messages = this.processMessages(messageList)
console.log(`处理后消息: ${messages.length}`)
// 更新消息列表
if (options.isPullUp) {
this.messageList.unshift(...messages)
} else {
this.messageList = messages
}
// 缓存消息
this.cacheMessages()
this.isLoading = false
// 触发回调仅在非getAllMessages调用时触发
if (!options.skipCallback) {
this.triggerCallback('onMessageListLoaded', {
messages: this.messageList,
isPullUp: options.isPullUp,
hasMore: !isCompleted,
nextReqMessageID: nextReqMessageID,
isCompleted: isCompleted
})
}
// 返回当前批次的消息而不是整个messageList
return {
success: true,
messages: messages, // 返回处理后的当前批次消息
hasMore: !isCompleted,
nextReqMessageID: nextReqMessageID,
isCompleted: isCompleted
}
}
// 获取全部历史消息
async getAllMessages(options) {
console.log('开始获取全部历史消息...')
let allMessages = []
let nextReqMessageID = options.nextReqMessageID || ''
let isCompleted = false
let requestCount = 0
const maxRequests = 50 // 最多请求50次避免无限循环
while (!isCompleted && requestCount < maxRequests) {
requestCount++
console.log(`获取历史消息第${requestCount}批...`)
const batchOptions = {
...options,
nextReqMessageID: nextReqMessageID,
isPullUp: false, // 改为false因为我们是在构建完整列表
skipCallback: true // 跳过单批回调,最后统一回调
}
const result = await this.getMessagesBatch(batchOptions)
if (!result.success) {
console.error(`${requestCount}批消息获取失败:`, result.error)
throw new Error(result.error)
}
console.log(`${requestCount}批获取到${result.messages.length}条消息`)
// 将当前批次的消息添加到开头(历史消息在前)
if (result.messages.length > 0) {
// 使用Set去重避免重复消息
const existingIds = new Set(allMessages.map(m => m.ID))
const newMessages = result.messages.filter(m => !existingIds.has(m.ID))
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
if (newMessages.length > 0) {
allMessages.unshift(...newMessages)
console.log(`添加${newMessages.length}条新消息,当前总数: ${allMessages.length}`)
}
}
nextReqMessageID = result.nextReqMessageID
isCompleted = result.isCompleted
console.log(`isCompleted: ${isCompleted}, nextReqMessageID: ${nextReqMessageID}`)
// 如果没有更多消息ID说明已经获取完毕
if (!nextReqMessageID) {
console.log('没有更多消息ID获取完成')
break
}
}
if (requestCount >= maxRequests) {
console.warn(`达到最大请求次数限制(${maxRequests}),停止获取`)
}
// 更新消息列表
this.messageList = allMessages
this.cacheMessages()
this.isLoading = false
console.log(`全部历史消息获取完成,共${allMessages.length}条消息,请求了${requestCount}`)
// 触发回调
this.triggerCallback('onMessageListLoaded', {
messages: this.messageList,
isPullUp: false,
hasMore: false,
nextReqMessageID: '',
isCompleted: true
})
return {
success: true,
messages: this.messageList,
hasMore: false,
nextReqMessageID: '',
isCompleted: true
}
}
// 处理消息 - 统一过滤和转换
processMessages(messageList) {
const filteredMessages = messageList.filter(message => this.filterMessage(message))
return filteredMessages.map(message => this.convertMessageFormat(message))
}
// 缓存消息 - 统一缓存逻辑
cacheMessages() {
// 缓存功能已移除
}
// 清理指定会话的缓存(已弃用)
clearConversationCache(conversationID) {
// 缓存功能已移除
}
// 清理所有缓存(已弃用)
clearAllCache() {
// 缓存功能已移除
}
// 获取消息列表(兼容旧方法)
getMessageList(options = {}) {
return this.getMessages(options)
}
// 发送文本消息
async sendTextMessage(text, cloudCustomData = '') {
if (!text.trim()) return
if (!this.tim) {
this.triggerCallback('onError', 'IM未初始化')
2026-01-22 16:35:05 +08:00
return { success: false, error: 'IM未初始化' }
2026-01-20 13:21:50 +08:00
}
2026-01-22 16:35:05 +08:00
// 检查登录状态
if (!this.isLoggedIn) {
console.error('IM未登录无法发送消息');
this.triggerCallback('onError', 'IM未登录请稍后重试')
return { success: false, error: 'IM未登录' }
2026-01-20 13:21:50 +08:00
}
2026-01-22 16:35:05 +08:00
// 优先使用 currentConversationID如果没有则尝试从 conversation 获取
let conversationID = this.currentConversationID;
if (!conversationID && this.conversation) {
conversationID = this.conversation.conversationID;
}
if (!conversationID) {
console.error('会话ID不存在');
this.triggerCallback('onError', '会话不存在,请重新进入聊天')
return { success: false, error: '会话ID不存在' }
}
// 从 conversationID 提取 groupID
let groupID = null;
if (conversationID.startsWith('GROUP')) {
groupID = conversationID.replace('GROUP', '');
} else if (this.conversation?.groupProfile?.groupID) {
groupID = this.conversation.groupProfile.groupID;
2026-01-20 13:21:50 +08:00
}
if (!groupID) {
2026-01-22 16:35:05 +08:00
console.error('无法获取群聊IDconversationID:', conversationID);
2026-01-20 13:21:50 +08:00
this.triggerCallback('onError', '无法获取群聊ID')
return { success: false, error: '无法获取群聊ID' }
}
const localMessage = {
ID: `local_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
flow: 'out',
type: 'TIMTextElem',
payload: { text: text },
lastTime: Date.now(),
status: 'sending',
avatar: '/static/center/user-avatar.png',
conversationID: conversationID,
from: this.currentUserID
}
// 缓存功能已移除
this.triggerCallback('onMessageReceived', localMessage)
const message = this.tim.createTextMessage({
to: groupID,
conversationType: TIM.TYPES.CONV_GROUP,
cloudCustomData,
payload: { text: text }
})
try {
await this.tim.sendMessage(message)
localMessage.status = 'success'
return { success: true, message: localMessage }
} catch (error) {
console.error('文本消息发送失败:', error)
localMessage.status = 'failed'
2026-01-22 16:35:05 +08:00
// 如果是因为未登录导致的失败,尝试重连
if (error.message && (error.message.includes('not login') || error.message.includes('sdk not ready'))) {
console.log('检测到未登录错误,尝试重连...');
this.isLoggedIn = false;
this.ensureIMConnection();
}
2026-01-20 13:21:50 +08:00
return { success: false, error }
}
}
// 发送图片消息
async sendImageMessage(imageFile) {
2026-01-22 16:35:05 +08:00
console.log('sendImageMessage 被调用,参数:', imageFile);
2026-01-20 13:21:50 +08:00
if (!this.tim) {
this.triggerCallback('onError', 'IM未初始化')
2026-01-22 16:35:05 +08:00
return { success: false, error: 'IM未初始化' }
2026-01-20 13:21:50 +08:00
}
2026-01-22 16:35:05 +08:00
// 检查登录状态
if (!this.isLoggedIn) {
console.error('IM未登录无法发送消息');
this.triggerCallback('onError', 'IM未登录请稍后重试')
return { success: false, error: 'IM未登录' }
}
// 优先使用 currentConversationID如果没有则尝试从 conversation 获取
let conversationID = this.currentConversationID;
if (!conversationID && this.conversation) {
conversationID = this.conversation.conversationID;
}
if (!conversationID) {
console.error('会话ID不存在');
this.triggerCallback('onError', '会话不存在,请重新进入聊天')
return { success: false, error: '会话ID不存在' }
2026-01-20 13:21:50 +08:00
}
2026-01-22 16:35:05 +08:00
// 从 conversationID 提取 groupID
let groupID = null;
if (conversationID.startsWith('GROUP')) {
groupID = conversationID.replace('GROUP', '');
} else if (this.conversation?.groupProfile?.groupID) {
groupID = this.conversation.groupProfile.groupID;
2026-01-20 13:21:50 +08:00
}
if (!groupID) {
2026-01-22 16:35:05 +08:00
console.error('无法获取群聊IDconversationID:', conversationID);
2026-01-20 13:21:50 +08:00
this.triggerCallback('onError', '无法获取群聊ID')
return { success: false, error: '无法获取群聊ID' }
}
2026-01-22 16:35:05 +08:00
console.log('发送图片消息conversationID:', conversationID, 'groupID:', groupID);
2026-01-22 17:02:15 +08:00
// imageFile 现在是完整的 wx.chooseImage 返回对象
console.log('接收到的图片选择结果:', imageFile);
console.log('类型:', typeof imageFile);
console.log('keys:', imageFile ? Object.keys(imageFile) : 'null');
// 验证对象
if (!imageFile) {
console.error('图片选择结果为空');
this.triggerCallback('onError', '图片文件无效');
return { success: false, error: '图片选择结果为空' };
}
// 获取文件路径用于显示预览
let previewPath = '';
if (imageFile.tempFilePaths && imageFile.tempFilePaths.length > 0) {
previewPath = imageFile.tempFilePaths[0];
} else if (imageFile.tempFiles && imageFile.tempFiles.length > 0) {
previewPath = imageFile.tempFiles[0].tempFilePath || imageFile.tempFiles[0].path;
2026-01-22 16:35:05 +08:00
}
2026-01-20 13:21:50 +08:00
2026-01-22 17:02:15 +08:00
console.log('预览路径:', previewPath);
// 获取图片尺寸信息(用于本地预览)
const imageInfo = await this.getImageInfo(previewPath);
2026-01-20 13:21:50 +08:00
const localMessage = {
ID: `local_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
flow: 'out',
type: 'TIMImageElem',
payload: {
imageInfoArray: [{
2026-01-22 17:02:15 +08:00
url: previewPath,
2026-01-20 13:21:50 +08:00
width: imageInfo.width,
height: imageInfo.height
}]
},
lastTime: Date.now(),
status: 'sending',
avatar: '/static/center/user-avatar.png',
conversationID: conversationID,
from: this.currentUserID
}
console.log('创建本地图片消息:', localMessage)
// 缓存功能已移除
// 触发消息接收回调让UI立即显示
this.triggerCallback('onMessageReceived', localMessage)
2026-01-22 17:02:15 +08:00
console.log('准备创建 TIM 图片消息groupID:', groupID, 'imageFile:', imageFile);
2026-01-22 16:35:05 +08:00
2026-01-22 17:02:15 +08:00
try {
// 创建图片消息 - 直接传递 wx.chooseImage 的完整返回对象
const message = this.tim.createImageMessage({
to: groupID,
conversationType: TIM.TYPES.CONV_GROUP,
payload: {
file: imageFile // 传递完整的 wx.chooseImage 返回对象
}
})
2026-01-20 13:21:50 +08:00
2026-01-22 17:02:15 +08:00
console.log('TIM 图片消息已创建:', message);
2026-01-22 16:35:05 +08:00
console.log('开始发送图片消息...');
const sendResult = await this.tim.sendMessage(message);
console.log('图片消息发送成功:', sendResult);
2026-01-20 13:21:50 +08:00
localMessage.status = 'success'
return { success: true, message: localMessage }
} catch (error) {
console.error('图片消息发送失败:', error)
2026-01-22 17:02:15 +08:00
console.error('错误详情:', {
message: error.message,
stack: error.stack,
imageFile: imageFile
});
2026-01-20 13:21:50 +08:00
localMessage.status = 'failed'
2026-01-22 16:35:05 +08:00
// 如果是因为未登录导致的失败,尝试重连
if (error.message && (error.message.includes('not login') || error.message.includes('sdk not ready'))) {
console.log('检测到未登录错误,尝试重连...');
this.isLoggedIn = false;
this.ensureIMConnection();
}
2026-01-20 13:21:50 +08:00
return { success: false, error }
}
}
// 发送语音消息
async sendVoiceMessage(voiceFile, duration) {
if (!this.tim) {
this.triggerCallback('onError', 'IM未初始化')
2026-01-22 16:35:05 +08:00
return { success: false, error: 'IM未初始化' }
2026-01-20 13:21:50 +08:00
}
2026-01-22 16:35:05 +08:00
// 检查登录状态
if (!this.isLoggedIn) {
console.error('IM未登录无法发送消息');
this.triggerCallback('onError', 'IM未登录请稍后重试')
return { success: false, error: 'IM未登录' }
}
// 优先使用 currentConversationID如果没有则尝试从 conversation 获取
let conversationID = this.currentConversationID;
if (!conversationID && this.conversation) {
conversationID = this.conversation.conversationID;
}
if (!conversationID) {
console.error('会话ID不存在');
this.triggerCallback('onError', '会话不存在,请重新进入聊天')
return { success: false, error: '会话ID不存在' }
2026-01-20 13:21:50 +08:00
}
2026-01-22 16:35:05 +08:00
// 从 conversationID 提取 groupID
let groupID = null;
if (conversationID.startsWith('GROUP')) {
groupID = conversationID.replace('GROUP', '');
} else if (this.conversation?.groupProfile?.groupID) {
groupID = this.conversation.groupProfile.groupID;
2026-01-20 13:21:50 +08:00
}
if (!groupID) {
2026-01-22 16:35:05 +08:00
console.error('无法获取群聊IDconversationID:', conversationID);
2026-01-20 13:21:50 +08:00
this.triggerCallback('onError', '无法获取群聊ID')
return { success: false, error: '无法获取群聊ID' }
}
const localMessage = {
ID: `local_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
flow: 'out',
type: 'TIMSoundElem',
payload: {
url: this.getVoiceUrl(voiceFile),
second: duration
},
lastTime: Date.now(),
status: 'sending',
avatar: '/static/center/user-avatar.png',
conversationID: conversationID,
from: this.currentUserID
}
// 缓存功能已移除
this.triggerCallback('onMessageReceived', localMessage)
const message = this.tim.createAudioMessage({
to: groupID,
conversationType: TIM.TYPES.CONV_GROUP,
payload: { file: voiceFile }
})
try {
await this.tim.sendMessage(message)
localMessage.status = 'success'
return { success: true, message: localMessage }
} catch (error) {
console.error('语音消息发送失败:', error)
localMessage.status = 'failed'
2026-01-22 16:35:05 +08:00
// 如果是因为未登录导致的失败,尝试重连
if (error.message && (error.message.includes('not login') || error.message.includes('sdk not ready'))) {
console.log('检测到未登录错误,尝试重连...');
this.isLoggedIn = false;
this.ensureIMConnection();
}
2026-01-20 13:21:50 +08:00
return { success: false, error }
}
}
// 发送自定义消息
async sendCustomMessage(messageData) {
if (!this.tim) {
this.triggerCallback('onError', 'IM未初始化')
2026-01-22 16:35:05 +08:00
return { success: false, error: 'IM未初始化' }
2026-01-20 13:21:50 +08:00
}
2026-01-22 16:35:05 +08:00
// 检查登录状态
if (!this.isLoggedIn) {
console.error('IM未登录无法发送消息');
this.triggerCallback('onError', 'IM未登录请稍后重试')
return { success: false, error: 'IM未登录' }
}
// 优先使用 currentConversationID如果没有则尝试从 conversation 获取
let conversationID = this.currentConversationID;
if (!conversationID && this.conversation) {
conversationID = this.conversation.conversationID;
2026-01-20 13:21:50 +08:00
}
2026-01-22 16:35:05 +08:00
if (!conversationID) {
console.error('会话ID不存在');
this.triggerCallback('onError', '会话不存在,请重新进入聊天')
return { success: false, error: '会话ID不存在' }
}
// 从 conversationID 提取 groupID
let groupID = null;
if (conversationID.startsWith('GROUP')) {
groupID = conversationID.replace('GROUP', '');
} else if (this.conversation?.groupProfile?.groupID) {
groupID = this.conversation.groupProfile.groupID;
2026-01-20 13:21:50 +08:00
}
if (!groupID) {
2026-01-22 16:35:05 +08:00
console.error('无法获取群聊IDconversationID:', conversationID);
2026-01-20 13:21:50 +08:00
this.triggerCallback('onError', '无法获取群聊ID')
return { success: false, error: '无法获取群聊ID' }
}
const localMessage = {
ID: `local_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
flow: 'out',
type: 'TIMCustomElem',
payload: {
data: JSON.stringify(messageData),
description: messageData.content || '自定义消息',
extension: messageData.messageType || 'custom'
},
lastTime: Date.now(),
status: 'sending',
avatar: '/static/center/user-avatar.png',
conversationID: conversationID,
from: this.currentUserID
}
// 缓存功能已移除
this.triggerCallback('onMessageReceived', localMessage)
const message = this.tim.createCustomMessage({
to: groupID,
conversationType: TIM.TYPES.CONV_GROUP,
payload: {
data: JSON.stringify(messageData),
description: messageData.content || '自定义消息',
extension: messageData.messageType || 'custom'
}
})
try {
await this.tim.sendMessage(message)
localMessage.status = 'success'
return { success: true, message: localMessage }
} catch (error) {
console.error('自定义消息发送失败:', error)
localMessage.status = 'failed'
2026-01-22 16:35:05 +08:00
// 如果是因为未登录导致的失败,尝试重连
if (error.message && (error.message.includes('not login') || error.message.includes('sdk not ready'))) {
console.log('检测到未登录错误,尝试重连...');
this.isLoggedIn = false;
this.ensureIMConnection();
}
2026-01-20 13:21:50 +08:00
return { success: false, error }
}
}
// 工具方法
filterMessage(message) {
if (message.type === 'TIMCustomElem' && message.payload && message.payload.data) {
if (message.payload.data === 'group_create' || message.payload.data === 'purchased') {
console.log('过滤消息:', message.ID, message.payload.data)
return false
}
}
return true
}
convertMessageFormat(timMessage) {
let lastTime
if (timMessage.lastTime) {
lastTime = timMessage.lastTime * 1000
} else if (timMessage.time) {
lastTime = timMessage.time * 1000
} else if (timMessage.timestamp) {
lastTime = timMessage.timestamp * 1000
} else {
lastTime = Date.now()
}
const message = {
ID: timMessage.ID,
from: timMessage.from,
flow: timMessage.flow,
type: timMessage.type,
payload: timMessage.payload,
lastTime: lastTime,
status: timMessage.status || 'success',
2026-01-22 16:35:05 +08:00
avatar: timMessage.flow === 'in' ? '/static/home/avatar.svg' : '/static/center/user-avatar.png',
2026-01-20 13:21:50 +08:00
// 优先使用消息本身的conversationID确保消息归属正确的会话
conversationID: timMessage.conversationID
}
return message
}
formatLastMessage(message) {
try {
switch (message.type) {
case 'TIMTextElem':
return message.payload.text || '[文本消息]'
case 'TIMImageElem':
return '[图片]'
case 'TIMSoundElem':
return '[语音]'
case 'TIMCustomElem':
try {
const customData = JSON.parse(message.payload.data)
if (customData.messageType === 'symptom') {
return '[病情描述]'
} else if (customData.messageType === 'prescription') {
return '[处方单]'
} else if (customData.messageType === 'refill') {
return '[续方申请]'
} else if (customData.messageType === 'survey') {
return '[问卷调查]'
} else {
return customData.content || '[自定义消息]'
}
} catch (error) {
return '[自定义消息]'
}
default:
return '[未知消息类型]'
}
} catch (error) {
console.error('格式化最后一条消息失败:', error)
return '[消息]'
}
}
getImageUrl(imageFile) {
2026-01-22 17:02:15 +08:00
// 支持 tempFilePath 或 path
2026-01-22 16:35:05 +08:00
if (imageFile?.tempFilePath) {
2026-01-22 17:02:15 +08:00
return imageFile.tempFilePath;
}
if (imageFile?.path) {
return imageFile.path;
2026-01-22 16:35:05 +08:00
}
// 处理字符串路径
2026-01-20 13:21:50 +08:00
if (typeof imageFile === 'string') {
2026-01-22 17:02:15 +08:00
return imageFile;
2026-01-20 13:21:50 +08:00
}
2026-01-22 16:35:05 +08:00
console.warn('无法获取图片URL使用默认图片:', imageFile);
2026-01-22 17:02:15 +08:00
return '/static/home/photo.png';
2026-01-20 13:21:50 +08:00
}
// 获取图片尺寸信息
2026-01-22 17:02:15 +08:00
getImageInfo(imagePath) {
2026-01-20 13:21:50 +08:00
return new Promise((resolve) => {
2026-01-22 17:02:15 +08:00
// 如果传入的是对象,尝试提取路径
if (typeof imagePath === 'object') {
if (imagePath?.tempFilePath) {
imagePath = imagePath.tempFilePath;
} else if (imagePath?.path) {
imagePath = imagePath.path;
} else {
console.warn('无法从对象中获取图片路径,使用默认尺寸:', imagePath);
resolve({ width: 400, height: 300 });
return;
}
}
// 如果不是字符串,使用默认尺寸
if (typeof imagePath !== 'string' || !imagePath) {
console.warn('图片路径无效,使用默认尺寸:', imagePath);
2026-01-20 13:21:50 +08:00
resolve({ width: 400, height: 300 });
return;
}
2026-01-22 16:35:05 +08:00
console.log('获取图片信息,路径:', imagePath);
2026-01-20 13:21:50 +08:00
// 使用uni.getImageInfo获取图片尺寸
uni.getImageInfo({
src: imagePath,
success: (res) => {
console.log('获取图片尺寸成功:', res);
resolve({
width: res.width,
height: res.height
});
},
fail: (err) => {
console.error('获取图片尺寸失败:', err);
// 失败时使用默认尺寸
resolve({ width: 400, height: 300 });
}
});
});
}
getVoiceUrl(voiceFile) {
if (typeof voiceFile === 'string') {
return voiceFile
}
if (voiceFile && voiceFile.tempFilePath) {
return voiceFile.tempFilePath
}
if (voiceFile && voiceFile.filePath) {
return voiceFile.filePath
}
return '/static/voice/default.mp3'
}
// 标记会话为已读
markConversationAsRead(conversationID) {
if (!this.tim || !this.isLoggedIn) return
try {
let formattedConversationID = conversationID
if (!conversationID.startsWith('GROUP')) {
formattedConversationID = `GROUP${conversationID}`
}
this.tim.setMessageRead({
conversationID: formattedConversationID
}).then(() => {
this.triggerCallback('onConversationListUpdated', {
conversationID: formattedConversationID,
unreadCount: 0
})
}).catch(error => {
console.error('标记会话已读失败:', error)
})
} catch (error) {
console.error('标记会话已读异常:', error)
}
}
// 更新会话列表
updateConversationListOnNewMessage(message) {
try {
const conversationID = message.conversationID || this.currentConversationID
if (!conversationID) return
const conversationUpdate = {
conversationID: conversationID,
lastMessage: this.formatLastMessage(message),
lastMessageTime: message.lastTime,
unreadCount: 1,
messageType: message.type,
messageFlow: message.flow
}
this.triggerCallback('onConversationListUpdated', {
reason: 'NEW_MESSAGE_RECEIVED',
conversation: conversationUpdate,
message: message
})
} catch (error) {
console.error('更新会话列表失败:', error)
}
}
// 设置回调函数
setCallback(event, callback) {
this.callbacks[event] = callback
}
// 触发回调
triggerCallback(event, data) {
if (this.callbacks[event]) {
this.callbacks[event](data)
}
}
// 清理资源(优化版:更完整的清理)
destroy() {
console.log('=== 开始清理IM资源 ===')
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 清理所有定时器(使用统一方法)
this.clearAllTimers()
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 清理消息缓存(已弃用)
console.log('缓存功能已移除')
// 清理TIM实例
if (this.tim) {
try {
// 移除事件监听器
this.removeEventListeners()
// 退出登录(同步处理,不等待结果)
if (typeof this.tim.logout === 'function') {
this.tim.logout()
.then(() => console.log('✓ TIM登出成功'))
.catch(error => console.warn('TIM登出失败:', error))
}
} catch (error) {
console.error('清理TIM时出错:', error)
}
}
// 重置所有状态(使用统一方法)
this.resetAllStates()
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
// 清理其他状态
this.messageList = []
this.conversation = null
this.currentUserID = ''
this.currentUserSig = ''
this.currentConversationID = null
this.tim = null
2026-01-22 15:13:26 +08:00
2026-01-20 13:21:50 +08:00
console.log('=== IM资源清理完成 ===')
}
}
// 创建全局IM管理器实例
const globalTimChatManager = new TimChatManager()
// 全局IM初始化函数 - 支持强制重新初始化
let initPromise = null;
const initGlobalTIM = async (userID, forceReinit = false) => {
console.log('开始初始化全局IMuserID:', userID, 'forceReinit:', forceReinit)
if (globalTimChatManager.isInitializing) {
console.log('全局IM正在初始化中跳过重复初始化')
return true
}
if (forceReinit && globalTimChatManager.tim) {
console.log('强制重新初始化清理现有IM状态')
globalTimChatManager.isLoggedIn = false
globalTimChatManager.lastLoginTime = 0
globalTimChatManager.reconnectAttempts = 0
if (globalTimChatManager.tim.isLoggedIn) {
await globalTimChatManager.tim.logout()
console.log('强制重新初始化TIM登出成功')
}
await globalTimChatManager.initTIM(userID)
console.log('强制重新初始化完成')
return true
}
if (!forceReinit && globalTimChatManager.tim && globalTimChatManager.isLoggedIn) {
console.log('全局IM已经初始化并登录跳过重复初始化')
return true
}
await globalTimChatManager.initTIM(userID)
console.log('全局IM初始化成功')
return true
}
const getInitIMPromise = async (userID, forceReinit) => {
if (initPromise) return initPromise;
initPromise = initGlobalTIM(userID, forceReinit);
2026-01-22 15:13:26 +08:00
return initPromise;
2026-01-20 13:21:50 +08:00
}
const clearInitIMPromise = () => {
initPromise = null;
}
// 获取群聊列表(全局函数)
const getGroupList = async () => {
return await globalTimChatManager.getGroupList()
}
// 进入群聊
2026-01-22 15:13:26 +08:00
const enterChatGroupRoom = async (chatGroup, navigateType = 'navigateTo', viewType) => {
2026-01-20 13:21:50 +08:00
const groupListResult = await globalTimChatManager.getGroupList();
const groupList = groupListResult && Array.isArray(groupListResult.groupList) ? groupListResult.groupList : [];
let group = groupList.find(g => g.groupID === chatGroup.groupID);
const navigateFn = ['navigateTo', 'redirectTo', 'reLaunch'].includes(navigateType) ? navigateType : 'navigateTo'
uni[navigateFn]({
2026-01-22 15:13:26 +08:00
url: `/pages-im/IM/index?conversationID=GROUP${chatGroup.groupID}&groupID=${chatGroup.groupID}&conversationType=GROUP&viewType=${viewType}`,
2026-01-20 13:21:50 +08:00
})
}
// 检查全局IM状态
const checkGlobalIMStatus = () => {
return globalTimChatManager.isLoggedIn
}
// 清理指定会话的缓存
const clearConversationCache = (conversationID) => {
return globalTimChatManager.clearConversationCache(conversationID)
}
// 清理所有消息缓存
const clearAllMessageCache = () => {
return globalTimChatManager.clearAllCache()
}
// 确保全局IM连接
const ensureGlobalIMConnection = async () => {
return await globalTimChatManager.ensureIMConnection()
}
// 获取全局IM登录状态
const getGlobalIMLoginStatus = () => {
return globalTimChatManager.isLoggedIn
}
// 设置全局IM回调
const setGlobalIMCallback = (event, callback) => {
globalTimChatManager.setCallback(event, callback)
}
// 导出
export {
TimChatManager,
globalTimChatManager,
initGlobalTIM,
getInitIMPromise,
clearInitIMPromise,
enterChatGroupRoom,
checkGlobalIMStatus,
ensureGlobalIMConnection,
getGlobalIMLoginStatus,
setGlobalIMCallback,
getGroupList,
clearConversationCache,
clearAllMessageCache
}
export default globalTimChatManager