diff --git a/.env.development b/.env.development index 9928b9d..c35990f 100644 --- a/.env.development +++ b/.env.development @@ -1,3 +1,4 @@ MP_API_BASE_URL=http://localhost:8080 MP_CACHE_PREFIX=development MP_WX_APP_ID=wx93af55767423938e +MP_CORP_ID=wwe3fb2faa52cf9dfb diff --git a/App.vue b/App.vue index b6f4564..32f7d93 100644 --- a/App.vue +++ b/App.vue @@ -1,351 +1,353 @@ diff --git a/api/consult-order.js b/api/consult-order.js new file mode 100644 index 0000000..b32b178 --- /dev/null +++ b/api/consult-order.js @@ -0,0 +1,98 @@ +// 患者管理模块 API 封装 +import api from '../utils/http.js' + +// API 基础路径 +const BASE_PATH = '/order' + +/** + * 初始化咨询订单 + */ +export function initConsultOrder({ doctorCode, accountId, openId, memberId }) { + return api.post(`${BASE_PATH}/consult-orders/init`, { doctorCode, accountId, openId, memberId }) +} + +/** + * 绑定订单的患者信息 + */ +export function bindOrderPatient({ accountId, memberId, orderId }) { + return api.post(`${BASE_PATH}/consult-orders/bind-member`, { accountId, memberId, orderId }) +} + +/** + * 补充病情描述 + */ +export function submitOrderDescription({ accountId, orderId, description, diseases, images, hasVisitedHospital }) { + return api.post(`${BASE_PATH}/consult-orders/supplement-description`, { accountId, orderId, description, diseases, images, hasVisitedHospital }) +} + +/** + * 获取订单信息 + */ +export function getOrderInfo({ orderId }) { + return api.post(`${BASE_PATH}/consult-orders/get`, { orderId }) +} + +/** + * 获取订单列表 + */ +export function getOrderList({ accountId, page, pageSize: limit, orderStatus }) { + return api.post(`${BASE_PATH}/consult-orders/list`, { accountId, page, limit, orderStatus }) +} + +/** + * 获取最新订单 + */ +export function getLatestOrder({ accountId, chatGroupId }) { + return api.post(`${BASE_PATH}/consult-orders/latest`, { accountId, chatGroupId }) +} + +/** + * 获取订单支付信息 + */ +export function getOrderTradeNo({ orderId, accountId }) { + return api.post(`${BASE_PATH}/consult-orders/get-trade-no`, { accountId, orderId }) +} + +/** + * 取消订单 + */ +export function cancelOrder({ orderId, accountId }) { + return api.post(`${BASE_PATH}/consult-orders/cancel`, { orderId, accountId }) +} + +/** + * 获取账号统计信息 + */ +export function getAccountStats(accountId, statusList) { + return api.post(`${BASE_PATH}/consult-orders/account-stats`, { accountId, statusList }) +} + +export function getConsultInfo({doctorCode, memberId, accountId}) { + return api.post(`${BASE_PATH}/consult-orders/get-consult-info`, { doctorCode, memberId, accountId }) +} + +/** + * 更新订单最后一条消息ID + */ +export function updateLastMessageId({ orderId, lastMessageId }) { + return api.post(`${BASE_PATH}/consult-orders/update-last-message`, { orderId, lastMessageId }) +} + +/** + * 发送消息到群组 + * @param {Object} params + * @param {string} params.groupId - 群组ID + * @param {string} params.desc - 消息描述/类型,如:'WAIT_DOCTOR_ACCEPT' + * @param {string} params.message - 消息内容 + * @param {string} params.ext - 扩展信息(JSON字符串) + * @param {string} params.fromAccount - 发送者账号(可选) + */ +export function sendMessageToGroup({ groupId, desc, message, ext, fromAccount }) { + return api.post(`${BASE_PATH}/consult-orders/send-message-to-group`, { + groupId, + desc, + message, + ext, + fromAccount + }) +} \ No newline at end of file diff --git a/api/corp/dept.js b/api/corp/dept.js new file mode 100644 index 0000000..10caf9c --- /dev/null +++ b/api/corp/dept.js @@ -0,0 +1,13 @@ +import api from "../../utils/http.js"; + +const BASE_PATH = "/corp"; + +/** + * 获取科室列表(按 corpId) + * @param {Object} params + * @param {string} params.corpId + */ +export function getDeptList(params = {}) { + return api.post(`${BASE_PATH}`, { type: "getDeptList", ...params }); +} + diff --git a/api/corp/im.js b/api/corp/im.js new file mode 100644 index 0000000..62c45af --- /dev/null +++ b/api/corp/im.js @@ -0,0 +1,48 @@ +import api from '../../utils/http.js' + +// API 基础路径 +const BASE_PATH = '/corp' + +/** + * 获取用户签名 + * @param {string} userId - 用户ID + * @returns {Promise} 返回包含 userSig 信息的 Promise + */ +export const getUserSig = (userId) => { + return api.post(`${BASE_PATH}/tencent-im/user-sig`, { userId }) +} + +export async function getChatStatus(chatGroupId) { + return api.post(`${BASE_PATH}/tencent-im/get-chat-status`, { chatGroupId, role: 'patient' }) +} + +export const sendSystemMessage = (groupId, data, Desc = '', Ext = '') => { + return api.post(`${BASE_PATH}/tencent-im/send-group-message`, { + groupId, + msgBody: [ + { + MsgType: "TIMCustomElem", + MsgContent: { + Data: data, + Desc, + Ext + } + } + ] + }) +} + +/** + * 获取群组聊天记录(POST请求) + * @param {string} groupId - 群组ID + * @param {number} limit - 每页数量,默认20,最大100 + * @param {number} skip - 跳过数量,默认0 + * @returns {Promise} 返回包含聊天记录的 Promise + */ +export const getChatRecordsByGroupId = (groupId, limit = 20, skip = 0) => { + return api.post(`${BASE_PATH}/tencent-im/chat-records`, { + GroupId: groupId, + limit, + skip + }) +} diff --git a/api/corp/rate.js b/api/corp/rate.js new file mode 100644 index 0000000..7849409 --- /dev/null +++ b/api/corp/rate.js @@ -0,0 +1,15 @@ +import api from '../../utils/http.js' + +// API 基础路径 +const BASE_PATH = '/corp/rate-records' + +export const getRate = (id) => { + return api.post(`${BASE_PATH}/get-by-id`, { id }) +} +export function submitRate({ id, rate, words }) { + return api.post(`${BASE_PATH}/submit`, { id, rate, words }) +} + +export const getRateList = ({ page, pageSize, doctorId }) => { + return api.post(`${BASE_PATH}/displayable-list`, { page, limit: pageSize, userId: doctorId }) +} \ No newline at end of file diff --git a/api/doctor/doctor.js b/api/doctor/doctor.js new file mode 100644 index 0000000..7358cae --- /dev/null +++ b/api/doctor/doctor.js @@ -0,0 +1,75 @@ +import api from '../../utils/http.js' + +const BASE_PATH = "/corp/doctors"; + +/** + * 获取医生 + * @param {string} id - 医生ID + * @returns {Promise} 医生数据 + */ +export function getDoctorInfo(id) { + return api.post(`${BASE_PATH}/get-by-id`, { id }) +} + +/** + * 获取邀请医生 + * @param {string} doctorId - 医生ID + * @returns {Promise} 邀请医生数据 + */ +export function getDoctorByDoctorId(doctorId) { + return api.get(`${BASE_PATH}/doctor/${doctorId}`) +} + +/** + * 获取医生 + * @param {string} accountId - 账户ID + * @returns + */ +export function getDoctorInfoByAccountId(accountId) { + return api.post(`${BASE_PATH}/get-by-account-id`, { accountId }) +} +/** + * 获取医生 + * @param {string} doctorId - 医生ID + * @returns + */ +export function getDoctorInfoByDoctorId(doctorId) { + return api.post(`${BASE_PATH}/get-by-doctor-id`, { doctorId }) +} +/** + * 创建医生 + * @param {Object} data - 医生数据 + * @param {string} data.accountId - 账户ID + * @param {string} data.avatar - 医生头像 + * @param {string} data.name - 医生姓名 + * @param {string} data.phone - 医生手机号 + * @param {string} data.hospitalId - 医院ID + * @param {string} data.hospitalName - 医院名称 + * @param {string} data.departmentId - 科室ID + * @param {string} data.title - 医生职称 + * @param {string} data.specialty - 医生擅长 + * @param {string} data.intro - 个人简介 + * @param {string} data.titleCertificate - 职称证书 + * @param {string} data.practiceLicenseCode - 执业证编号 + * @param {string} data.practiceLicenseFront - 执业证正面 + * @param {string} data.practiceLicenseBack - 执业证背面 + * @param {string} data.medicalLicenseFront - 资格证正面 + * @param {string} data.medicalLicenseBack - 资格证背面 + * @param {string} data.medicalLicenseCode - 资格证编号 + * @param {string} data.idCardFront - 身份证正面 + * @param {string} data.idCardBack - 身份证背面 + * @param {string} data.workCard - 工作证 + * @returns {Promise} 创建结果 + */ +export function createDoctorInfo(data) { + return api.post(`${BASE_PATH}/create`, data) +} + +/** + * 更新医生 + * @param {Object} data - 医生数据 + * @returns {Promise} 更新结果 + */ +export function updateDoctorInfo(data) { + return api.post(`${BASE_PATH}/update`, data) +} diff --git a/api/medicine.js b/api/medicine.js new file mode 100644 index 0000000..2314f8c --- /dev/null +++ b/api/medicine.js @@ -0,0 +1,50 @@ +// 患者管理模块 API 封装 +import api from '../utils/http.js' + +// API 基础路径 +const BASE_PATH = '/medicine' + +export function getDiagnosisList({ type, keyword, page, limit }) { + return api.post(`${BASE_PATH}/diagnoses/list`, { type, keyword, page, limit }) +} + +export function getAccountPrescriptions({ accountId: patientAccountId, doctorId, patientId, statusList, page, limit }) { + return api.post(`${BASE_PATH}/prescriptions/get-by-patient-account`, { patientAccountId, doctorId, statusList, page, limit, patientId }) +} + +export function getPrescriptionStats(accountId, statusList) { + return api.post(`${BASE_PATH}/prescriptions/account-stats`, { accountId, statusList }) +} + +export function getPrescriptionDetail(id) { + return api.post(`${BASE_PATH}/prescriptions/get-by-id`, { id }) +} + +export function getPrescriptionDetailWithRps(id) { + return api.post(`${BASE_PATH}/prescriptions/get-by-id-with-rps`, { id }) +} + +export function bindSharePrescription({ id, accountId, openId, patientId }) { + return api.post(`${BASE_PATH}/prescriptions/bind-member`, { id, accountId, openId, patientId }) +} + +export function getPrescriptionTradeNo({ id, openId, accountId, logistics }) { + return api.post(`${BASE_PATH}/prescriptions/get-trade-no`, { id, openId, accountId, logistics }) +} + +export function cancelPrescription({ accountId, id }) { + return api.post(`${BASE_PATH}/prescriptions/cancel`, { id, accountId }) +} + +export function applyMedicines({ id, patientAccountId, openId }) { + return api.post(`${BASE_PATH}/prescriptions/apply-represcription`, { id, patientAccountId, openId }) +} + +export function getCartMedicines({ accountId, ids }) { + return api.post(`${BASE_PATH}/prescriptions/get-cart-medicines`, { accountId, ids }) +} + +export function getMergeTradeNo({ ids, openId, accountId, logistics }) { + return api.post(`${BASE_PATH}/prescriptions/get-merge-trade-no`, { ids, openId, accountId, logistics }) + +} \ No newline at end of file diff --git a/hooks/useImageUpload.js b/hooks/useImageUpload.js new file mode 100644 index 0000000..08811c8 --- /dev/null +++ b/hooks/useImageUpload.js @@ -0,0 +1,76 @@ +import { ref } from "vue"; +import { uploadFile } from '@/api/file'; +import { loading as showLoading, hideLoading, toast } from '@/utils/widget' + +const BusinessType = { + order: 'consult_order', // 咨询订单 + chat: 'chat_message', // 聊天消息 + other: 'other' // 其他 +} + +/** + * 图片上传hook + * @param {*} maxCount + * @returns + */ +export default function useImageUpload(type, maxCount = 1) { + const images = ref([]) + const businessType = BusinessType[type] || ''; + const tempPaths = []; + + if (!businessType) { + toast('无效的businessType') + } + + function add() { + if (!businessType) { + toast(`无效的businessType`) + return + } + if (images.value.length > maxCount) { + toast('最多上传' + maxCount + '张图片') + return + } + uni.chooseMedia({ + count: maxCount - images.value.length, + mediaType: ['image'], + sourceType: ['album', 'camera'], + sizeType: ['compressed'], + success: (res) => { + const tempFilePaths = res.tempFiles.map(item => item.tempFilePath); + images.value = [...images.value, ...tempFilePaths]; + tempPaths.push(...tempFilePaths); + } + }) + } + + function remove(index) { + images.value.splice(index, 1) + } + + async function uploadImages() { + const uploadPaths = images.value.filter(i => tempPaths.includes(i)); + showLoading('正在上传图片...') + const res = await Promise.all(uploadPaths.map(uploadImage)); + let failCount = 0; + uploadPaths.forEach((path, idx) => { + const index = images.value.findIndex(i => i === path && path); + const url = typeof res[idx] === 'string' && res[idx].trim() ? res[idx] : ''; + if(url && index >= 0) { + images.value[index] = url; + } else if(!url) { + failCount++; + } + }) + hideLoading() + toast(failCount ? `${failCount}张图片上传失败` : '图片上传成功') + return failCount === 0 + } + + async function uploadImage(path) { + const res = await uploadFile(path, businessType); + return res ? res.previewUrl : ''; + } + + return { images, add, remove, uploadImage, uploadImages } +} \ No newline at end of file diff --git a/hooks/useOnceEvent.js b/hooks/useOnceEvent.js new file mode 100644 index 0000000..dd54a5e --- /dev/null +++ b/hooks/useOnceEvent.js @@ -0,0 +1,22 @@ +import { onLoad } from "@dcloudio/uni-app"; + +export function registerOnceEvent(callback, prefix = 'once_') { + // 生成一个唯一事件名 + const eventName = `${prefix}${Date.now()}_${Math.floor(Math.random() * 10000)}`; + uni.$once(eventName, callback); + return eventName; +} + +// 触发事件 +export function triggerOnceEvent() { + let eventName = ''; + onLoad((e) => { + eventName = e.eventName; + }) + function trigger(data) { + if (eventName && typeof eventName === 'string') { + uni.$emit(eventName, data); + } + } + return trigger; +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index eae5086..db80b11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,9 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "dayjs": "^1.11.10" + "dayjs": "^1.11.10", + "tim-upload-plugin": "^1.4.2", + "tim-wx-sdk": "^2.27.6" }, "devDependencies": {} }, @@ -17,6 +19,16 @@ "version": "1.11.19", "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz", "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==" + }, + "node_modules/tim-upload-plugin": { + "version": "1.4.3", + "resolved": "https://registry.npmmirror.com/tim-upload-plugin/-/tim-upload-plugin-1.4.3.tgz", + "integrity": "sha512-3ZmbA36dr3eG9YGDon9MLBUtbNawYWkL+TBa+VS0Uviguc7PlVSOIVRG2C4irXX16slDT2Kj+HAZapp+Xqp2xg==" + }, + "node_modules/tim-wx-sdk": { + "version": "2.27.6", + "resolved": "https://registry.npmmirror.com/tim-wx-sdk/-/tim-wx-sdk-2.27.6.tgz", + "integrity": "sha512-zB+eRdmigdhEDeqrXC0bLJonUQZzS5uKNPLFtrje503WAnmuxVQjq/n4Zle4FYHG4FiKHKhsrVd0aCYXABlFEg==" } } } diff --git a/package.json b/package.json index e7519bb..d40d825 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "author": "", "license": "ISC", "dependencies": { - "dayjs": "^1.11.10" + "dayjs": "^1.11.10", + "tim-upload-plugin": "^1.4.2", + "tim-wx-sdk": "^2.27.6" }, "uni-app": { "scripts": { diff --git a/pages.json b/pages.json index d11d514..c555ad9 100644 --- a/pages.json +++ b/pages.json @@ -65,6 +65,24 @@ "style": { "navigationBarTitleText": "工作台" } + }, + { + "path": "pages/work/profile", + "style": { + "navigationBarTitleText": "完善个人信息" + } + }, + { + "path": "pages/work/department-select", + "style": { + "navigationBarTitleText": "选择科室" + } + }, + { + "path": "pages/login/login", + "style": { + "navigationBarTitleText": "登录" + } } ], "globalStyle": { diff --git a/pages/login/login.vue b/pages/login/login.vue index 1bd6e10..ee59991 100644 --- a/pages/login/login.vue +++ b/pages/login/login.vue @@ -82,7 +82,7 @@ function remind() { function toHome() { uni.navigateTo({ - url: "/pages/home/home", + url: "/pages/message/message", }); } @@ -112,7 +112,11 @@ onLoad((opts) => { console.log("redirectUrl", redirectUrl.value); return; } - redirectUrl.value = opts.redirectUrl || ""; + if (opts.redirect) { + redirectUrl.value = decodeURIComponent(opts.redirect); + } else { + redirectUrl.value = opts.redirectUrl || ""; + } }); \ No newline at end of file diff --git a/pages/message/components/consultation-bar.vue b/pages/message/components/consultation-bar.vue new file mode 100644 index 0000000..6c73414 --- /dev/null +++ b/pages/message/components/consultation-bar.vue @@ -0,0 +1,43 @@ + + + + + \ No newline at end of file diff --git a/pages/message/components/description-popup.vue b/pages/message/components/description-popup.vue new file mode 100644 index 0000000..bda3e8d --- /dev/null +++ b/pages/message/components/description-popup.vue @@ -0,0 +1,417 @@ + + + + + \ No newline at end of file diff --git a/pages/message/components/head-card.vue b/pages/message/components/head-card.vue new file mode 100644 index 0000000..f069f85 --- /dev/null +++ b/pages/message/components/head-card.vue @@ -0,0 +1,60 @@ + + + \ No newline at end of file diff --git a/pages/message/components/message-card/fill-description-card.vue b/pages/message/components/message-card/fill-description-card.vue new file mode 100644 index 0000000..492cc07 --- /dev/null +++ b/pages/message/components/message-card/fill-description-card.vue @@ -0,0 +1,85 @@ + + + \ No newline at end of file diff --git a/pages/message/components/message-card/medicines-card.vue b/pages/message/components/message-card/medicines-card.vue new file mode 100644 index 0000000..077f2fe --- /dev/null +++ b/pages/message/components/message-card/medicines-card.vue @@ -0,0 +1,140 @@ + + + \ No newline at end of file diff --git a/pages/message/components/message-card/message-card.vue b/pages/message/components/message-card/message-card.vue new file mode 100644 index 0000000..31eb100 --- /dev/null +++ b/pages/message/components/message-card/message-card.vue @@ -0,0 +1,574 @@ + + + + + diff --git a/pages/message/components/message-card/refill-medicine-card.vue b/pages/message/components/message-card/refill-medicine-card.vue new file mode 100644 index 0000000..9b98219 --- /dev/null +++ b/pages/message/components/message-card/refill-medicine-card.vue @@ -0,0 +1,102 @@ + + + \ No newline at end of file diff --git a/pages/message/components/message-card/reject-refill-medicine-card.vue b/pages/message/components/message-card/reject-refill-medicine-card.vue new file mode 100644 index 0000000..dbc06e0 --- /dev/null +++ b/pages/message/components/message-card/reject-refill-medicine-card.vue @@ -0,0 +1,50 @@ + + + \ No newline at end of file diff --git a/pages/message/components/message-types.vue b/pages/message/components/message-types.vue new file mode 100644 index 0000000..6794062 --- /dev/null +++ b/pages/message/components/message-types.vue @@ -0,0 +1,168 @@ + + + + + \ No newline at end of file diff --git a/pages/message/components/special-message/evaluation.vue b/pages/message/components/special-message/evaluation.vue new file mode 100644 index 0000000..3d83880 --- /dev/null +++ b/pages/message/components/special-message/evaluation.vue @@ -0,0 +1,188 @@ + + + + + \ No newline at end of file diff --git a/pages/message/components/special-message/index.vue b/pages/message/components/special-message/index.vue new file mode 100644 index 0000000..f71641d --- /dev/null +++ b/pages/message/components/special-message/index.vue @@ -0,0 +1,40 @@ + + \ No newline at end of file diff --git a/pages/message/components/system-message.vue b/pages/message/components/system-message.vue new file mode 100644 index 0000000..0d28603 --- /dev/null +++ b/pages/message/components/system-message.vue @@ -0,0 +1,64 @@ + + + + + \ No newline at end of file diff --git a/pages/message/hooks/chat-order-hook.js b/pages/message/hooks/chat-order-hook.js new file mode 100644 index 0000000..49d176d --- /dev/null +++ b/pages/message/hooks/chat-order-hook.js @@ -0,0 +1,30 @@ +import { computed, onUnmounted, ref, watch } from 'vue'; +import { onShow } from "@dcloudio/uni-app"; +import dayjs from 'dayjs'; +import { getChatOrder, orderStatus } from "@/utils/order.js"; + +export default function useChatOrder(orderId) { + const currentOrder = ref(null);// 当前聊天室的最新咨询订单 + const countdown = ref(''); // 倒计时 + + const chatRoomStatus = computed(() => { + const order = currentOrder.value || {}; + const isWaiting = order.orderStatus === orderStatus.PAID; + const isPending = order.orderStatus === orderStatus.CONSULTING; + return { isWaiting, isPending }; + }) + + async function getCurrentOrder() { + if (orderId.value) { + currentOrder.value = await getChatOrder(orderId.value) + } else { + currentOrder.value = null + } + } + + watch(orderId, n=>{ + getCurrentOrder() + }) + + return { currentOrder, chatRoomStatus, countdown, getCurrentOrder } +} \ No newline at end of file diff --git a/pages/message/hooks/use-chat-business.js b/pages/message/hooks/use-chat-business.js new file mode 100644 index 0000000..61c662d --- /dev/null +++ b/pages/message/hooks/use-chat-business.js @@ -0,0 +1,146 @@ +import { ref, computed } from 'vue'; +import { onShow, onUnload } from "@dcloudio/uni-app"; +import { getChatStatus } from "@/api/corp/im.js"; +import { getDoctorByDoctorId } from '@/api/doctor/doctor'; +import { toast } from '@/utils/widget'; +import dayjs from 'dayjs'; + +import useChatOrder from './chat-order-hook'; + + +/** + * 获取聊天室当前进行中业务hook + * @param {string聊天群组ID} groupID + */ +export default function useChatBusiness(groupID) { + const groupInfo = ref({}); + + const assistant = computed(() => { + if (groupInfo.value.assistant?.memberAccount) { + return { assistantId: groupInfo.value.assistant.memberAccount, name: groupInfo.value.assistant.name } + } + return null; + }) + + const doctor = computed(() => { + // 优先返回从API获取的详细医生信息 + if (doctorInfo.value) { + return doctorInfo.value; + } + // 如果详细信息还未加载,返回基本信息 + if (groupInfo.value.doctor?.memberAccount) { + return { + doctorId: groupInfo.value.doctor.memberAccount, + name: groupInfo.value.doctor.name, + avatar: groupInfo.value.doctor.avatar + } + } + return null; + }) + + const patient = computed(() => { + if (groupInfo.value.patient?.memberAccount) { + return { openId: groupInfo.value.patient.memberAccount, name: groupInfo.value.patient.name, patientId: groupInfo.value.patient.patientId } + } + return null; + }) + const chatMember = computed(() => { + const res = { + assistant: { name: '医生助理', avatar: '/static/assistant.png' } + }; + if (assistant.value) { + res[assistant.value.assistantId] = { name: `医生助理${assistant.value.name}`, avatar: '/static/assistant.png' }; + } + if (doctor.value) { + + res[doctor.value.doctorId] = { name: `医生${doctor.value.name || ''}`, avatar: doctor.value.avatar || '/static/home/doctor.svg' }; + } + if (patient.value) { + res[patient.value.openId] = { name: '我' || `患者${patient.value.name}`, avatar: '/static/center/user-avatar.png' }; + } + return res; + }) + + const chatRoomBusiness = computed(() => groupInfo.value.groupBusiness || {}); + const isPending = computed(() => groupInfo.value.status === 'active'); + const isClosed = computed(() => !isPending.value); + const doctorInfo = ref(null) // 医生信息 + + const orderId = computed(() => { + return chatRoomBusiness.value.businessType === 'consultation' ? chatRoomBusiness.value.businessId : '' + }) + + const { currentOrder, chatRoomStatus, countdown, getCurrentOrder } = useChatOrder(orderId) + + const chatRoomCountDown = ref('') // 会话倒计时 + const showCountdown = computed(() => { + const isPendingOrder = currentOrder.value && chatRoomStatus.value.isPending && orderId.value == currentOrder.value.orderId; + const isRefill = chatRoomBusiness.value.businessType === 'refill_prescription' + return chatRoomCountDown.value && (isPendingOrder || isRefill) + }) + + async function getChatBusiness() { + if (!groupID.value) return; + const res = await getChatStatus(groupID.value); + if (res && res.success) { + groupInfo.value = res.data; + setCountdown() + getDoctor() + } else { + await toast('获取聊天室业务失败') + uni.navigateBack() + } + } + + function setCountdown() { + if (isPending.value && groupInfo.value.expireTime && dayjs(groupInfo.value.expireTime).isAfter(dayjs())) { + if (getApp().chatRoomCountDownTimer) clearInterval(getApp().chatRoomCountDownTimer); + getApp().chatRoomCountDownTimer = setInterval(() => { + const now = dayjs(); + const endTime = dayjs(groupInfo.value.expireTime); + const diff = endTime.diff(now, 'second'); + if (diff <= 0) { + clearCountdown(); + getChatBusiness() + return; + } + // 计算剩余时分秒 + const hours = Math.floor(diff / 3600); + const minutes = Math.floor((diff % 3600) / 60); + const seconds = diff % 60; + + const pad = n => n.toString().padStart(2, '0'); + chatRoomCountDown.value = `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`.replace(/^00\:/, ''); + }, 1000) + } else { + clearCountdown() + } + } + + function clearCountdown() { + if (getApp().chatRoomCountDownTimer) { + clearInterval(getApp().chatRoomCountDownTimer); + getApp().chatRoomCountDownTimer = null; + } + chatRoomCountDown.value = '' + } + + async function getDoctor() { + const doctorId = groupInfo.value.doctor?.memberAccount; + if (doctorId && (!doctorInfo.value || doctorInfo.value.doctorId !== doctorId)) { + const res = await getDoctorByDoctorId(doctorId) + doctorInfo.value = res && res.data ? res.data : null; + } + } + + onUnload(() => { + clearCountdown() + }) + + onShow(() => { + getChatBusiness() + }) + + + return { chatRoomBusiness, getChatBusiness, currentOrder, chatRoomStatus, countdown, getCurrentOrder, chatRoomCountDown, showCountdown, doctorInfo, isClosed, chatMember } +} \ No newline at end of file diff --git a/pages/message/index.vue b/pages/message/index.vue new file mode 100644 index 0000000..1fc6e9e --- /dev/null +++ b/pages/message/index.vue @@ -0,0 +1,993 @@ + + + + + + diff --git a/pages/work/department-select.vue b/pages/work/department-select.vue new file mode 100644 index 0000000..9f4e480 --- /dev/null +++ b/pages/work/department-select.vue @@ -0,0 +1,402 @@ + + + + + + diff --git a/pages/work/profile.vue b/pages/work/profile.vue new file mode 100644 index 0000000..f9e3eeb --- /dev/null +++ b/pages/work/profile.vue @@ -0,0 +1,288 @@ + + + + + + diff --git a/pages/work/work.vue b/pages/work/work.vue index 85ca984..0b2dce0 100644 --- a/pages/work/work.vue +++ b/pages/work/work.vue @@ -1,9 +1,213 @@ - - \ No newline at end of file diff --git a/routes/index.js b/routes/index.js index 4fba4e3..97275ac 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,8 +1,20 @@ export default [ { - path: 'pages/home/home', - meta: { title: '首页', login: true }, + path: 'pages/message/message', + meta: { title: '首页', login: false }, style: { navigationStyle: 'custom' } + }, + { + path: 'pages/work/work', + meta: { title: '工作台', login: false } + }, + { + path: 'pages/work/profile', + meta: { title: '完善个人信息', login: false } + }, + { + path: 'pages/work/department-select', + meta: { title: '选择科室', login: false } } ] diff --git a/store/account.js b/store/account.js index eb59baa..9698e03 100644 --- a/store/account.js +++ b/store/account.js @@ -2,6 +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"; const env = __VITE_ENV__; @@ -9,11 +10,16 @@ export default defineStore("accountStore", () => { const appid = env.MP_WX_APP_ID; const account = ref(null); const loading = ref(false) + // IM 相关 + const openid = ref(""); + // 医生信息 + const doctorInfo = ref(null); + async function login(phoneCode = '') { + debugger if (loading.value) return; loading.value = true; - try { const { code } = await uni.login({ appid, @@ -26,10 +32,23 @@ export default defineStore("accountStore", () => { phoneCode, code, }); - loading.value = false - console.log(res) - if (res.success && res.data && res.data.mobile) { + loading.value = false; + if (res.success && res.data) { + if (!res.data.mobile) { + const pages = getCurrentPages(); + const current = pages[pages.length - 1]; + const params = current && current.options + ? Object.keys(current.options).map(key => `${key}=${current.options[key]}`).join('&') + : ''; + const redirectUrl = current && current.route ? `/${current.route}${params ? `?${params}` : ''}` : ''; + const target = redirectUrl ? `/pages/login/login?redirect=${encodeURIComponent(redirectUrl)}` : '/pages/login/login'; + uni.redirectTo({ url: target }); + return; + } account.value = res.data; + openid.value = res.data.openid; + debugger; + await getDoctorInfo(openid.value); return res.data } } @@ -40,5 +59,18 @@ export default defineStore("accountStore", () => { loading.value = false } - return { account, login } + async function getDoctorInfo(weChatOpenId) { + try { + const res = await api('getCorpMemberData', { + weChatOpenId, + }); + if (res.success && res.data) { + doctorInfo.value = res.data; + } + } catch (e) { + console.error('获取医生信息失败:', e); + } + } + + return { account, openid, doctorInfo, login } }) \ No newline at end of file diff --git a/utils/api.js b/utils/api.js index 505caaf..6b4655c 100644 --- a/utils/api.js +++ b/utils/api.js @@ -7,6 +7,10 @@ const urlsConfig = { getTeamData: 'getTeamData', queryWxJoinedTeams: 'queryWxJoinedTeams', wxAppLogin: 'wxAppLogin', + getDeptList: 'getRealDeptList', + getHospitalList: 'getRealHospital', + addCorpMember: 'addCorpMember', + getCorpMemberData: 'getCorpMemberData' }, knowledgeBase: { @@ -24,6 +28,7 @@ const urlsConfig = { wecom: { addContactWay: 'addContactWay' } + } const urls = Object.keys(urlsConfig).reduce((acc, path) => { const config = urlsConfig[path] || {}; @@ -53,4 +58,5 @@ export default async function api(urlId, data) { type, } }) -} \ No newline at end of file +} + 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/file.js b/utils/file.js new file mode 100644 index 0000000..8875173 --- /dev/null +++ b/utils/file.js @@ -0,0 +1,75 @@ +const env = __VITE_ENV__; + +export async function uploadFile(tempFilePath, businessType, accessLevel = 'public') { + try { + const res = await new Promise((resolve, reject) => { + uni.uploadFile({ + url: `${env.MP_API_BASE_URL}/upload`, + filePath: tempFilePath, + name: 'file', + formData: { businessType, accessLevel }, + success: (resp) => resolve(resp), + fail: (err) => reject(err), + }); + }); + + const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data; + if (data && data.success) { + return data.data; + } + } catch (e) { + console.log('upload file error:', e); + } + return undefined; +} + +/** + * 选择单张图片并上传,成功返回上传结果(接口返回的 data.data) + * - 内部会处理 loading / toast + * - 失败或取消返回 null + */ +export async function chooseAndUploadImage(options = {}) { + const { + count = 1, + sizeType = ['compressed'], + sourceType = ['album', 'camera'], + businessType = 'other', + accessLevel = 'public', + loadingTitle = '上传中...', + successToast = '上传成功', + failToast = '上传失败', + } = options; + + const imageResult = await new Promise((resolve) => { + uni.chooseImage({ + count, + sizeType, + sourceType, + success: (res) => resolve(res), + fail: () => resolve(null), + }); + }); + + const tempFilePath = imageResult?.tempFilePaths?.[0]; + if (!tempFilePath) { + return null; + } + + uni.showLoading({ title: loadingTitle }); + try { + const uploadRes = await uploadFile(tempFilePath, businessType, accessLevel); + uni.hideLoading(); + if (uploadRes) { + uni.showToast({ title: successToast, icon: 'success' }); + return uploadRes; + } + uni.showToast({ title: failToast, icon: 'none' }); + return null; + } catch (e) { + uni.hideLoading(); + uni.showToast({ title: failToast, icon: 'none' }); + return null; + } +} + + diff --git a/utils/http.js b/utils/http.js index 4f7acc8..e29328e 100644 --- a/utils/http.js +++ b/utils/http.js @@ -97,6 +97,10 @@ async function refreshAccessToken() { const request = async (options = {}, showLoading = true) => { // 合并用户传入的配置和默认配置 if (!options.data) options.data = {}; + if(!options.data.corpId) { + options.data.corpId = env.MP_CORP_ID; + } + const config = { ...defaultOptions, diff --git a/utils/im-status-manager.js b/utils/im-status-manager.js new file mode 100644 index 0000000..7deb31a --- /dev/null +++ b/utils/im-status-manager.js @@ -0,0 +1,229 @@ +import { + checkGlobalIMStatus, + ensureGlobalIMConnection, + getGlobalIMLoginStatus, + setGlobalIMCallback +} from './tim-chat.js' + +/** + * 全局IM状态管理器 + * 提供统一的IM状态检测和管理接口 + */ +class IMStatusManager { + constructor() { + this.statusCheckInterval = null + this.isMonitoring = false + this.checkIntervalTime = 60000 // 默认1分钟检查一次 + this.lastCheckTime = 0 + this.callbacks = { + onStatusChange: [], + onReconnectSuccess: [], + onReconnectFailed: [] + } + } + + /** + * 启动IM状态监控 + * @param {number} intervalTime 检查间隔时间(毫秒) + */ + startMonitoring(intervalTime = 60000) { + if (this.isMonitoring) { + console.log('IM状态监控已在运行') + return + } + + this.checkIntervalTime = intervalTime + this.isMonitoring = true + + // 立即检查一次 + this.checkIMStatus() + + // 启动定时检查 + this.statusCheckInterval = setInterval(() => { + this.checkIMStatus() + }, this.checkIntervalTime) + + console.log(`IM状态监控已启动,检查间隔:${intervalTime / 1000}秒`) + } + + /** + * 停止IM状态监控 + */ + stopMonitoring() { + if (this.statusCheckInterval) { + clearInterval(this.statusCheckInterval) + this.statusCheckInterval = null + } + this.isMonitoring = false + console.log('IM状态监控已停止') + } + + /** + * 检查IM状态 + */ + async checkIMStatus() { + const now = Date.now() + this.lastCheckTime = now + + try { + console.log('执行IM状态检查...') + const isLoggedIn = checkGlobalIMStatus() + + // 触发状态变化回调 + this.triggerCallbacks('onStatusChange', { + isLoggedIn, + checkTime: now, + timestamp: new Date().toLocaleString() + }) + + if (!isLoggedIn) { + console.log('检测到IM未登录,尝试重连...') + await this.attemptReconnect() + } else { + console.log('IM状态正常') + } + + return isLoggedIn + } catch (error) { + console.error('IM状态检查异常:', error) + return false + } + } + + /** + * 尝试重连IM + */ + async attemptReconnect() { + try { + console.log('开始尝试IM重连...') + const success = await ensureGlobalIMConnection() + + if (success) { + console.log('IM重连成功') + this.triggerCallbacks('onReconnectSuccess', { + timestamp: new Date().toLocaleString() + }) + + } else { + console.log('IM重连失败') + this.triggerCallbacks('onReconnectFailed', { + timestamp: new Date().toLocaleString() + }) + } + + return success + } catch (error) { + console.error('IM重连异常:', error) + this.triggerCallbacks('onReconnectFailed', { + error, + timestamp: new Date().toLocaleString() + }) + return false + } + } + + /** + * 手动触发IM连接检查 + */ + async forceCheck() { + console.log('手动触发IM连接检查') + return await this.checkIMStatus() + } + + /** + * 获取当前IM登录状态 + */ + getCurrentStatus() { + return { + isLoggedIn: getGlobalIMLoginStatus(), + isMonitoring: this.isMonitoring, + lastCheckTime: this.lastCheckTime, + checkInterval: this.checkIntervalTime + } + } + + /** + * 添加状态变化回调 + */ + onStatusChange(callback) { + if (typeof callback === 'function') { + this.callbacks.onStatusChange.push(callback) + } + } + + /** + * 添加重连成功回调 + */ + onReconnectSuccess(callback) { + if (typeof callback === 'function') { + this.callbacks.onReconnectSuccess.push(callback) + } + } + + /** + * 添加重连失败回调 + */ + onReconnectFailed(callback) { + if (typeof callback === 'function') { + this.callbacks.onReconnectFailed.push(callback) + } + } + + /** + * 移除回调 + */ + removeCallback(type, callback) { + if (this.callbacks[type]) { + const index = this.callbacks[type].indexOf(callback) + if (index > -1) { + this.callbacks[type].splice(index, 1) + } + } + } + + /** + * 触发回调 + */ + triggerCallbacks(type, data) { + if (this.callbacks[type]) { + this.callbacks[type].forEach(callback => { + try { + callback(data) + } catch (error) { + console.error(`执行${type}回调失败:`, error) + } + }) + } + } + + /** + * 获取状态报告 + */ + getStatusReport() { + const status = this.getCurrentStatus() + return { + ...status, + report: { + isLoggedIn: status.isLoggedIn ? '已登录' : '未登录', + monitoring: status.isMonitoring ? '监控中' : '未监控', + lastCheck: status.lastCheckTime ? + new Date(status.lastCheckTime).toLocaleString() : '从未检查', + interval: `${status.checkInterval / 1000}秒` + } + } + } +} + +// 创建全局实例 +const globalIMStatusManager = new IMStatusManager() + +// 便捷函数 +export const startIMMonitoring = (interval) => globalIMStatusManager.startMonitoring(interval) +export const stopIMMonitoring = () => globalIMStatusManager.stopMonitoring() +export const checkIMStatusNow = () => globalIMStatusManager.forceCheck() +export const getIMStatus = () => globalIMStatusManager.getCurrentStatus() +export const getIMStatusReport = () => globalIMStatusManager.getStatusReport() + +// 导出管理器实例和类 +export { globalIMStatusManager, IMStatusManager } +export default globalIMStatusManager \ No newline at end of file diff --git a/utils/order.js b/utils/order.js new file mode 100644 index 0000000..ba6f485 --- /dev/null +++ b/utils/order.js @@ -0,0 +1,22 @@ +import { getOrderInfo } from "@/api/consult-order.js"; + +// 聊天页依赖的最小订单状态枚举(与后端 consult-order 一致) +export const orderStatus = { + INIT: "INIT", + PAID: "PAID", + CONSULTING: "CONSULTING", + COMPLETED: "COMPLETED", + CANCELLED: "CANCELLED", +}; + +/** + * 供 `pages/message/hooks/chat-order-hook.js` 使用: + * 根据 orderId 获取订单信息 + */ +export async function getChatOrder(orderId) { + const res = await getOrderInfo({ orderId }); + if (res && res.success) return res.data; + return Promise.reject(res?.message || "获取订单失败"); +} + + diff --git a/utils/tim-chat.js b/utils/tim-chat.js new file mode 100644 index 0000000..7682f0a --- /dev/null +++ b/utils/tim-chat.js @@ -0,0 +1,2652 @@ +// 引入腾讯IM SDK +import TIM from 'tim-wx-sdk' +import TIMUploadPlugin from 'tim-upload-plugin' +import { getUserSig, sendSystemMessage, getChatRecordsByGroupId } from '../api/corp/im.js' + +const env = __VITE_ENV__; + +// 腾讯IM配置 - SDKAppID 必须是 number 类型,使用 Number() 转换 +const TIM_CONFIG = { + SDKAppID: Number(env.APP_TIM_SDK_APP_ID), // 患者端 IM SDKAppID +} + +// 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 + + // 用户信息 + 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 + + // 回调函数 + this.callbacks = { + onMessageReceived: null, + onMessageSent: null, + onSDKReady: null, + onSDKNotReady: null, + onError: null, + onLoginStatusChanged: null, + 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) + } + } + + // ============== 资源管理方法 ============== + + // 清理所有定时器 + clearAllTimers() { + if (this.loginCheckInterval) { + clearTimeout(this.loginCheckInterval) + 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 + this.isLoggingIn = false + this.isInitializing = false + this.reconnectAttempts = 0 + this.lastLoginTime = 0 + this.heartbeatFailCount = 0 + } + + // ============== 初始化方法 ============== + + // 初始化腾讯IM + async initTIM(userID = null) { + if (this.isInitializing) { + console.log('IM正在初始化中,跳过重复初始化') + return false + } + + this.isInitializing = true + console.log('=== 开始初始化IM ===') + + try { + // 重置重连次数,允许重新登录 + this.reconnectAttempts = 0 + + // 如果存在旧的TIM实例,先完整清理 + if (this.tim) { + console.log('检测到旧的TIM实例,开始清理...') + await this.cleanupOldInstance() + } + + if (!TIM) { + throw new Error('TIM SDK 未正确导入') + } + + + 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}`) + + // 初始化失败时清理资源 + await this.cleanupOldInstance() + return false + } finally { + this.isInitializing = false + } + } + + // 清理旧的TIM实例 + async cleanupOldInstance() { + try { + // 清理所有定时器 + this.clearAllTimers() + + // 移除事件监听器 + this.removeEventListeners() + + // 登出 + if (this.tim && this.tim.isLoggedIn) { + try { + await this.tim.logout() + console.log('已登出旧实例') + } catch (err) { + 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实例已就绪') + resolve() + } else if (Date.now() - startTime > timeout) { + reject(new Error('等待TIM实例就绪超时')) + } else { + setTimeout(checkReady, IM_CONNECTION_CONFIG.TIM_INSTANCE_READY_CHECK_INTERVAL) + } + } + + 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 + + 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 { + console.log(`等待SDK Ready... ${Math.floor(elapsed/1000)}/${Math.floor(timeout/1000)}秒`) + 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 + } + + // 逐个注册事件监听器,如果某个注册失败则继续注册其他的 + 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') { + 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 { + const response = await getUserSig(userID) + if (response?.success && response?.data?.userSig) { + return response.data.userSig + } + 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('开始登录腾讯IM,userID:', this.currentUserID) + this.tim.login({ + userID: this.currentUserID, + userSig: this.currentUserSig + }).then(() => { + console.log('腾讯IM登录成功') + this.isLoggingIn = false + this.isLoggedIn = true + this.reconnectAttempts = 0 + + // 启动心跳检测 + this.startHeartbeat() + + // 启动登录状态检测 + this.startLoginStatusCheck() + + // 触发登录状态变化回调 + this.triggerCallback('onLoginStatusChanged', { + isLoggedIn: true, + userID: this.currentUserID, + reason: 'LOGIN_SUCCESS' + }) + + // 获取会话列表(确保连接正常) + this.getConversationList() + + resolve() + }).catch(error => { + console.error('腾讯IM登录失败:', error) + this.isLoggingIn = false + this.triggerCallback('onLoginStatusChanged', { + isLoggedIn: false, + error, + reason: 'LOGIN_FAILED' + }) + reject(error) + }) + }) + } + + // ============== 连接监控方法 ============== + + // 启动登录状态检测(优化版:使用配置常量) + startLoginStatusCheck() { + this.stopLoginStatusCheck() + + // 根据连接稳定性动态调整检查间隔 + const getCheckInterval = () => { + if (this.reconnectAttempts > 0) { + return IM_CONNECTION_CONFIG.LOGIN_CHECK_INTERVAL_UNSTABLE + } + 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('📡 登录状态检测:发现未登录,尝试重连') + this.attemptReconnect() + } else if (isLoggedIn) { + // 登录正常,重置重连计数 + if (this.reconnectAttempts > 0) { + 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}秒后进行`) + } + + // 停止登录状态检测 + 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 // 默认保持登录状态 + + 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) + // 出错时保持当前状态 + } + } + + // 只有 SDK 明确返回未登录时才更新状态 + if (!sdkLoginStatus) { + this.isLoggedIn = false + this.triggerCallback('onLoginStatusChanged', { + isLoggedIn: false, + userID: this.currentUserID, + reason: 'SDK_STATUS_CHECK' + }) + return false + } + + // 保持登录状态 + 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 + + // 只有非首次重连才需要检查冷却时间 + 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 + } + + 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) + + // 如果还有重连机会,自动安排下一次重连 + 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) + } + + 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() // 启动心跳检测 + this.getConversationList() + } + + // 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) + // 确保使用消息本身的conversationID,而不是当前会话ID + 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 + }) + 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) + + // 处理已读状态 + 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 + + // 更新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 + + // 更新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 + + 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(() => { + if (this.tim && !this.isLoggedIn && !this.isLoggingIn) { + console.log('🔄 网络已稳定,开始重连') + this.ensureIMConnection() + } else if (this.isLoggedIn) { + console.log('✓ 网络已稳定,IM连接正常') + } + 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', + message: '网络波动,将自动重连' + }) + } + } + + // 被踢下线处理 + 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: '提示', + content: '您的账号在其他设备登录,您已被踢下线', + showCancel: false, + confirmText: '确定', + success: () => { + // 清理本地缓存并跳转到登录页 + uni.removeStorageSync('token') + uni.removeStorageSync('refreshToken') + uni.removeStorageSync('account') + uni.removeStorageSync('openid') + uni.reLaunch({ + url: '/pages/login/login' + }) + } + }) + } + + // 手动检测并重连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() + + // 心跳失败计数器 + this.heartbeatFailCount = 0 + const MAX_HEARTBEAT_FAIL = IM_CONNECTION_CONFIG.HEARTBEAT_MAX_FAIL + const INTERVAL = IM_CONNECTION_CONFIG.HEARTBEAT_INTERVAL + + // 定时心跳检测 + this.heartbeatInterval = setInterval(() => { + // 如果已达到最大重连次数(被踢下线),停止心跳检测 + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.log('⚠️ 已被踢下线或达到最大重连次数,停止心跳检测') + this.stopHeartbeat() + return + } + + // 只在已登录状态下进行心跳检测 + if (!this.tim || !this.isLoggedIn) { + console.log('⏸ 心跳检测:未登录,跳过检测') + return + } + + this.tim.getConversationList() + .then(() => { + if (this.heartbeatFailCount > 0) { + console.log(`💚 心跳恢复正常(之前失败${this.heartbeatFailCount}次)`) + } + this.heartbeatFailCount = 0 // 重置失败计数 + }) + .catch((error) => { + this.heartbeatFailCount++ + console.error(`💔 心跳失败 (${this.heartbeatFailCount}/${MAX_HEARTBEAT_FAIL}):`, error.message) + + // 只有连续失败多次才认为真的断线 + if (this.heartbeatFailCount >= MAX_HEARTBEAT_FAIL) { + console.log('❌ 心跳连续失败,标记为未登录并尝试重连') + this.isLoggedIn = false + this.heartbeatFailCount = 0 + // 只有未被踢下线才尝试重连 + if (this.reconnectAttempts < this.maxReconnectAttempts) { + this.attemptReconnect() + } + } + }) + }, INTERVAL) + + console.log(`💓 心跳检测已启动(间隔${INTERVAL/1000}秒,最多失败${MAX_HEARTBEAT_FAIL}次)`) + } + + // 停止心跳检测 + stopHeartbeat() { + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval) + this.heartbeatInterval = null + console.log('IM心跳检测已停止') + } + } + + // 获取会话列表 + getConversationList() { + if (!this.tim) { + console.error('TIM实例不存在,无法获取会话列表') + return + } + + 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 + } + + this.tim.getConversationList({ withGroupInfo: 1, withAllFields: 1 }).then(response => { + if (this.conversationID) { + this.enterConversation(this.conversationID) + } + }).catch(error => { + console.error('获取会话列表失败:', error) + }) + } + + // 获取群聊列表 + getGroupList() { + return new Promise((resolve, reject) => { + if (!this.tim) { + reject(new Error('TIM实例不存在')) + return + } + + if (!this.isLoggedIn) { + console.log('SDK未ready,等待SDK初始化...') + const checkSDKReady = () => { + if (this.isLoggedIn) { + console.log('SDK已ready,开始获取群聊列表') + this.getGroupListInternal().then(resolve).catch(reject) + } else { + console.log('SDK仍未ready,继续等待...') + setTimeout(checkSDKReady, 1000) + } + } + checkSDKReady() + return + } + + this.getGroupListInternal().then(resolve).catch(reject) + }) + } + + // 内部获取群聊列表方法 + getGroupListInternal() { + 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') + }) + + console.log('群聊会话列表:', groupConversations) + + 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 + } + + 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 + } + } + } 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 = '[未知消息类型]' + } + + 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) + resolve({ + success: true, + groupList: groupsWithInfo, + totalCount: groupsWithInfo.length, + data: conversationResponse.data + }) + }).catch((imError) => { + console.error('获取会话列表失败:', imError) + reject({ + success: false, + error: imError + }) + }) + }) + } + + // 创建问诊群聊 + 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 { + await sendSystemMessage(groupID, 'pending') + 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) + + // 更新当前会话ID + this.currentConversationID = conversationID + + // 清空当前消息列表 + this.messageList = [] + + // 重置分页状态 + this.nextReqMessageID = "" + this.isCompleted = false + this.isLoadingMore = false + + console.log(" 会话ID已更新,消息列表已清空,分页状态已重置") + + // 进入群聊会话,默认加载20条消息 + this.enterGroupConversation(conversationID, 20) + } + + // 进入群聊会话 + async enterGroupConversation(groupID, count = 20) { + console.log("【enterGroupConversation】进入群聊会话, groupID:", groupID, "count:", count) + + 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) + + // 调用本地接口获取聊天记录 + const response = await getChatRecordsByGroupId(groupID, count, skip) + + console.log(" 📥 收到本地API响应:", response) + + 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) { + // 上拉加载更多:将新消息插入到列表前面(历史消息) + this.messageList.unshift(...convertedMessages) + // 再次排序确保顺序正确 + this.messageList.sort((a, b) => a.lastTime - b.lastTime) + } else { + // 首次加载:直接替换消息列表 + this.messageList = convertedMessages + } + + // 设置分页状态 - 使用后端返回的 hasMore 字段 + this.isCompleted = !hasMore + + console.log(` 分页状态: isCompleted=${this.isCompleted}, hasMore=${hasMore}`) + + // 触发回调 + this.triggerCallback("onMessageListLoaded", { + messages: this.messageList, + isPullUp: isPullUp, + hasMore: hasMore, + isCompleted: this.isCompleted, + total: total, + }) + + 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, + } + } + } + + // 将数据库消息转换为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) { + // MsgTime 是秒级时间戳 + lastTime = dbMsg.MsgTime * 1000 + } 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)}`, + from: dbMsg.From_Account || '', + flow: flow, + type: msgType, + payload: this.convertDBPayloadToIMPayload(msgType, msgBody.MsgContent), + lastTime: lastTime, + status: 'success', + avatar: flow === 'in' ? '/static/home/doctor.png' : '/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 || '', + 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 + } + + case 'TIMFileElem': + return { + url: msgContent.Url || '', + fileSize: msgContent.FileSize || 0, + fileName: msgContent.FileName || '', + downloadFlag: msgContent.Download_Flag || 2 + } + + 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 + + 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 + + // 获取群组ID + let groupID = '' + if (this.conversation && this.conversation.groupProfile) { + groupID = this.conversation.groupProfile.groupID + } else if (this.currentConversationID) { + groupID = this.currentConversationID.replace('GROUP', '') + } + + if (!groupID) { + console.error(" ❌ 无法获取群组ID") + this.isLoadingMore = false + return { + success: false, + 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) + + 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) + + this.messageList = filteredMessages + + console.log(`消息刷新完成,过滤后${filteredMessages.length}条消息`) + + // 触发更新回调 + 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() + + // 先添加服务器消息 + serverMessages.forEach(msg => { + if (msg.ID) { + messageMap.set(msg.ID, msg) + } + }) + + // 再添加缓存消息(覆盖服务器消息,保留本地最新状态) + 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) + } + } + }) + + // 转换为数组并按时间排序 + const mergedArray = Array.from(messageMap.values()) + mergedArray.sort((a, b) => a.lastTime - b.lastTime) + + console.log(`消息合并完成: 服务器${serverMessages.length}条, 缓存${cachedMessages.length}条, 合并后${mergedArray.length}条`) + + 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)) + + 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未初始化') + return + } + + if (!this.conversation) { + this.triggerCallback('onError', '群聊会话不存在') + return { success: false, error: '群聊会话不存在' } + } + + 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', '') + } + + if (!groupID) { + 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', + 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' + return { success: false, error } + } + } + + // 发送图片消息 + async sendImageMessage(imageFile) { + if (!this.tim) { + this.triggerCallback('onError', 'IM未初始化') + return + } + + if (!this.conversation) { + this.triggerCallback('onError', '群聊会话不存在') + return { success: false, error: '群聊会话不存在' } + } + + 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', '') + } + + if (!groupID) { + this.triggerCallback('onError', '无法获取群聊ID') + return { success: false, error: '无法获取群聊ID' } + } + + // 确保使用当前会话的conversationID + const conversationID = this.conversation.conversationID || this.currentConversationID + + // 获取图片尺寸信息 + const imageInfo = await this.getImageInfo(imageFile); + + const localMessage = { + ID: `local_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + flow: 'out', + type: 'TIMImageElem', + payload: { + imageInfoArray: [{ + url: this.getImageUrl(imageFile), + 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) + + const message = this.tim.createImageMessage({ + to: groupID, + conversationType: TIM.TYPES.CONV_GROUP, + payload: { file: imageFile } + }) + + try { + await this.tim.sendMessage(message) + localMessage.status = 'success' + return { success: true, message: localMessage } + } catch (error) { + console.error('图片消息发送失败:', error) + localMessage.status = 'failed' + return { success: false, error } + } + } + + // 发送语音消息 + async sendVoiceMessage(voiceFile, duration) { + if (!this.tim) { + this.triggerCallback('onError', 'IM未初始化') + return + } + + if (!this.conversation) { + this.triggerCallback('onError', '群聊会话不存在') + return { success: false, error: '群聊会话不存在' } + } + + 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', '') + } + + if (!groupID) { + 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', + 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' + return { success: false, error } + } + } + + // 发送自定义消息 + async sendCustomMessage(messageData) { + if (!this.tim) { + this.triggerCallback('onError', 'IM未初始化') + return + } + + if (!this.conversation) { + this.triggerCallback('onError', '群聊会话不存在') + return { success: false, error: '群聊会话不存在' } + } + + 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', '') + } + + if (!groupID) { + 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', + 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' + 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', + avatar: timMessage.flow === 'in' ? '/static/home/doctor.png' : '/static/center/user-avatar.png', + // 优先使用消息本身的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) { + if (imageFile?.tempFiles?.length > 0) { + return imageFile.tempFiles[0].tempFilePath + } + if (typeof imageFile === 'string') { + return imageFile + } + return '/static/home/photo.png' + } + + // 获取图片尺寸信息 + getImageInfo(imageFile) { + return new Promise((resolve) => { + let imagePath = ''; + + // 获取图片路径 + if (imageFile?.tempFiles?.length > 0) { + imagePath = imageFile.tempFiles[0].tempFilePath; + } else if (typeof imageFile === 'string') { + imagePath = imageFile; + } else { + // 默认尺寸 + resolve({ width: 400, height: 300 }); + return; + } + + // 使用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资源 ===') + + // 清理所有定时器(使用统一方法) + this.clearAllTimers() + + // 清理消息缓存(已弃用) + 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() + + // 清理其他状态 + this.messageList = [] + this.conversation = null + this.currentUserID = '' + this.currentUserSig = '' + this.currentConversationID = null + this.tim = null + + console.log('=== IM资源清理完成 ===') + } +} + +// 创建全局IM管理器实例 +const globalTimChatManager = new TimChatManager() + +// 全局IM初始化函数 - 支持强制重新初始化 +let initPromise = null; +const initGlobalTIM = async (userID, forceReinit = false) => { + console.log('开始初始化全局IM,userID:', 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); + return initPromise; +} +const clearInitIMPromise = () => { + initPromise = null; +} + +// 获取群聊列表(全局函数) +const getGroupList = async () => { + return await globalTimChatManager.getGroupList() +} + +// 进入群聊 +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}`, + }) +} + +// 检查全局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