/** * 聊天室管理器 * 封装聊天室相关操作,包括进入房间、离开房间、发送消息、接收消息、管理未读数等 */ import { chatConfig } from '@/config/chat.js'; import { sendWebSocketMessage, isWebSocketConnected } from '@/utils/ws/initWebSocket.js'; import { getChatRegisterInfoApi, getMessagesByRoomIdApi } from '@/api/chat.js'; import { getMessagePreview } from '@/utils/chat/messagePreview.js'; function unwrapMessagesPayload(res) { if (res == null) return null; if (res.result !== undefined && res.result !== null) return res.result; if (res.data && res.data.result !== undefined && res.data.result !== null) return res.data.result; if (res.data && typeof res.data === 'object') return res.data; return res; } /** 房间 ID 归一化,便于 WS 与列表接口字段比较 */ export function normalizeRoomId(id) { return String(id == null ? '' : id).trim(); } /** 仅在工作台 / 在线接诊列表为栈顶页时派发列表增量,减少后台页无意义合并 */ function shouldEmitDoctorImListUpdate() { const pages = typeof getCurrentPages === 'function' ? getCurrentPages() : []; const cur = pages && pages.length ? pages[pages.length - 1] : null; const route = cur && cur.route ? cur.route : ''; return ( route === 'pages/workbench/index' || route === 'subPackages/sub_online_reception/pages/list' ); } /** * 聊天室管理器类 */ class ChatRoomManager { constructor() { // 当前房间ID this.currentRoomId = null; // 最后一条消息ID(用于分页加载) this.lastMessageId = 0; // 未读数(按房间ID存储) this.unreadCounts = {}; // 消息缓存(按房间ID存储) this.roomMessages = {}; // 消息处理器列表 this.messageHandlers = []; } /** * 进入聊天室 * @param {string} roomId - 房间ID * 职责:设置当前房间,重置未读数,保存到本地存储 */ enterRoom(roomId) { const id = normalizeRoomId(roomId); if (!id) { console.warn('房间ID不能为空'); return; } this.currentRoomId = id; uni.setStorageSync('currentRoomId', id); this.resetUnreadCount(id); console.log('进入聊天室:', id); } /** * 离开聊天室 * 职责:清空当前房间,移除本地存储 */ leaveRoom() { this.currentRoomId = null; uni.removeStorageSync('currentRoomId'); console.log('离开聊天室'); } /** * 获取当前房间ID * @returns {string|null} 当前房间ID */ getCurrentRoomId() { return this.currentRoomId || uni.getStorageSync('currentRoomId'); } /** * 增加未读数 * @param {string} roomId - 房间ID * 职责:如果房间不是当前房间,则增加未读数并通知 */ increaseUnreadCount(roomId) { const key = normalizeRoomId(roomId); if (!key) return; if (normalizeRoomId(this.getCurrentRoomId()) === key) { return; } this.unreadCounts[key] = (this.unreadCounts[key] || 0) + 1; this.notifyUnreadChange(); } /** * 重置未读数 * @param {string} roomId - 房间ID * 职责:将指定房间的未读数重置为0并通知 */ resetUnreadCount(roomId) { const key = normalizeRoomId(roomId); if (this.unreadCounts[key]) { this.unreadCounts[key] = 0; this.notifyUnreadChange(); } } /** * 获取未读数 * @param {string} roomId - 房间ID * @returns {number} 未读数 */ getUnreadCount(roomId) { const key = normalizeRoomId(roomId); return this.unreadCounts[key] || 0; } /** * 获取所有未读数总和 * @returns {number} 总未读数 */ getAllUnreadCount() { return Object.values(this.unreadCounts).reduce((sum, count) => sum + count, 0); } /** * 格式化未读数显示 * @param {string} roomId - 房间ID * @returns {string} 格式化后的未读数 */ getFormattedUnreadCount(roomId) { const count = this.getUnreadCount(roomId); return chatConfig.formatUnreadCount(count); } /** * 通知未读数变化 * 职责:触发未读数更新事件 */ notifyUnreadChange() { uni.$emit('unread-message-update', { total: this.getAllUnreadCount(), byRoom: { ...this.unreadCounts } }); } /** * 添加消息到房间 * @param {string} roomId - 房间ID * @param {Object} message - 消息对象 * 职责:将消息添加到指定房间的消息列表 */ addMessageToRoom(roomId, message) { const key = normalizeRoomId(roomId); if (!key || !message) return; if (!this.roomMessages[key]) { this.roomMessages[key] = []; } // 检查消息是否已存在(避免重复) const existingIndex = this.roomMessages[key].findIndex(m => m.id === message.id); if (existingIndex !== -1) { this.roomMessages[key][existingIndex] = message; } else { this.roomMessages[key].push(message); } } /** * 获取房间消息列表 * @param {string} roomId - 房间ID * @returns {Array} 消息列表 */ getRoomMessages(roomId) { const key = normalizeRoomId(roomId); return this.roomMessages[key] || []; } /** * 加载房间消息 * @param {string} roomId - 房间ID * @param {number} lastMessageId - 最后一条消息ID(可选,用于分页) * @returns {Promise} 消息列表 */ async loadRoomMessages(roomId, lastMessageId = 0) { const key = normalizeRoomId(roomId); if (!key) { console.warn('房间ID不能为空'); return []; } try { const res = await getMessagesByRoomIdApi({ room_id: key, last_message_id: lastMessageId || 0 }); const payload = unwrapMessagesPayload(res); if (payload && typeof payload === 'object') { const rawList = payload.list; const list = Array.isArray(rawList) ? rawList : rawList != null ? [rawList] : []; const messages = this.processMessages(list); this.lastMessageId = payload.last_message_id || 0; // 如果是首次加载,替换消息列表;否则追加到前面 if (lastMessageId === 0) { this.roomMessages[key] = messages; } else { this.roomMessages[key] = [...messages, ...(this.roomMessages[key] || [])]; } uni.$emit('load-room-message', lastMessageId === 0 ? 1 : 0); return messages; } return []; } catch (error) { console.error('加载房间消息失败:', error); return []; } } /** * 处理消息数据 * @param {Array|Object} messages - 消息数据(数组或单个消息) * @returns {Array|Object} 处理后的消息 */ processMessages(messages) { if (!Array.isArray(messages)) { return this.processSingleMessage(messages); } return messages.map((item) => { const result = { id: item.id, sender_user_id: item.sender_user_id, receiver_user_id: item.receiver_user_id, message_type: item.message_type, message_content: item.message_content, duration: item.duration, room_id: item.room_id, created_at: item.created_at || item.send_time, created_at_text: item.created_at_text || this.formatTime(item.send_time || item.created_at), timestamp: item.send_time || item.created_at }; // 如果是挂号消息(type=10),异步加载详细信息 if (item.message_type === 10) { this.loadRegisterInfo(result); } return result; }); } /** * 处理单个消息 * @param {Object} message - 消息对象 * @returns {Object} 处理后的消息 */ processSingleMessage(message) { return { id: message.id, sender_user_id: message.sender_user_id, receiver_user_id: message.receiver_user_id, message_type: message.message_type, message_content: message.message_content, duration: message.duration, room_id: message.room_id, created_at: message.created_at || message.send_time, created_at_text: message.created_at_text || this.formatTime(message.send_time || message.created_at), timestamp: message.send_time || message.created_at }; } /** * 格式化时间(HH:mm),时间戳支持秒或毫秒 * @param {number} timestamp * @returns {string} */ formatTime(timestamp) { if (timestamp === undefined || timestamp === null || timestamp === '') return ''; const n = Number(timestamp); if (Number.isNaN(n)) return ''; const ms = n > 1e12 ? n : n * 1000; const date = new Date(ms); const hours = date.getHours().toString().padStart(2, '0'); const minutes = date.getMinutes().toString().padStart(2, '0'); return `${hours}:${minutes}`; } /** * 加载挂号信息(异步) * @param {Object} message - 消息对象 * 职责:为挂号消息加载详细信息 */ loadRegisterInfo(message) { const parsedContent = this.parseMessageContent(message.message_content); if (parsedContent._loaded || !parsedContent.id) return; getChatRegisterInfoApi({ id: parsedContent.id, message_id: message.id || '' }).then((res) => { const info = res && (res.result != null ? res.result : res.data && res.data.result); if (res && info) { const resultWithFlag = { ...info, _loaded: true }; const roomId = message.room_id; const roomMessages = this.roomMessages[roomId]; if (roomMessages) { const index = roomMessages.findIndex(m => m.id === message.id); if (index !== -1) { roomMessages[index].message_content = JSON.stringify(resultWithFlag); uni.$emit('message-updated', { roomId, messageId: message.id }); } } } }).catch(error => { console.error('加载挂号信息失败:', error); }); } /** * 解析消息内容 * @param {string|Object} content - 消息内容 * @returns {Object|string} 解析后的内容 */ parseMessageContent(content) { try { return typeof content === 'object' ? content : JSON.parse(content); } catch (e) { return content; } } /** * 通过 WebSocket 发送消息(request_type: send_message)。 * 在线接诊聊天页(sub_online_reception/pages/chat.vue)主路径已改为 Go IM `POST /api/send-to-user`(与 websocket 配置同基址), * 此处保留供其它仍走 WS 直发的场景;新页面发聊天消息请优先使用 HTTP API。 * @param {Object} messageData - 消息数据 * @param {string} messageData.room_id - 房间ID * @param {string} messageData.sender_user_id - 发送者ID * @param {string} messageData.receiver_user_id - 接收者ID * @param {number|string} messageData.message_type - 消息类型 * @param {string} messageData.message_content - 消息内容 * @param {number} messageData.duration - 语音时长(可选) * @returns {boolean} 是否发送成功 */ sendMessage(messageData) { if (!isWebSocketConnected()) { console.error('WebSocket未连接,无法发送消息'); return false; } // 确保消息类型是数字 let messageType = messageData.message_type; if (typeof messageType === 'string') { messageType = chatConfig.getMessageType(messageType); } const message = { request_type: 'send_message', room_id: messageData.room_id, sender_user_id: messageData.sender_user_id, receiver_user_id: messageData.receiver_user_id, message_type: messageType, message_content: messageData.message_content || '', duration: messageData.duration || 0 }; return sendWebSocketMessage(message); } /** * 注册消息处理器 * @param {Function} handler - 消息处理函数 * 职责:注册消息处理器,当收到WebSocket消息时调用 */ onMessage(handler) { if (typeof handler !== 'function') { console.warn('消息处理器必须是函数'); return; } this.messageHandlers.push(handler); } /** * 移除消息处理器 * @param {Function} handler - 消息处理函数 */ offMessage(handler) { const index = this.messageHandlers.indexOf(handler); if (index !== -1) { this.messageHandlers.splice(index, 1); } } /** * 处理收到的WebSocket消息 * @param {Object} data - WebSocket消息数据 * 职责:处理收到的消息,更新未读数,调用注册的处理器 */ handleWebSocketMessage(data) { // 只处理聊天消息 if (data.request_type !== 'receive_message' && !data.room_id) { return; } const roomId = normalizeRoomId(data.room_id); if (!roomId) return; const dataNorm = { ...data, room_id: roomId }; // 处理消息 const message = this.processSingleMessage(dataNorm); // 添加到房间消息列表 this.addMessageToRoom(roomId, message); const currentNorm = normalizeRoomId(this.getCurrentRoomId()); const incrementUnread = currentNorm !== roomId; // 如果不是当前房间,增加未读数 if (incrementUnread) { this.increaseUnreadCount(roomId); } // 调用所有注册的处理器 this.messageHandlers.forEach(handler => { try { handler(message, roomId); } catch (error) { console.error('消息处理器执行失败:', error); } }); const lastMessageTime = dataNorm.send_time != null && dataNorm.send_time !== '' ? dataNorm.send_time : dataNorm.created_at != null && dataNorm.created_at !== '' ? dataNorm.created_at : message.timestamp != null && message.timestamp !== '' ? message.timestamp : message.created_at; const unreadForRoom = this.getUnreadCount(roomId); if (shouldEmitDoctorImListUpdate()) { uni.$emit('doctor-im-room-update', { roomId, last_message_preview: getMessagePreview(dataNorm), last_message_time: lastMessageTime, incrementUnread, unreadForRoom }); } } /** * 清空房间消息缓存 * @param {string} roomId - 房间ID(可选,不传则清空所有) */ clearRoomCache(roomId = null) { if (roomId) { const key = normalizeRoomId(roomId); if (key) delete this.roomMessages[key]; } else { this.roomMessages = {}; } } } // 导出单例 export const chatRoomManager = new ChatRoomManager();