diff --git a/.env.development b/.env.development index c35990f..d8e7453 100644 --- a/.env.development +++ b/.env.development @@ -1,4 +1,6 @@ MP_API_BASE_URL=http://localhost:8080 +MP_IMAGE_URL=https://patient.youcan365.com MP_CACHE_PREFIX=development MP_WX_APP_ID=wx93af55767423938e MP_CORP_ID=wwe3fb2faa52cf9dfb +MP_TIM_SDK_APP_ID=1600123876 diff --git a/.env.localhost b/.env.localhost index 2281248..f379283 100644 --- a/.env.localhost +++ b/.env.localhost @@ -1,4 +1,5 @@ MP_API_BASE_URL=http://192.168.60.2:8080 MP_CACHE_PREFIX=development MP_WX_APP_ID=wx93af55767423938e -MP_CORP_ID=wwe3fb2faa52cf9dfb \ No newline at end of file +MP_CORP_ID=wwe3fb2faa52cf9dfb +MP_TIM_SDK_APP_ID=1600072268 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5480842 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "kiroAgent.configureMCP": "Disabled" +} \ No newline at end of file diff --git a/App.vue b/App.vue index 4215f26..0ea7c2d 100644 --- a/App.vue +++ b/App.vue @@ -1,5 +1,7 @@ diff --git a/index.html b/index.html index b5d330d..9e3eac7 100644 --- a/index.html +++ b/index.html @@ -1,20 +1,21 @@ - - - - - - - - -
- - - + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/pages.json b/pages.json index bed9914..ed78caf 100644 --- a/pages.json +++ b/pages.json @@ -3,8 +3,14 @@ { "path": "pages/message/message", "style": { - "navigationBarTitleText": "消息", - "navigationStyle": "custom" + "navigationBarTitleText": "消息" + } + }, + { + "path": "pages/message/index", + "style": { + "navigationBarTitleText": "聊天", + "enablePullDownRefresh": false } }, { diff --git a/pages/message/chat.scss b/pages/message/chat.scss index 15fdf23..a885920 100644 --- a/pages/message/chat.scss +++ b/pages/message/chat.scss @@ -1,4 +1,9 @@ - +// SCSS 变量定义 +$font-size-text: 28rpx; +$font-size-tip: 24rpx; +$font-size-title: 32rpx; +$text-color-sub: #999; +$primary-color: #0877F1; .chat-page { position: fixed; @@ -115,7 +120,6 @@ } .system-message { - // background-color: #f0f0f0; border-radius: 16rpx; padding: 12rpx; margin: 20rpx 24rpx; @@ -136,7 +140,7 @@ } .message-item { - margin-bottom: 16rpx; + margin-bottom: 30rpx; } .message-content { @@ -157,14 +161,14 @@ width: 60rpx; height: 60rpx; border-radius: 50%; - margin-top: 28rpx; // 向下移动与气泡箭头对齐 + margin-top: 10rpx; } .user-msg-avatar { width: 60rpx; height: 60rpx; border-radius: 50%; - margin-top: 28rpx; // 向下移动与气泡箭头对齐 + margin-top: 10rpx; } // 消息气泡容器 @@ -1035,6 +1039,19 @@ object-fit: cover; /* 保持图片比例,裁剪多余部分 */ } +/* 图片消息气泡 - 无背景色 */ +.image-bubble { + background: transparent !important; + padding: 0 !important; + border-radius: 0 !important; +} + +/* 移除图片消息气泡的小三角 */ +.image-bubble::before, +.image-bubble::after { + display: none !important; +} + .message-right .message-card { margin-right: 8rpx; } @@ -1231,5 +1248,4 @@ @keyframes bounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-10rpx); } -} - +} \ No newline at end of file diff --git a/pages/message/components/chat-input.vue b/pages/message/components/chat-input.vue index 851e5b8..5184f6f 100644 --- a/pages/message/components/chat-input.vue +++ b/pages/message/components/chat-input.vue @@ -53,7 +53,7 @@ diff --git a/pages/message/components/consultation-bar.vue b/pages/message/components/consultation-bar.vue deleted file mode 100644 index 6c73414..0000000 --- a/pages/message/components/consultation-bar.vue +++ /dev/null @@ -1,43 +0,0 @@ - - - - - \ No newline at end of file diff --git a/pages/message/components/head-card.vue b/pages/message/components/head-card.vue index f069f85..568b64e 100644 --- a/pages/message/components/head-card.vue +++ b/pages/message/components/head-card.vue @@ -12,7 +12,6 @@ 等待医生接诊..... - @@ -46,7 +45,7 @@ const props = defineProps({ }) const hasFilledDescription = computed(() => props.order && ('description' in props.order)); -const avatar = computed(() => props.doctorInfo?.avatar || '/static/home/doctor.png') +const avatar = computed(() => props.doctorInfo?.avatar || '/static/home/avatar.svg') function addSymptomDescription() { if (!hasFilledDescription.value) { diff --git a/pages/message/components/message-types.vue b/pages/message/components/message-types.vue index 6794062..4d358fe 100644 --- a/pages/message/components/message-types.vue +++ b/pages/message/components/message-types.vue @@ -63,7 +63,7 @@ - diff --git a/pages/message/hooks/use-group-chat.js b/pages/message/hooks/use-group-chat.js new file mode 100644 index 0000000..e550907 --- /dev/null +++ b/pages/message/hooks/use-group-chat.js @@ -0,0 +1,62 @@ +import { ref, computed } from 'vue' +import { onShow, onUnload } from '@dcloudio/uni-app' + +/** + * 简单的群聊hook + * @param {string} groupID 群组ID + */ +export default function useGroupChat(groupID) { + const groupInfo = ref({}) + const members = ref([]) + + // 群聊成员映射 + const chatMember = computed(() => { + const res = {} + members.value.forEach(member => { + res[member.id] = { + name: member.name, + avatar: member.avatar || '/static/default-avatar.png' + } + }) + return res + }) + + // 获取群聊信息 + async function getGroupInfo() { + const gid = typeof groupID === 'string' ? groupID : groupID.value + if (!gid) return + + try { + // 这里可以调用API获取群聊信息 + // const res = await getGroupDetail(gid) + // if (res && res.success) { + // groupInfo.value = res.data + // members.value = res.data.members || [] + // } + + // 暂时使用本地数据 + groupInfo.value = { + groupID: gid, + name: '群聊', + status: 'active' + } + } catch (error) { + console.error('获取群聊信息失败:', error) + } + } + + onShow(() => { + getGroupInfo() + }) + + onUnload(() => { + // 清理资源 + }) + + return { + groupInfo, + members, + chatMember, + getGroupInfo + } +} diff --git a/pages/message/index.vue b/pages/message/index.vue index 1fc6e9e..19d0432 100644 --- a/pages/message/index.vue +++ b/pages/message/index.vue @@ -1,993 +1,607 @@ - - - - - - + + + + + diff --git a/pages/message/message.vue b/pages/message/message.vue index fb6b3e7..0cacc4d 100644 --- a/pages/message/message.vue +++ b/pages/message/message.vue @@ -1,9 +1,535 @@ - - - - \ No newline at end of file + + diff --git a/static/home/avatar.svg b/static/home/avatar.svg new file mode 100644 index 0000000..21ec70f --- /dev/null +++ b/static/home/avatar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/icon/changyongyu.png b/static/icon/changyongyu.png new file mode 100644 index 0000000..7e6de39 Binary files /dev/null and b/static/icon/changyongyu.png differ diff --git a/static/icon/fuzhenyuyue.png b/static/icon/fuzhenyuyue.png new file mode 100644 index 0000000..8609748 Binary files /dev/null and b/static/icon/fuzhenyuyue.png differ diff --git a/static/icon/icon-chinese-rx.png b/static/icon/icon-chinese-rx.png new file mode 100644 index 0000000..10ee891 Binary files /dev/null and b/static/icon/icon-chinese-rx.png differ diff --git a/static/icon/icon-western-rx.png b/static/icon/icon-western-rx.png new file mode 100644 index 0000000..00cb9a8 Binary files /dev/null and b/static/icon/icon-western-rx.png differ diff --git a/static/icon/jieshuzixun.png b/static/icon/jieshuzixun.png new file mode 100644 index 0000000..1e36ede Binary files /dev/null and b/static/icon/jieshuzixun.png differ diff --git a/static/icon/kaichufang.png b/static/icon/kaichufang.png new file mode 100644 index 0000000..647f267 Binary files /dev/null and b/static/icon/kaichufang.png differ diff --git a/static/icon/kaiyizhu.png b/static/icon/kaiyizhu.png new file mode 100644 index 0000000..e47bc0c Binary files /dev/null and b/static/icon/kaiyizhu.png differ diff --git a/static/icon/kaizhongyao.png b/static/icon/kaizhongyao.png new file mode 100644 index 0000000..f9d649e Binary files /dev/null and b/static/icon/kaizhongyao.png differ diff --git a/static/icon/paizhao.png b/static/icon/paizhao.png new file mode 100644 index 0000000..170af2a Binary files /dev/null and b/static/icon/paizhao.png differ diff --git a/static/icon/quxiaobingtuikuan.png b/static/icon/quxiaobingtuikuan.png new file mode 100644 index 0000000..97c24a3 Binary files /dev/null and b/static/icon/quxiaobingtuikuan.png differ diff --git a/static/icon/xuanjiaowenzhang.png b/static/icon/xuanjiaowenzhang.png new file mode 100644 index 0000000..dffdb37 Binary files /dev/null and b/static/icon/xuanjiaowenzhang.png differ diff --git a/static/icon/zhaopian.png b/static/icon/zhaopian.png new file mode 100644 index 0000000..d31c0de Binary files /dev/null and b/static/icon/zhaopian.png differ diff --git a/static/icon/zhenliaoyijian.png b/static/icon/zhenliaoyijian.png new file mode 100644 index 0000000..08851f4 Binary files /dev/null and b/static/icon/zhenliaoyijian.png differ diff --git a/store/account.js b/store/account.js index 6691911..a4ce42f 100644 --- a/store/account.js +++ b/store/account.js @@ -2,7 +2,7 @@ import { ref } from "vue"; import { defineStore } from "pinia"; import api from '@/utils/api'; import { toast } from '@/utils/widget'; -import { getInitIMPromise, clearInitIMPromise } from "@/utils/tim-chat.js"; +import { initGlobalTIM, globalTimChatManager } from "@/utils/tim-chat.js"; const env = __VITE_ENV__; @@ -14,6 +14,7 @@ export default defineStore("accountStore", () => { const loginPromise = ref(null); // IM 相关 const openid = ref(""); + const isIMInitialized = ref(false); // 医生信息 const doctorInfo = ref(null); @@ -49,6 +50,16 @@ export default defineStore("accountStore", () => { } account.value = res.data; openid.value = res.data.openid; + // 登录成功后初始化腾讯IM + try { + console.log('开始初始化腾讯IM,userID:', res.data.openid); + await initGlobalTIM(res.data.openid); + isIMInitialized.value = true; + console.log('腾讯IM初始化成功'); + } catch (imError) { + console.error('腾讯IM初始化失败:', imError); + // IM初始化失败不影响登录流程 + } await getDoctorInfo(openid.value); return res.data } @@ -74,5 +85,39 @@ export default defineStore("accountStore", () => { } } - return { account, openid, doctorInfo, login, getDoctorInfo } + async function initIMAfterLogin(userID) { + if (isIMInitialized.value) { + return true; + } + try { + await initGlobalTIM(userID); + isIMInitialized.value = true; + return true; + } catch (error) { + console.error('IM初始化失败:', error); + return false; + } + } + + // 退出登录 + async function logout() { + try { + // 退出腾讯IM + if (globalTimChatManager && globalTimChatManager.tim) { + console.log('开始退出腾讯IM'); + await globalTimChatManager.destroy(); + console.log('腾讯IM退出成功'); + } + } catch (error) { + console.error('退出腾讯IM失败:', error); + } + + // 清空账户信息 + account.value = null; + openid.value = ""; + isIMInitialized.value = false; + doctorInfo.value = null; + } + + return { account, openid, isIMInitialized, doctorInfo, login, getDoctorInfo, initIMAfterLogin, logout } }) \ No newline at end of file diff --git a/utils/api.js b/utils/api.js index 04c937f..0807950 100644 --- a/utils/api.js +++ b/utils/api.js @@ -30,6 +30,11 @@ const urlsConfig = { }, wecom: { addContactWay: 'addContactWay' + }, + im: { + getUserSig: 'getUserSig', + sendSystemMessage: "sendSystemMessage", + getChatRecordsByGroupId: "getChatRecordsByGroupId" } } diff --git a/utils/chat-utils.js b/utils/chat-utils.js new file mode 100644 index 0000000..c2d5231 --- /dev/null +++ b/utils/chat-utils.js @@ -0,0 +1,798 @@ +/** + * 聊天相关工具函数 + */ + +// 通用消息提示 +export const showMessage = (title, icon = 'none') => { + uni.showToast({ + title, + icon, + }); +}; + +// 检查问诊状态 +export const checkConsultationStatus = (waitingForDoctor, consultationEnded) => { + if (waitingForDoctor) { + showMessage("等待医生接诊中,无法发送消息"); + return false; + } + + if (consultationEnded) { + showMessage("问诊已结束,无法发送消息"); + return false; + } + + return true; +}; + +// 检查IM连接状态 +export const checkIMConnection = (timChatManager) => { + if (!timChatManager.tim || !timChatManager.isLoggedIn) { + // showMessage("IM连接异常,请重新进入"); + return false; + } + return true; +}; + +// 发送消息前的通用验证 +export const validateBeforeSend = (waitingForDoctor, consultationEnded, timChatManager) => { + if (!checkConsultationStatus(waitingForDoctor, consultationEnded)) { + return false; + } + + if (!checkIMConnection(timChatManager)) { + return false; + } + + return true; +}; + +// 获取语音文件URL +export const getVoiceUrl = (message) => { + let voiceUrl = ''; + if (message.payload && message.payload.url) { + voiceUrl = message.payload.url; + } else if (message.payload && message.payload.file) { + voiceUrl = message.payload.file; + } else if (message.payload && message.payload.tempFilePath) { + voiceUrl = message.payload.tempFilePath; + } else if (message.payload && message.payload.filePath) { + voiceUrl = message.payload.filePath; + } + return voiceUrl; +}; + +// 验证语音URL格式 +export const validateVoiceUrl = (voiceUrl) => { + if (!voiceUrl) { + console.error('语音文件URL不存在'); + showMessage('语音文件不存在'); + return false; + } + + if (!voiceUrl.startsWith('http') && !voiceUrl.startsWith('wxfile://') && !voiceUrl.startsWith('/')) { + console.error('语音文件URL格式不正确:', voiceUrl); + showMessage('语音文件格式错误'); + return false; + } + + return true; +}; + +// 创建音频上下文 +export const createAudioContext = (voiceUrl) => { + const audioContext = uni.createInnerAudioContext(); + audioContext.src = voiceUrl; + + audioContext.onPlay(() => { + console.log('语音开始播放'); + }); + + audioContext.onEnded(() => { + console.log('语音播放结束'); + }); + + audioContext.onError((err) => { + console.error('语音播放失败:', err); + console.error('错误详情:', { + errMsg: err.errMsg, + errno: err.errno, + src: voiceUrl + }); + showMessage('语音播放失败'); + }); + + return audioContext; +}; + +// ==================== 时间相关工具方法 ==================== + +/** + * 验证时间戳格式 + * @param {number|string} timestamp - 时间戳 + * @returns {boolean} 是否为有效时间戳 + */ +export const validateTimestamp = (timestamp) => { + if (!timestamp) return false; + + const num = Number(timestamp); + if (isNaN(num)) return false; + + // 检查是否为有效的时间戳范围(1970年到2100年) + const minTimestamp = 0; + const maxTimestamp = 4102444800000; // 2100年1月1日 + + return num >= minTimestamp && num <= maxTimestamp; +}; + +/** + * 格式化时间 - 今天/昨天显示文字,其他显示日期 + 空格 + 24小时制时间 + * @param {number|string} timestamp - 时间戳 + * @returns {string} 格式化后的时间字符串 + */ +export const formatTime = (timestamp) => { + // 验证时间戳 + if (!validateTimestamp(timestamp)) { + return "未知时间"; + } + + // 确保时间戳是毫秒级 + let timeInMs = timestamp; + if (timestamp < 1000000000000) { + // 如果时间戳小于这个值,可能是秒级时间戳 + timeInMs = timestamp * 1000; + } + + const date = new Date(timeInMs); + const now = new Date(); + + // 验证日期是否有效 + if (isNaN(date.getTime())) { + return "未知时间"; + } + + // 格式化时间:HH:MM (24小时制) + const hours = String(date.getHours()).padStart(2, "0"); + const minutes = String(date.getMinutes()).padStart(2, "0"); + const timeStr = `${hours}:${minutes}`; + + // 检查是否是今天 + if (date.toDateString() === now.toDateString()) { + return `${timeStr}`; + } + + // 检查是否是昨天 + const yesterday = new Date(now); + yesterday.setDate(yesterday.getDate() - 1); + if (date.toDateString() === yesterday.toDateString()) { + return `昨天 ${timeStr}`; + } + + // 其他日期显示完整日期 + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + const dateStr = `${month}/${day}`; + + return `${dateStr} ${timeStr}`; +}; + +/** + * 计算时间差 + * @param {number|string} startTime - 开始时间戳 + * @param {number|string} endTime - 结束时间戳 + * @returns {object} 包含天、小时、分钟、秒的时间差对象 + */ +export const calculateTimeDiff = (startTime, endTime) => { + if (!validateTimestamp(startTime) || !validateTimestamp(endTime)) { + return { days: 0, hours: 0, minutes: 0, seconds: 0 }; + } + + let startMs = startTime; + let endMs = endTime; + + if (startTime < 1000000000000) startMs = startTime * 1000; + if (endTime < 1000000000000) endMs = endTime * 1000; + + const diffMs = Math.abs(endMs - startMs); + + const days = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((diffMs % (1000 * 60)) / 1000); + + return { days, hours, minutes, seconds }; +}; + +/** + * 格式化倒计时 + * @param {number|string} endTime - 结束时间戳 + * @param {number|string} currentTime - 当前时间戳(可选,默认使用当前时间) + * @returns {string} 格式化后的倒计时字符串 + */ +export const formatCountdown = (endTime, currentTime = Date.now()) => { + const diff = calculateTimeDiff(currentTime, endTime); + + if (diff.days > 0) { + return `${diff.days}天${diff.hours}时${diff.minutes}分`; + } else if (diff.hours > 0) { + return `${diff.hours}时${diff.minutes}分${diff.seconds}秒`; + } else if (diff.minutes > 0) { + return `${diff.minutes}分${diff.seconds}秒`; + } else { + return `${diff.seconds}秒`; + } +}; + +// ==================== 媒体选择相关工具方法 ==================== + +/** + * 检查并请求相册权限 + * @returns {Promise} 是否有权限 + */ +const checkAlbumPermission = () => { + return new Promise((resolve) => { + uni.getSetting({ + success: (res) => { + const authStatus = res.authSetting['scope.album']; + + if (authStatus === undefined) { + // 未授权过,会自动弹出授权窗口 + resolve(true); + } else if (authStatus === false) { + // 已拒绝授权,需要引导用户手动开启 + uni.showModal({ + title: '需要相册权限', + content: '请在设置中开启相册权限,以便选择图片', + confirmText: '去设置', + success: (modalRes) => { + if (modalRes.confirm) { + uni.openSetting({ + success: (settingRes) => { + if (settingRes.authSetting['scope.album']) { + resolve(true); + } else { + resolve(false); + } + }, + fail: () => { + resolve(false); + } + }); + } else { + resolve(false); + } + }, + fail: () => { + resolve(false); + } + }); + } else { + // 已授权 + resolve(true); + } + }, + fail: () => { + // 获取设置失败,尝试直接调用 + resolve(true); + } + }); + }); +}; + +/** + * 选择媒体文件 + * @param {object} options - 选择选项 + * @param {function} onSuccess - 成功回调 + * @param {function} onFail - 失败回调 + */ +export const chooseMedia = async (options, onSuccess, onFail) => { + // 如果需要从相册选择,先检查权限 + const sourceType = options.sourceType || ['album', 'camera']; + if (sourceType.includes('album')) { + const hasPermission = await checkAlbumPermission(); + if (!hasPermission) { + console.log('用户未授予相册权限'); + if (onFail) { + onFail({ errMsg: '未授权相册权限' }); + } + return; + } + } + + uni.chooseMedia({ + count: options.count || 1, + mediaType: options.mediaType || ['image'], + sizeType: options.sizeType || ['original', 'compressed'], + sourceType: sourceType, + success: function (res) { + console.log('选择媒体成功:', res); + if (onSuccess) onSuccess(res); + }, + fail: function (err) { + // 用户取消选择 + if (err.errMsg.includes('cancel')) { + console.log('用户取消选择'); + return; + } + + // 权限相关错误 + if (err.errMsg.includes('permission') || err.errMsg.includes('auth') || err.errMsg.includes('拒绝')) { + console.error('相册权限被拒绝:', err); + uni.showModal({ + title: '需要相册权限', + content: '请在设置中开启相册权限后重试', + confirmText: '去设置', + success: (modalRes) => { + if (modalRes.confirm) { + uni.openSetting(); + } + } + }); + if (onFail) { + onFail(err); + } + return; + } + + // 其他错误 + console.error('选择媒体失败:', err); + if (onFail) { + onFail(err); + } else { + showMessage('选择图片失败,请重试'); + } + } + }); +}; + +/** + * 选择图片 + * @param {function} onSuccess - 成功回调 + * @param {function} onFail - 失败回调 + */ +export const chooseImage = (onSuccess, onFail) => { + chooseMedia({ + count: 1, + mediaType: ['image'], + sizeType: ['original', 'compressed'], + sourceType: ['album', 'camera'] + }, onSuccess, onFail); +}; + +/** + * 拍照 + * @param {function} onSuccess - 成功回调 + * @param {function} onFail - 失败回调 + */ +export const takePhoto = (onSuccess, onFail) => { + chooseMedia({ + count: 1, + mediaType: ['image'], + sizeType: ['original', 'compressed'], + sourceType: ['camera'] + }, onSuccess, onFail); +}; + +// ==================== 录音相关工具方法 ==================== + +/** + * 初始化录音管理器 + * @param {object} options - 录音选项 + * @param {function} onStop - 录音结束回调 + * @param {function} onError - 录音错误回调 + * @returns {object} 录音管理器实例 + */ +export const initRecorderManager = (options = {}, onStop, onError) => { + const recorderManager = wx.getRecorderManager(); + + // 监听录音结束事件 + recorderManager.onStop((res) => { + console.log('录音成功,结果:', res); + if (onStop) onStop(res); + }); + + // 监听录音错误事件 + recorderManager.onError((err) => { + console.error('录音失败:', err); + if (onError) { + onError(err); + } else { + showMessage("录音失败"); + } + }); + + return recorderManager; +}; + +/** + * 开始录音 + * @param {object} recorderManager - 录音管理器 + * @param {object} options - 录音参数 + */ +export const startRecord = (recorderManager, options = {}) => { + if (!recorderManager) { + console.error('录音管理器未初始化'); + return; + } + + const recordOptions = { + duration: 60000, // 录音的时长,单位 ms,最大值 600000(10 分钟) + sampleRate: 44100, // 采样率 + numberOfChannels: 1, // 录音通道数 + encodeBitRate: 192000, // 编码码率 + format: 'aac', // 音频格式 + ...options + }; + + recorderManager.start(recordOptions); +}; + +/** + * 停止录音 + * @param {object} recorderManager - 录音管理器 + */ +export const stopRecord = (recorderManager) => { + if (!recorderManager) { + console.error('录音管理器未初始化'); + return; + } + recorderManager.stop(); +}; + +// ==================== 消息发送相关工具方法 ==================== + +/** + * 创建自定义消息 + * @param {string} messageType - 消息类型 + * @param {object} data - 消息数据 + * @param {function} formatTime - 时间格式化函数 + * @returns {object} 自定义消息对象 + */ +export const createCustomMessage = (messageType, data, formatTime) => { + return { + messageType, + time: formatTime(Date.now()), + ...data + }; +}; + +/** + * 发送自定义消息的通用方法 + * @param {object} messageData - 消息数据 + * @param {object} timChatManager - IM管理器 + * @param {function} validateBeforeSend - 发送前验证函数 + * @param {function} onSuccess - 成功回调 + */ +export const sendCustomMessage = async (messageData, timChatManager, validateBeforeSend, onSuccess) => { + if (!validateBeforeSend()) { + return; + } + + const result = await timChatManager.sendCustomMessage(messageData); + + if (result && result.success) { + if (onSuccess) onSuccess(); + } else { + console.error('发送自定义消息失败:', result?.error); + } +}; + +/** + * 发送消息的通用方法 + * @param {string} messageType - 消息类型 + * @param {any} data - 消息数据 + * @param {object} timChatManager - IM管理器 + * @param {function} validateBeforeSend - 发送前验证函数 + * @param {function} onSuccess - 成功回调 + */ +export const sendMessage = async (messageType, data, timChatManager, validateBeforeSend, onSuccess, cloudCustomData) => { + if (!validateBeforeSend()) { + return; + } + + let result; + + switch (messageType) { + case 'text': + result = await timChatManager.sendTextMessage(data, cloudCustomData); + break; + case 'image': + result = await timChatManager.sendImageMessage(data, cloudCustomData); + break; + case 'voice': + result = await timChatManager.sendVoiceMessage(data.file, data.duration,cloudCustomData); + break; + default: + console.error('未知的消息类型:', messageType); + return; + } + + if (result && result.success) { + if (onSuccess) onSuccess(); + } else { + console.error('发送消息失败:', result?.error); + showMessage('发送失败,请重试'); + } +}; + +// ==================== 状态检查相关工具方法 ==================== + +/** + * 检查IM连接状态 + * @param {object} timChatManager - IM管理器 + * @param {function} onError - 错误回调 + * @returns {boolean} 连接状态 + */ +export const checkIMConnectionStatus = (timChatManager, onError) => { + if (!timChatManager.tim || !timChatManager.isLoggedIn) { + const errorMsg = "IM连接异常,请重新进入"; + if (onError) { + onError(errorMsg); + } else { + showMessage(errorMsg); + } + return false; + } + return true; +}; + +/** + * 检查是否显示时间分割线 + * @param {object} message - 当前消息 + * @param {number} index - 消息索引 + * @param {Array} messageList - 消息列表 + * @returns {boolean} 是否显示时间分割线 + */ +export const shouldShowTime = (message, index, messageList) => { + if (index === 0) return true; + + const prevMessage = messageList[index - 1]; + + // 使用工具函数验证时间戳 + if (!validateTimestamp(message.lastTime) || !validateTimestamp(prevMessage.lastTime)) { + return false; + } + + const timeDiff = message.lastTime - prevMessage.lastTime; + + return timeDiff > 5 * 60 * 1000; // 5分钟显示一次时间 +}; + +/** + * 预览图片 + * @param {string} url - 图片URL + */ +export const previewImage = (url) => { + uni.previewImage({ + urls: [url], + current: url, + }); +}; + +// ==================== 录音相关工具方法 ==================== + +/** + * 检查录音时长并处理 + * @param {object} res - 录音结果 + * @param {Function} onTimeTooShort - 时间太短的回调 + * @returns {boolean} 录音时长是否有效 + */ +export const checkRecordingDuration = (res, onTimeTooShort = null) => { + const duration = Math.floor(res.duration / 1000); + if (duration < 1) { + console.log('录音时间太短,取消发送'); + if (onTimeTooShort) { + onTimeTooShort(); + } else { + showMessage('说话时间太短'); + } + return false; + } + return true; +}; + +// ==================== 防抖和节流工具 ==================== + +/** + * 防抖函数 + * @param {Function} func - 要防抖的函数 + * @param {number} wait - 等待时间(毫秒) + * @returns {Function} 防抖后的函数 + */ +export const debounce = (func, wait = 300) => { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +}; + +/** + * 节流函数 + * @param {Function} func - 要节流的函数 + * @param {number} limit - 限制时间(毫秒) + * @returns {Function} 节流后的函数 + */ +export const throttle = (func, limit = 300) => { + let inThrottle; + return function executedFunction(...args) { + if (!inThrottle) { + func.apply(this, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; +}; + +// ==================== 自定义消息解析相关工具方法 ==================== + +// 自定义消息解析缓存 +const customMessageCache = new Map(); + +/** + * 解析自定义消息(带缓存) + * @param {object} message - 消息对象 + * @param {function} formatTime - 时间格式化函数 + * @returns {object} 解析后的消息对象 + */ +export const parseCustomMessage = (message, formatTime) => { + // 使用消息ID作为缓存键 + const cacheKey = message.ID; + + // 检查缓存 + if (customMessageCache.has(cacheKey)) { + return customMessageCache.get(cacheKey); + } + + try { + const customData = JSON.parse(message.payload.data); + const parsedMessage = { + messageType: customData.messageType, + content: customData.content, + symptomContent: customData.symptomContent, + hasVisitedHospital: customData.hasVisitedHospital, + selectedDiseases: customData.selectedDiseases, + images: customData.images, + medicines: customData.medicines, + diagnosis: customData.diagnosis, + prescriptionType: customData.prescriptionType, + prescriptionDesc: customData.prescriptionDesc, + tcmPrescription: customData.tcmPrescription, // 新增中药处方字段 + patientName: customData.patientName, + gender: customData.gender, + age: customData.age, + surveyTitle: customData.surveyTitle, + surveyDescription: customData.surveyDescription, + surveyName: customData.surveyName, + estimatedTime: customData.estimatedTime, + reward: customData.reward, + note: customData.note, + orderId: customData.orderId, // 新增订单ID字段 + timestamp: customData.timestamp, // 新增时间戳字段 + conversationID: message.conversationID, // 保留conversationID + time: formatTime(message.lastTime), + }; + + // 缓存解析结果 + customMessageCache.set(cacheKey, parsedMessage); + return parsedMessage; + } catch (error) { + const fallbackMessage = { + messageType: "unknown", + content: "未知消息类型", + }; + + // 缓存错误结果,避免重复解析 + customMessageCache.set(cacheKey, fallbackMessage); + return fallbackMessage; + } +}; + +/** + * 清理消息缓存 + */ +export const clearMessageCache = () => { + customMessageCache.clear(); +}; + +/** + * 获取解析后的自定义消息(带缓存) + * @param {object} message - 消息对象 + * @param {function} formatTime - 时间格式化函数 + * @returns {object} 解析后的消息对象 + */ +export const getParsedCustomMessage = (message, formatTime) => { + return parseCustomMessage(message, formatTime); +}; + +/** + * 处理查看详情 + * @param {object} message - 解析后的消息对象 + * @param {object} patientInfo - 患者信息 + */ +export const handleViewDetail = (message, patientInfo) => { + if (message.messageType === "symptom") { + uni.showModal({ + title: "完整病情描述", + content: message.symptomContent, + showCancel: false, + confirmText: "知道了", + }); + } else if (message.messageType === "prescription") { + // 处理处方单详情查看 + let content = `患者:${patientInfo.name}\n诊断:${message.diagnosis || '无'}\n\n`; + + if (message.prescriptionType === '中药处方' && message.tcmPrescription) { + content += `处方类型:中药处方\n处方详情:${message.tcmPrescription.description}\n`; + if (message.tcmPrescription.usage) { + content += `用法用量:${message.tcmPrescription.usage}\n`; + } + } else if (message.prescriptionType === '西药处方' && message.medicines) { + content += `处方类型:西药处方\n药品清单:\n`; + const medicineDetails = message.medicines + .map((med) => `${med.name} ${med.spec} ×${med.count}`) + .join("\n"); + content += medicineDetails + "\n"; + + // 添加用法用量 + const usageDetails = message.medicines + .filter(med => med.usage) + .map(med => `${med.name}:${med.usage}`) + .join("\n"); + if (usageDetails) { + content += `\n用法用量:\n${usageDetails}\n`; + } + } + + content += `\n开方时间:${message.time}`; + + uni.showModal({ + title: "处方详情", + content: content, + showCancel: false, + confirmText: "知道了", + }); + } else if (message.messageType === "refill") { + // 处理续方申请详情查看 + let content = `患者:${message.patientName} ${message.gender} ${message.age}岁\n诊断:${message.diagnosis}\n\n`; + + if (message.prescriptionType === "中药处方") { + content += `处方类型:${message.prescriptionType}\n处方详情:${message.prescriptionDesc}`; + } else { + const medicineDetails = message.medicines + .map((med) => `${med.name} ${med.spec} ${med.count}\n${med.usage}`) + .join("\n\n"); + content += `药品清单:\n${medicineDetails}`; + } + + uni.showModal({ + title: "续方申请详情", + content: content, + showCancel: false, + confirmText: "知道了", + }); + } else if (message.messageType === "survey") { + // 处理问卷调查详情查看或跳转 + uni.showModal({ + title: "问卷调查", + content: `${message.surveyTitle}\n\n${message.surveyDescription + }\n\n问卷名称:${message.surveyName}\n预计用时:${message.estimatedTime}${message.reward ? "\n完成奖励:" + message.reward : "" + }${message.note ? "\n\n说明:" + message.note : ""}`, + confirmText: "去填写", + cancelText: "稍后再说", + success: (res) => { + if (res.confirm) { + // 这里可以跳转到问卷页面 + uni.showToast({ + title: "正在跳转到问卷页面", + icon: "none", + }); + } + }, + }); + } +}; \ No newline at end of file diff --git a/utils/im-status-manager.js b/utils/im-status-manager.js index 7deb31a..7fc2ee3 100644 --- a/utils/im-status-manager.js +++ b/utils/im-status-manager.js @@ -1,8 +1,7 @@ -import { - checkGlobalIMStatus, +import { + checkGlobalIMStatus, ensureGlobalIMConnection, - getGlobalIMLoginStatus, - setGlobalIMCallback + getGlobalIMLoginStatus } from './tim-chat.js' /** @@ -68,10 +67,10 @@ class IMStatusManager { try { console.log('执行IM状态检查...') const isLoggedIn = checkGlobalIMStatus() - + // 触发状态变化回调 - this.triggerCallbacks('onStatusChange', { - isLoggedIn, + this.triggerCallbacks('onStatusChange', { + isLoggedIn, checkTime: now, timestamp: new Date().toLocaleString() }) @@ -97,26 +96,26 @@ class IMStatusManager { try { console.log('开始尝试IM重连...') const success = await ensureGlobalIMConnection() - + if (success) { console.log('IM重连成功') - this.triggerCallbacks('onReconnectSuccess', { - timestamp: new Date().toLocaleString() + this.triggerCallbacks('onReconnectSuccess', { + timestamp: new Date().toLocaleString() }) } else { console.log('IM重连失败') - this.triggerCallbacks('onReconnectFailed', { - timestamp: new Date().toLocaleString() + this.triggerCallbacks('onReconnectFailed', { + timestamp: new Date().toLocaleString() }) } return success } catch (error) { console.error('IM重连异常:', error) - this.triggerCallbacks('onReconnectFailed', { + this.triggerCallbacks('onReconnectFailed', { error, - timestamp: new Date().toLocaleString() + timestamp: new Date().toLocaleString() }) return false } @@ -206,7 +205,7 @@ class IMStatusManager { report: { isLoggedIn: status.isLoggedIn ? '已登录' : '未登录', monitoring: status.isMonitoring ? '监控中' : '未监控', - lastCheck: status.lastCheckTime ? + lastCheck: status.lastCheckTime ? new Date(status.lastCheckTime).toLocaleString() : '从未检查', interval: `${status.checkInterval / 1000}秒` } diff --git a/utils/tim-chat.js b/utils/tim-chat.js index 3944300..cbd6fb7 100644 --- a/utils/tim-chat.js +++ b/utils/tim-chat.js @@ -1,13 +1,21 @@ // 引入腾讯IM SDK import TIM from 'tim-wx-sdk' import TIMUploadPlugin from 'tim-upload-plugin' -// import { getUserSig, sendSystemMessage, getChatRecordsByGroupId } from '../api/corp/im.js' +import api from './api.js' const env = __VITE_ENV__; // 腾讯IM配置 - SDKAppID 必须是 number 类型,使用 Number() 转换 const TIM_CONFIG = { - SDKAppID: Number(env.APP_TIM_SDK_APP_ID), // 患者端 IM SDKAppID + 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 + }) } // IM连接配置常量 @@ -33,38 +41,38 @@ class TimChatManager { // TIM实例和会话 this.tim = null this.conversation = null - + // 用户信息 this.currentUserID = '' this.currentUserSig = '' - + // 消息管理 this.messageList = [] this.currentConversationID = null - + // 分页加载相关状态 this.nextReqMessageID = "" this.isCompleted = false this.isLoadingMore = false - + // 状态标志 this.isLoading = false this.isLoggedIn = false this.isInitializing = false this.isLoggingIn = false - + // 定时器 this.loginCheckInterval = null this.heartbeatInterval = null this.networkReconnectTimer = null - + // 重连管理 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 - + // 心跳管理 this.heartbeatFailCount = 0 @@ -79,21 +87,21 @@ class TimChatManager { onConversationListUpdated: null } - // 绑定事件处理函数 - 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) - } + // 绑定事件处理函数 + 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) + } } // ============== 资源管理方法 ============== - + // 清理所有定时器 clearAllTimers() { if (this.loginCheckInterval) { @@ -101,25 +109,25 @@ class TimChatManager { clearInterval(this.loginCheckInterval) this.loginCheckInterval = null } - + if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval) this.heartbeatInterval = null } - + if (this.networkReconnectTimer) { clearTimeout(this.networkReconnectTimer) this.networkReconnectTimer = null } - + console.log('所有定时器已清理') } - + // 清理缓存(已弃用缓存功能) cleanupCache() { // 缓存功能已移除 } - + // 重置所有状态 resetAllStates() { this.isLoggedIn = false @@ -131,7 +139,7 @@ class TimChatManager { } // ============== 初始化方法 ============== - + // 初始化腾讯IM async initTIM(userID = null) { if (this.isInitializing) { @@ -145,7 +153,7 @@ class TimChatManager { try { // 重置重连次数,允许重新登录 this.reconnectAttempts = 0 - + // 如果存在旧的TIM实例,先完整清理 if (this.tim) { console.log('检测到旧的TIM实例,开始清理...') @@ -155,8 +163,12 @@ class TimChatManager { if (!TIM) { throw new Error('TIM SDK 未正确导入') } + // 验证 SDKAppID + if (!TIM_CONFIG.SDKAppID || isNaN(TIM_CONFIG.SDKAppID)) { + throw new Error(`TIM SDK配置错误: SDKAppID无效 (${TIM_CONFIG.SDKAppID}),请检查环境变量 MP_TIM_SDK_APP_ID`) + } - + console.log('创建TIM实例,SDKAppID:', TIM_CONFIG.SDKAppID) this.tim = TIM.create({ SDKAppID: TIM_CONFIG.SDKAppID }) // 等待TIM实例初始化完成 @@ -187,7 +199,7 @@ class TimChatManager { } catch (error) { console.error('=== IM初始化失败 ===', error) this.triggerCallback('onError', `初始化失败: ${error.message || error}`) - + // 初始化失败时清理资源 await this.cleanupOldInstance() return false @@ -195,16 +207,16 @@ class TimChatManager { this.isInitializing = false } } - + // 清理旧的TIM实例 async cleanupOldInstance() { try { // 清理所有定时器 this.clearAllTimers() - + // 移除事件监听器 this.removeEventListeners() - + // 登出 if (this.tim && this.tim.isLoggedIn) { try { @@ -214,25 +226,25 @@ class TimChatManager { console.warn('登出旧实例失败:', err) } } - + // 清理实例 this.tim = null - + // 重置状态 this.resetAllStates() - + console.log('旧实例清理完成') } catch (error) { console.error('清理旧实例时出错:', error) } } - + // 等待TIM实例准备就绪 waitForTIMInstanceReady() { return new Promise((resolve, reject) => { const startTime = Date.now() const timeout = 10000 // 10秒超时 - + const checkReady = () => { if (this.tim && typeof this.tim.on === 'function') { console.log('TIM实例已就绪') @@ -243,7 +255,7 @@ class TimChatManager { setTimeout(checkReady, IM_CONNECTION_CONFIG.TIM_INSTANCE_READY_CHECK_INTERVAL) } } - + checkReady() }) } @@ -258,7 +270,7 @@ class TimChatManager { const checkSDKReady = () => { checkCount++ const elapsed = Date.now() - startTime - + if (this.isLoggedIn) { console.log(`✓ SDK已Ready(耗时${elapsed}ms,检查${checkCount}次)`) resolve() @@ -268,7 +280,7 @@ class TimChatManager { // 超时不算致命错误,尝试继续 resolve() } else { - console.log(`等待SDK Ready... ${Math.floor(elapsed/1000)}/${Math.floor(timeout/1000)}秒`) + console.log(`等待SDK Ready... ${Math.floor(elapsed / 1000)}/${Math.floor(timeout / 1000)}秒`) setTimeout(checkSDKReady, checkInterval) } } @@ -290,17 +302,17 @@ class TimChatManager { 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 }, - { event: TIM.EVENT.KICKED_OUT, handler: this.boundEventHandlers.onKickedOut } - ] + // 逐个注册事件监听器,如果某个注册失败则继续注册其他的 + 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 } + ] events.forEach(({ event, handler }) => { if (event && handler && typeof this.tim.on === 'function') { @@ -377,9 +389,9 @@ class TimChatManager { // 获取 userSig async getUserSig(userID) { try { - const response = await getUserSig(userID) - if (response?.success && response?.data?.userSig) { - return response.data.userSig + const response = await api('getUserSig', { userId: userID }) + if (response?.success && response?.data) { + return response.data } throw new Error('获取 userSig 失败: 接口返回数据格式错误') } catch (error) { @@ -415,27 +427,18 @@ class TimChatManager { userID: this.currentUserID, userSig: this.currentUserSig }).then(() => { - console.log('腾讯IM登录成功') + console.log('腾讯IM登录请求成功,等待SDK_READY事件...') this.isLoggingIn = false - this.isLoggedIn = true + // 不在这里设置 isLoggedIn = true,等待 onSDKReady 事件 this.reconnectAttempts = 0 - - // 启动心跳检测 - this.startHeartbeat() - - // 启动登录状态检测 - this.startLoginStatusCheck() - - // 触发登录状态变化回调 + + // 触发登录状态变化回调(登录中状态) this.triggerCallback('onLoginStatusChanged', { - isLoggedIn: true, + isLoggedIn: false, userID: this.currentUserID, - reason: 'LOGIN_SUCCESS' + reason: 'LOGIN_REQUESTED', + message: '登录请求成功,等待SDK就绪...' }) - - // 获取会话列表(确保连接正常) - this.getConversationList() - resolve() }).catch(error => { console.error('腾讯IM登录失败:', error) @@ -451,11 +454,11 @@ class TimChatManager { } // ============== 连接监控方法 ============== - + // 启动登录状态检测(优化版:使用配置常量) startLoginStatusCheck() { this.stopLoginStatusCheck() - + // 根据连接稳定性动态调整检查间隔 const getCheckInterval = () => { if (this.reconnectAttempts > 0) { @@ -463,22 +466,22 @@ class TimChatManager { } return IM_CONNECTION_CONFIG.LOGIN_CHECK_INTERVAL_STABLE } - + const checkStatus = () => { // 如果已达到最大重连次数(被踢下线),停止检查 if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.log('⚠️ 已被踢下线或达到最大重连次数,停止登录状态检查') return } - + // 如果正在登录中,跳过本次检查 if (this.isLoggingIn) { this.loginCheckInterval = setTimeout(checkStatus, getCheckInterval()) return } - + const isLoggedIn = this.checkLoginStatus() - + // 如果未登录且有用户ID,尝试重连 if (!isLoggedIn && this.currentUserID && !this.isLoggingIn) { console.log('📡 登录状态检测:发现未登录,尝试重连') @@ -489,21 +492,21 @@ class TimChatManager { console.log('✓ 登录状态检测:连接已恢复正常') this.reconnectAttempts = 0 } - + // 定期清理缓存 this.cleanupCache() } - + // 重新调度下一次检查(再次检查,防止在检查过程中被踢下线) if (this.reconnectAttempts < this.maxReconnectAttempts) { this.loginCheckInterval = setTimeout(checkStatus, getCheckInterval()) } } - + // 首次检查延迟 const firstDelay = IM_CONNECTION_CONFIG.LOGIN_CHECK_FIRST_DELAY this.loginCheckInterval = setTimeout(checkStatus, firstDelay) - console.log(`🔍 登录状态检测已启动,首次检查将在${firstDelay/1000}秒后进行`) + console.log(`🔍 登录状态检测已启动,首次检查将在${firstDelay / 1000}秒后进行`) } // 停止登录状态检测 @@ -525,7 +528,7 @@ class TimChatManager { if (this.isLoggedIn) { // 只在已登录状态下,通过 SDK 再次验证 let sdkLoginStatus = true // 默认保持登录状态 - + if (typeof this.tim.getLoginStatus === 'function') { try { const status = this.tim.getLoginStatus() @@ -539,7 +542,7 @@ class TimChatManager { // 出错时保持当前状态 } } - + // 只有 SDK 明确返回未登录时才更新状态 if (!sdkLoginStatus) { this.isLoggedIn = false @@ -550,7 +553,7 @@ class TimChatManager { }) return false } - + // 保持登录状态 return true } @@ -607,19 +610,19 @@ class TimChatManager { const delayIndex = Math.min(this.reconnectAttempts, this.reconnectDelays.length - 1) const reconnectDelay = this.reconnectDelays[delayIndex] const timeSinceLastLogin = now - this.lastLoginTime - + // 只有非首次重连才需要检查冷却时间 if (this.reconnectAttempts > 0 && timeSinceLastLogin < reconnectDelay) { const remainingTime = reconnectDelay - timeSinceLastLogin console.log(`重连冷却中,剩余时间:${Math.ceil(remainingTime / 1000)}秒`) - + // 安排下次重连 setTimeout(() => { if (!this.isLoggedIn && !this.isLoggingIn && this.reconnectAttempts < this.maxReconnectAttempts) { this.attemptReconnect() } }, remainingTime) - + return false } @@ -652,7 +655,7 @@ class TimChatManager { return false } catch (error) { console.error(`第${this.reconnectAttempts}次重连失败:`, error) - + // 如果还有重连机会,自动安排下一次重连 if (this.reconnectAttempts < this.maxReconnectAttempts) { const nextDelay = this.reconnectDelays[Math.min(this.reconnectAttempts, this.reconnectDelays.length - 1)] @@ -663,7 +666,7 @@ class TimChatManager { } }, nextDelay) } - + return false } } @@ -679,7 +682,6 @@ class TimChatManager { }) this.startLoginStatusCheck() this.startHeartbeat() // 启动心跳检测 - this.getConversationList() } // SDK Not Ready 事件 @@ -700,11 +702,8 @@ class TimChatManager { 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) - // 确保使用消息本身的conversationID,而不是当前会话ID if (!convertedMessage.conversationID) { convertedMessage.conversationID = message.conversationID } @@ -719,30 +718,22 @@ class TimChatManager { messageType: convertedMessage.type, from: convertedMessage.from }) - console.log(event) - - // 缓存功能已移除 - - // 判断是否为当前会话的消息(必须有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) - + // 判断是否为当前会话的消息(必须有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) // 处理已读状态 if (this.currentConversationID) { this.markConversationAsRead(this.currentConversationID) } - this.triggerCallback('onConversationListUpdated', { reason: 'NEW_MESSAGE_RECEIVED_IN_CURRENT_CONVERSATION', conversation: { @@ -770,7 +761,7 @@ class TimChatManager { onMessageSentSucceeded(event) { const messageID = event.data.ID const sentMessage = event.data.message - + // 更新messageList中的消息状态 const message = this.messageList.find(msg => msg.ID === messageID) if (message) { @@ -792,7 +783,7 @@ class TimChatManager { // 消息发送失败 onMessageSentFailed(event) { const messageID = event.data.ID - + // 更新messageList中的消息状态 const message = this.messageList.find(msg => msg.ID === messageID) if (message) { @@ -816,18 +807,18 @@ class TimChatManager { // 网络状态变化(优化版:更稳定的处理) onNetStateChange(event) { const netState = event.data.netState - + console.log('🌐 网络状态变化:', netState) - + // 清理之前的网络重连定时器 if (this.networkReconnectTimer) { clearTimeout(this.networkReconnectTimer) this.networkReconnectTimer = null } - + if (netState === TIM.TYPES.NET_STATE_CONNECTED) { console.log('✓ 网络已连接,延迟检查IM状态以确保稳定') - + // 网络恢复后延迟再检查,避免网络还不稳定时立即重连 const delay = IM_CONNECTION_CONFIG.NETWORK_RECONNECT_DELAY this.networkReconnectTimer = setTimeout(() => { @@ -839,22 +830,22 @@ class TimChatManager { } this.networkReconnectTimer = null }, delay) - + // 重置重连次数(网络恢复后给更多机会) if (this.reconnectAttempts > 0) { console.log(`重置重连次数(之前: ${this.reconnectAttempts})`) this.reconnectAttempts = 0 } - + } else if (netState === TIM.TYPES.NET_STATE_CONNECTING) { console.log('🔗 网络连接中,等待稳定...') - + } else if (netState === TIM.TYPES.NET_STATE_DISCONNECTED) { console.log('⚠️ 网络断开(暂不标记为未登录,等待心跳检测判断)') - + // 网络断开时不立即设置 isLoggedIn = false // 因为可能只是短暂的网络波动,让心跳检测来判断是否真的断线 - + this.triggerCallback('onLoginStatusChanged', { isLoggedIn: this.isLoggedIn, reason: 'NETWORK_DISCONNECTED', @@ -866,26 +857,26 @@ class TimChatManager { // 被踢下线处理 onKickedOut(event) { console.log('⚠️ 账号被踢下线:', event.data) - + // 更新登录状态 this.isLoggedIn = false this.isLoggingIn = false - + // 停止所有定时器(防止自动重连) this.clearAllTimers() this.stopLoginStatusCheck() this.stopHeartbeat() - + // 清除重连相关状态,阻止重连 this.reconnectAttempts = this.maxReconnectAttempts // 设置为最大值,阻止重连 - + // 触发登录状态变化回调 this.triggerCallback('onLoginStatusChanged', { isLoggedIn: false, reason: 'KICKED_OUT', message: '您的账号在其他设备登录' }) - + // 显示提示弹框 uni.showModal({ title: '提示', @@ -899,7 +890,7 @@ class TimChatManager { uni.removeStorageSync('account') uni.removeStorageSync('openid') uni.reLaunch({ - url: '/pages/login/login' + url: '/pages-center/login/login' }) } }) @@ -916,15 +907,14 @@ class TimChatManager { return true } - // 启动心跳检测(优化版:使用配置常量) startHeartbeat() { this.stopHeartbeat() - + // 心跳失败计数器 this.heartbeatFailCount = 0 const MAX_HEARTBEAT_FAIL = IM_CONNECTION_CONFIG.HEARTBEAT_MAX_FAIL const INTERVAL = IM_CONNECTION_CONFIG.HEARTBEAT_INTERVAL - + // 定时心跳检测 this.heartbeatInterval = setInterval(() => { // 如果已达到最大重连次数(被踢下线),停止心跳检测 @@ -933,24 +923,36 @@ class TimChatManager { this.stopHeartbeat() return } - + // 只在已登录状态下进行心跳检测 if (!this.tim || !this.isLoggedIn) { console.log('⏸ 心跳检测:未登录,跳过检测') return } - + + // 确保方法存在 + if (typeof this.tim.getConversationList !== 'function') { + console.log('⏸ 心跳检测:SDK方法不可用,跳过检测') + return + } + this.tim.getConversationList() .then(() => { if (this.heartbeatFailCount > 0) { - console.log(`💚 心跳恢复正常(之前失败${this.heartbeatFailCount}次)`) + console.log(`� 心跳恢复正常(之前失败${this.heartbeatFailCount}次)`) } this.heartbeatFailCount = 0 // 重置失败计数 }) .catch((error) => { + // 如果是SDK未就绪错误,不计入失败次数(这是临时状态) + if (error && error.message && error.message.includes('sdk not ready')) { + console.log('⏸ 心跳检测:SDK未就绪,跳过本次检测') + return + } + this.heartbeatFailCount++ console.error(`💔 心跳失败 (${this.heartbeatFailCount}/${MAX_HEARTBEAT_FAIL}):`, error.message) - + // 只有连续失败多次才认为真的断线 if (this.heartbeatFailCount >= MAX_HEARTBEAT_FAIL) { console.log('❌ 心跳连续失败,标记为未登录并尝试重连') @@ -963,8 +965,8 @@ class TimChatManager { } }) }, INTERVAL) - - console.log(`💓 心跳检测已启动(间隔${INTERVAL/1000}秒,最多失败${MAX_HEARTBEAT_FAIL}次)`) + + console.log(`💓 心跳检测已启动(间隔${INTERVAL / 1000}秒,最多失败${MAX_HEARTBEAT_FAIL}次)`) } // 停止心跳检测 @@ -982,7 +984,7 @@ class TimChatManager { console.error('TIM实例不存在,无法获取会话列表') return } - + if (!this.isLoggedIn) { console.log('SDK未ready,等待SDK初始化后再获取会话列表...') // 等待SDK就绪 @@ -997,14 +999,6 @@ class TimChatManager { setTimeout(checkSDKReady, 500) return } - - this.tim.getConversationList({ withGroupInfo: 1, withAllFields: 1 }).then(response => { - if (this.conversationID) { - this.enterConversation(this.conversationID) - } - }).catch(error => { - console.error('获取会话列表失败:', error) - }) } // 获取群聊列表 @@ -1017,15 +1011,27 @@ class TimChatManager { if (!this.isLoggedIn) { console.log('SDK未ready,等待SDK初始化...') + let waitTime = 0 + const maxWaitTime = 30000 // 最多等待30秒 + const checkInterval = 1000 // 每秒检查一次 + let timeoutHandle = null + const checkSDKReady = () => { if (this.isLoggedIn) { console.log('SDK已ready,开始获取群聊列表') + if (timeoutHandle) clearTimeout(timeoutHandle) this.getGroupListInternal().then(resolve).catch(reject) + } else if (waitTime >= maxWaitTime) { + console.error('等待SDK就绪超时') + if (timeoutHandle) clearTimeout(timeoutHandle) + reject(new Error('SDK初始化超时,请检查网络连接')) } else { - console.log('SDK仍未ready,继续等待...') - setTimeout(checkSDKReady, 1000) + waitTime += checkInterval + console.log(`等待SDK就绪... (${Math.floor(waitTime / 1000)}/${Math.floor(maxWaitTime / 1000)}秒)`) + timeoutHandle = setTimeout(checkSDKReady, checkInterval) } } + checkSDKReady() return } @@ -1039,112 +1045,104 @@ class TimChatManager { return new Promise((resolve, reject) => { console.log('开始获取群聊列表') - this.tim.getConversationList().then(async (conversationResponse) => { - console.clear() - console.log('获取会话列表成功:', conversationResponse) - const groupConversations = conversationResponse.data.conversationList.filter(conversation => { - return conversation.conversationID && conversation.conversationID.startsWith('GROUP') - }) + // 直接调用,SDK就绪检查已在getGroupList()中完成 + this.tim.getConversationList() + .then(async (conversationResponse) => { + console.log('获取会话列表成功') - console.log('群聊会话列表:', groupConversations) + const groupConversations = conversationResponse.data.conversationList.filter(conversation => { + return conversation.conversationID && conversation.conversationID.startsWith('GROUP') + }) - const groupsWithInfo = await Promise.all( - groupConversations.map(async (conversation) => { - const groupName = typeof conversation.groupProfile.name === 'string' ? conversation.groupProfile.name : conversation.groupProfile.name; - const [doctorId, patientName] = groupName.split('|') - try { - const groupID = conversation.conversationID.replace('GROUP', '') - let groupInfo = { - groupID: groupID, - name: '问诊群聊', - avatar: '/static/home/doctor.png', - memberCount: 0 - } + console.log('群聊会话列表数量:', groupConversations.length) + // 先获取一次群组列表,避免在循环中重复调用 + let allGroups = [] + try { + const groupListResponse = await this.tim.getGroupList() + allGroups = groupListResponse.data.groupList || [] + } catch (error) { + console.error('获取群组列表失败:', error) + } + + const groupsWithInfo = await Promise.all( + groupConversations.map(async (conversation) => { try { - const groupListResponse = await this.tim.getGroupList() - const group = groupListResponse.data.groupList.find(g => g.groupID === groupID) - if (group) { - groupInfo = { - ...groupInfo, - name: group.name || '问诊群聊', - memberCount: group.memberCount || 0 + 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 + } + + 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 + } + + return { + ...groupInfo, + groupID: groupID, + doctorId, + patientName, + conversationID: conversation.conversationID, + lastMessage: lastMessageText, + lastMessageTime: lastMessageTime, + unreadCount: conversation.unreadCount || 0 } } catch (error) { - console.error(`获取群组 ${groupID} 信息失败:`, error) - } - - const lastMessage = conversation.lastMessage - let lastMessageText = '' - let lastMessageTime = Date.now() - - if (lastMessage) { - console.log(`群聊 ${groupID} 最后一条消息:`, 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 ? lastMessage.payload.data : '[自定义消息]' - } else { - lastMessageText = '[未知消息类型]' + 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 } + } + }) + ) - if (lastMessage.lastTime) { - lastMessageTime = lastMessage.lastTime * 1000 - } else if (lastMessage.time) { - lastMessageTime = lastMessage.time * 1000 - } - } else { - console.log(`群聊 ${groupID} 没有最后一条消息`) - lastMessageText = '暂无消息' - } - - 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/doctor.png', - lastMessage: '获取失败', - lastMessageTime: Date.now(), - unreadCount: conversation.unreadCount || 0, - memberCount: 0 - } - } + console.log('处理后的群聊列表数量:', groupsWithInfo.length) + resolve({ + success: true, + groupList: groupsWithInfo, + totalCount: groupsWithInfo.length, + data: conversationResponse.data }) - ) - - console.log('处理后的群聊列表:', groupsWithInfo) - resolve({ - success: true, - groupList: groupsWithInfo, - totalCount: groupsWithInfo.length, - data: conversationResponse.data }) - }).catch((imError) => { - console.error('获取会话列表失败:', imError) - reject({ - success: false, - error: imError + .catch((imError) => { + console.error('获取会话列表失败:', imError) + reject({ + success: false, + error: imError + }) }) - }) }) } @@ -1264,7 +1262,7 @@ class TimChatManager { console.log('创建问诊群聊成功:', imResponse) try { - await sendSystemMessage(groupID, 'pending') + await api('sendSystemMessage', { groupID, status: 'pending' }) console.log('pending系统消息发送成功') } catch (error) { console.error('pending系统消息发送失败:', error) @@ -1360,20 +1358,20 @@ class TimChatManager { // 进入会话 enterConversation(conversationID) { console.log("【enterConversation】进入会话:", conversationID) - + // 更新当前会话ID this.currentConversationID = conversationID - + // 清空当前消息列表 this.messageList = [] - + // 重置分页状态 this.nextReqMessageID = "" this.isCompleted = false this.isLoadingMore = false - + console.log(" 会话ID已更新,消息列表已清空,分页状态已重置") - + // 进入群聊会话,默认加载20条消息 this.enterGroupConversation(conversationID, 20) } @@ -1381,7 +1379,7 @@ class TimChatManager { // 进入群聊会话 async enterGroupConversation(groupID, count = 20) { console.log("【enterGroupConversation】进入群聊会话, groupID:", groupID, "count:", count) - + let conversationID = groupID let actualGroupID = groupID @@ -1424,30 +1422,28 @@ class TimChatManager { try { console.log("【loadMessagesFromLocalAPI】开始从本地API加载聊天记录") console.log(" groupID:", groupID, "count:", count, "skip:", skip, "isPullUp:", isPullUp) - + // 调用本地接口获取聊天记录 - const response = await getChatRecordsByGroupId(groupID, count, skip) - - console.log(" 📥 收到本地API响应:", response) - + const response = await api('getChatRecordsByGroupId', { groupID, count, skip }) + 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 - + console.log(` 成功获取 ${dbMessages.length} 条聊天记录`) console.log(` hasMore: ${hasMore}, total: ${total}`) - + // 将数据库消息转换为IM消息格式 const convertedMessages = dbMessages .map(dbMsg => this.convertDBMessageToIMFormat(dbMsg)) .filter(msg => msg !== null && this.filterMessage(msg)) - + console.log(` 转换后 ${convertedMessages.length} 条消息`) - + // 按时间排序(从早到晚) convertedMessages.sort((a, b) => a.lastTime - b.lastTime) - + // 根据是否为上拉加载,决定如何更新消息列表 if (isPullUp) { // 上拉加载更多:将新消息插入到列表前面(历史消息) @@ -1458,12 +1454,12 @@ class TimChatManager { // 首次加载:直接替换消息列表 this.messageList = convertedMessages } - + // 设置分页状态 - 使用后端返回的 hasMore 字段 this.isCompleted = !hasMore - + console.log(` 分页状态: isCompleted=${this.isCompleted}, hasMore=${hasMore}`) - + // 触发回调 this.triggerCallback("onMessageListLoaded", { messages: this.messageList, @@ -1472,7 +1468,7 @@ class TimChatManager { isCompleted: this.isCompleted, total: total, }) - + return { success: true, count: convertedMessages.length, @@ -1499,22 +1495,22 @@ class TimChatManager { } } } - + // 将数据库消息转换为IM消息格式 convertDBMessageToIMFormat(dbMsg) { try { if (!dbMsg || !dbMsg.MsgBody || !Array.isArray(dbMsg.MsgBody) || dbMsg.MsgBody.length === 0) { return null } - + // 获取第一个消息体(通常一个消息只有一个消息体) const msgBody = dbMsg.MsgBody[0] const msgType = msgBody.MsgType - + // 确定消息的流向(in/out) // 根据 From_Account 判断是否为当前用户发送的消息 const flow = dbMsg.From_Account === this.currentUserID ? 'out' : 'in' - + // 计算时间戳(毫秒) let lastTime = Date.now() if (dbMsg.MsgTime) { @@ -1523,7 +1519,7 @@ class TimChatManager { } else if (dbMsg.createdAt) { lastTime = new Date(dbMsg.createdAt).getTime() } - + // 构建基础消息对象 const message = { ID: dbMsg.MsgSeq || dbMsg._id || `db_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, @@ -1533,50 +1529,50 @@ class TimChatManager { payload: this.convertDBPayloadToIMPayload(msgType, msgBody.MsgContent), lastTime: lastTime, status: 'success', - avatar: flow === 'in' ? '/static/home/doctor.png' : '/static/center/user-avatar.png', + avatar: flow === 'in' ? '/static/home/avatar.svg' : '/static/center/user-avatar.png', conversationID: this.currentConversationID, MsgSeq: dbMsg.MsgSeq, // 保留 MsgSeq 用于分页 } - + return message } catch (error) { console.error("转换数据库消息格式失败:", error, dbMsg) return null } } - + // 将数据库消息体转换为IM payload格式 convertDBPayloadToIMPayload(msgType, msgContent) { try { if (!msgContent) { return {} } - + switch (msgType) { case 'TIMTextElem': return { text: msgContent.Text || '' } - + case 'TIMImageElem': return { imageInfoArray: msgContent.ImageInfoArray || [] } - + case 'TIMSoundElem': return { url: msgContent.Url || '', second: msgContent.Second || 0, downloadFlag: msgContent.Download_Flag || 2 } - + case 'TIMCustomElem': return { data: msgContent.Data || '', description: msgContent.Desc || '', extension: msgContent.Ext || '' } - + case 'TIMVideoFileElem': return { videoUrl: msgContent.VideoUrl || '', @@ -1591,7 +1587,7 @@ class TimChatManager { thumbFormat: msgContent.ThumbFormat || '', thumbDownloadFlag: msgContent.ThumbDownloadFlag || 2 } - + case 'TIMFileElem': return { url: msgContent.Url || '', @@ -1599,7 +1595,7 @@ class TimChatManager { fileName: msgContent.FileName || '', downloadFlag: msgContent.Download_Flag || 2 } - + default: return msgContent } @@ -1641,7 +1637,7 @@ class TimChatManager { const messageList = response.data.messageList const oldNextReqMessageID = this.nextReqMessageID const oldIsCompleted = this.isCompleted - + this.nextReqMessageID = response.data.nextReqMessageID this.isCompleted = response.data.isCompleted @@ -1717,7 +1713,7 @@ class TimChatManager { // 从本地数据库加载更多历史消息 // 使用当前消息列表的长度作为 skip 值 const skip = this.messageList.length - + // 获取群组ID let groupID = '' if (this.conversation && this.conversation.groupProfile) { @@ -1725,7 +1721,7 @@ class TimChatManager { } else if (this.currentConversationID) { groupID = this.currentConversationID.replace('GROUP', '') } - + if (!groupID) { console.error(" ❌ 无法获取群组ID") this.isLoadingMore = false @@ -1734,12 +1730,12 @@ class TimChatManager { message: "无法获取群组ID", } } - + console.log(" 从本地数据库加载更多,groupID:", groupID, "skip:", skip) - + // 调用本地API加载更多消息 const result = await this.loadMessagesFromLocalAPI(groupID, 20, skip, true) - + this.isLoadingMore = false console.log(" 🔄 设置 isLoadingMore = false") console.log(" 📥 loadMessagesFromLocalAPI 返回结果:", result) @@ -1778,11 +1774,11 @@ class TimChatManager { if (result.success) { // 严格过滤:只保留属于当前会话的消息 const filteredMessages = result.messages.filter(msg => msg.conversationID === conversationID) - + this.messageList = filteredMessages - + console.log(`消息刷新完成,过滤后${filteredMessages.length}条消息`) - + // 触发更新回调 this.triggerCallback('onMessageListLoaded', { messages: this.messageList, @@ -1800,14 +1796,14 @@ class TimChatManager { // 合并消息(保留本地消息,避免覆盖) mergeMessages(serverMessages, cachedMessages) { const messageMap = new Map() - + // 先添加服务器消息 serverMessages.forEach(msg => { if (msg.ID) { messageMap.set(msg.ID, msg) } }) - + // 再添加缓存消息(覆盖服务器消息,保留本地最新状态) cachedMessages.forEach(msg => { if (msg.ID) { @@ -1820,13 +1816,13 @@ class TimChatManager { } } }) - + // 转换为数组并按时间排序 const mergedArray = Array.from(messageMap.values()) mergedArray.sort((a, b) => a.lastTime - b.lastTime) - + console.log(`消息合并完成: 服务器${serverMessages.length}条, 缓存${cachedMessages.length}条, 合并后${mergedArray.length}条`) - + return mergedArray } @@ -1963,7 +1959,7 @@ class TimChatManager { // 使用Set去重,避免重复消息 const existingIds = new Set(allMessages.map(m => m.ID)) const newMessages = result.messages.filter(m => !existingIds.has(m.ID)) - + if (newMessages.length > 0) { allMessages.unshift(...newMessages) console.log(`添加${newMessages.length}条新消息,当前总数: ${allMessages.length}`) @@ -2043,29 +2039,42 @@ class TimChatManager { if (!this.tim) { this.triggerCallback('onError', 'IM未初始化') - return + return { success: false, error: 'IM未初始化' } } - if (!this.conversation) { - this.triggerCallback('onError', '群聊会话不存在') - return { success: false, error: '群聊会话不存在' } + // 检查登录状态 + if (!this.isLoggedIn) { + console.error('IM未登录,无法发送消息'); + this.triggerCallback('onError', 'IM未登录,请稍后重试') + return { success: false, error: 'IM未登录' } } - let groupID = null - if (this.conversation.groupProfile && this.conversation.groupProfile.groupID) { - groupID = this.conversation.groupProfile.groupID - } else if (this.conversation.conversationID) { - groupID = this.conversation.conversationID.replace('GROUP', '') + // 优先使用 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; } if (!groupID) { + console.error('无法获取群聊ID,conversationID:', conversationID); this.triggerCallback('onError', '无法获取群聊ID') return { success: false, error: '无法获取群聊ID' } } - // 确保使用当前会话的conversationID - const conversationID = this.conversation.conversationID || this.currentConversationID - const localMessage = { ID: `local_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, flow: 'out', @@ -2096,6 +2105,14 @@ class TimChatManager { } catch (error) { console.error('文本消息发送失败:', error) localMessage.status = 'failed' + + // 如果是因为未登录导致的失败,尝试重连 + if (error.message && (error.message.includes('not login') || error.message.includes('sdk not ready'))) { + console.log('检测到未登录错误,尝试重连...'); + this.isLoggedIn = false; + this.ensureIMConnection(); + } + return { success: false, error } } } @@ -2143,7 +2160,7 @@ class TimChatManager { }, lastTime: Date.now(), status: 'sending', - avatar: '/static/center/user-avatar.png', + avatar: '', conversationID: conversationID, from: this.currentUserID } @@ -2171,34 +2188,46 @@ class TimChatManager { return { success: false, error } } } - // 发送语音消息 async sendVoiceMessage(voiceFile, duration) { if (!this.tim) { this.triggerCallback('onError', 'IM未初始化') - return + return { success: false, error: 'IM未初始化' } } - if (!this.conversation) { - this.triggerCallback('onError', '群聊会话不存在') - return { success: false, error: '群聊会话不存在' } + // 检查登录状态 + if (!this.isLoggedIn) { + console.error('IM未登录,无法发送消息'); + this.triggerCallback('onError', 'IM未登录,请稍后重试') + return { success: false, error: 'IM未登录' } } - let groupID = null - if (this.conversation.groupProfile && this.conversation.groupProfile.groupID) { - groupID = this.conversation.groupProfile.groupID - } else if (this.conversation.conversationID) { - groupID = this.conversation.conversationID.replace('GROUP', '') + // 优先使用 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; } if (!groupID) { + console.error('无法获取群聊ID,conversationID:', conversationID); this.triggerCallback('onError', '无法获取群聊ID') return { success: false, error: '无法获取群聊ID' } } - // 确保使用当前会话的conversationID - const conversationID = this.conversation.conversationID || this.currentConversationID - const localMessage = { ID: `local_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, flow: 'out', @@ -2231,6 +2260,14 @@ class TimChatManager { } catch (error) { console.error('语音消息发送失败:', error) localMessage.status = 'failed' + + // 如果是因为未登录导致的失败,尝试重连 + if (error.message && (error.message.includes('not login') || error.message.includes('sdk not ready'))) { + console.log('检测到未登录错误,尝试重连...'); + this.isLoggedIn = false; + this.ensureIMConnection(); + } + return { success: false, error } } } @@ -2239,29 +2276,42 @@ class TimChatManager { async sendCustomMessage(messageData) { if (!this.tim) { this.triggerCallback('onError', 'IM未初始化') - return + return { success: false, error: 'IM未初始化' } } - if (!this.conversation) { - this.triggerCallback('onError', '群聊会话不存在') - return { success: false, error: '群聊会话不存在' } + // 检查登录状态 + if (!this.isLoggedIn) { + console.error('IM未登录,无法发送消息'); + this.triggerCallback('onError', 'IM未登录,请稍后重试') + return { success: false, error: 'IM未登录' } } - let groupID = null - if (this.conversation.groupProfile && this.conversation.groupProfile.groupID) { - groupID = this.conversation.groupProfile.groupID - } else if (this.conversation.conversationID) { - groupID = this.conversation.conversationID.replace('GROUP', '') + // 优先使用 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; } if (!groupID) { + console.error('无法获取群聊ID,conversationID:', conversationID); this.triggerCallback('onError', '无法获取群聊ID') return { success: false, error: '无法获取群聊ID' } } - // 确保使用当前会话的conversationID - const conversationID = this.conversation.conversationID || this.currentConversationID - const localMessage = { ID: `local_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, flow: 'out', @@ -2299,6 +2349,14 @@ class TimChatManager { } catch (error) { console.error('自定义消息发送失败:', error) localMessage.status = 'failed' + + // 如果是因为未登录导致的失败,尝试重连 + if (error.message && (error.message.includes('not login') || error.message.includes('sdk not ready'))) { + console.log('检测到未登录错误,尝试重连...'); + this.isLoggedIn = false; + this.ensureIMConnection(); + } + return { success: false, error } } } @@ -2333,7 +2391,7 @@ class TimChatManager { payload: timMessage.payload, lastTime: lastTime, status: timMessage.status || 'success', - avatar: timMessage.flow === 'in' ? '/static/home/doctor.png' : '/static/center/user-avatar.png', + avatar: timMessage.flow === 'in' ? '/static/home/avatar.svg' : '/static/center/user-avatar.png', // 优先使用消息本身的conversationID,确保消息归属正确的会话 conversationID: timMessage.conversationID } @@ -2377,12 +2435,19 @@ class TimChatManager { } getImageUrl(imageFile) { + // 处理 tempFiles 数组格式 if (imageFile?.tempFiles?.length > 0) { return imageFile.tempFiles[0].tempFilePath } + // 处理单个文件对象 + if (imageFile?.tempFilePath) { + return imageFile.tempFilePath + } + // 处理字符串路径 if (typeof imageFile === 'string') { return imageFile } + console.warn('无法获取图片URL,使用默认图片:', imageFile); return '/static/home/photo.png' } @@ -2391,17 +2456,27 @@ class TimChatManager { return new Promise((resolve) => { let imagePath = ''; - // 获取图片路径 - if (imageFile?.tempFiles?.length > 0) { - imagePath = imageFile.tempFiles[0].tempFilePath; + // 获取图片路径 - 处理多种格式 + if (imageFile?.tempFilePaths?.length > 0) { + // wx.chooseImage 返回的对象 + imagePath = imageFile.tempFilePaths[0]; + } else if (imageFile?.tempFiles?.length > 0) { + // 从 tempFiles 中提取路径 + const tempFile = imageFile.tempFiles[0]; + imagePath = tempFile.path || tempFile.tempFilePath; + } else if (imageFile?.tempFilePath) { + imagePath = imageFile.tempFilePath; } else if (typeof imageFile === 'string') { imagePath = imageFile; } else { + console.warn('无法获取图片路径,使用默认尺寸:', imageFile); // 默认尺寸 resolve({ width: 400, height: 300 }); return; } + console.log('获取图片信息,路径:', imagePath); + // 使用uni.getImageInfo获取图片尺寸 uni.getImageInfo({ src: imagePath, @@ -2499,10 +2574,10 @@ class TimChatManager { // 清理资源(优化版:更完整的清理) destroy() { console.log('=== 开始清理IM资源 ===') - + // 清理所有定时器(使用统一方法) this.clearAllTimers() - + // 清理消息缓存(已弃用) console.log('缓存功能已移除') @@ -2525,7 +2600,7 @@ class TimChatManager { // 重置所有状态(使用统一方法) this.resetAllStates() - + // 清理其他状态 this.messageList = [] this.conversation = null @@ -2533,7 +2608,7 @@ class TimChatManager { this.currentUserSig = '' this.currentConversationID = null this.tim = null - + console.log('=== IM资源清理完成 ===') } } @@ -2579,7 +2654,7 @@ const initGlobalTIM = async (userID, forceReinit = false) => { const getInitIMPromise = async (userID, forceReinit) => { if (initPromise) return initPromise; initPromise = initGlobalTIM(userID, forceReinit); - return initPromise; + return initPromise; } const clearInitIMPromise = () => { initPromise = null; @@ -2591,14 +2666,14 @@ const getGroupList = async () => { } // 进入群聊 -const enterChatGroupRoom = async (chatGroup, navigateType = 'navigateTo' , viewType) => { +const enterChatGroupRoom = async (chatGroup, navigateType = 'navigateTo', viewType) => { 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]({ - url: `/pages/message/index?conversationID=GROUP${chatGroup.groupID}&groupID=${chatGroup.groupID}&conversationType=GROUP&viewType=${viewType}`, + url: `/pages-im/IM/index?conversationID=GROUP${chatGroup.groupID}&groupID=${chatGroup.groupID}&conversationType=GROUP&viewType=${viewType}`, }) }