// ==================== ChatClient 模块定义 (ES6 兼容) ==================== layui.define(['jquery', 'layer', 'laytpl', 'util'], function (exports) { "use strict"; class ChatClient { constructor(config) { this.config = Object.assign({ wsUrl: 'wss://echo.websocket.org', token: '', uid: '', reconnectDelay: 3000, maxReconnect: 5 }, config); this.ws = null; this.reconnectCount = 0; this.messageQueue = []; this.isConnected = false; this.currentSession = { id: '', type: '' }; this.requestCallbacks = {}; this.heartbeatTimer = null; this.eventHandlers = {}; this.init(); } /** * 系统事件 * @param {*} event * @param {*} handler * @returns */ on(event, handler) { if (!this.eventHandlers[event]) this.eventHandlers[event] = []; this.eventHandlers[event].push(handler); return this; } /** * 触发 * @param {*} event * @param {...any} args * @returns */ emit(event, ...args) { if (this.eventHandlers[event]) { this.eventHandlers[event].forEach(handler => handler.apply(this, args)); } return this; } /** * 初始化 */ init() { this.connect(); window.addEventListener('beforeunload', () => this.disconnect()); } /** * 链接 */ connect() { try { this.ws = new WebSocket(this.config.wsUrl); this.ws.onopen = () => { console.log('✅ WebSocket 连接成功'); this.isConnected = true; this.reconnectCount = 5; this.sendAuth(); this.startHeartbeat(); this.emit('connect'); // 重发队列消息 while (this.messageQueue.length > 0) { const msg = this.messageQueue.shift(); this.ws.send(JSON.stringify(msg)); } }; this.ws.onmessage = (event) => { try { const packet = JSON.parse(event.data); this.handleMessage(packet); } catch (e) { console.error('❌ 消息解析失败:', e); this.emit('error', e); } }; this.ws.onerror = (error) => { console.error('❌ WebSocket 错误:', error); this.emit('error', error); }; this.ws.onclose = () => { console.log('⚠️ WebSocket 连接关闭'); this.isConnected = false; this.stopHeartbeat(); this.emit('close'); this.attemptReconnect(); }; } catch (e) { console.error('❌ WebSocket 初始化失败:', e); this.attemptReconnect(); } } /** * 发送验证信息 */ sendAuth() { this.send({ content: this.config.token }, 'sys', 'auth'); } /** * 开始心跳 */ startHeartbeat() { this.stopHeartbeat(); this.heartbeatTimer = setInterval(() => { if (this.isConnected && this.ws.readyState === WebSocket.OPEN) { this.send({}, 'sys', 'heartbeat'); } }, 30000); } /** * 停止心跳 */ stopHeartbeat() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } } /** * 重连 */ attemptReconnect() { if (this.reconnectCount < this.config.maxReconnect && !this.isConnected) { this.reconnectCount++; console.log(`🔄 尝试重连 (${this.reconnectCount}/${this.config.maxReconnect})...`); setTimeout(() => this.connect(), this.config.reconnectDelay * this.reconnectCount); } else { this.emit('reconnect-failed'); } } /** * 发送 * @param {*} payload * @param {*} msgType * @param {*} options * @returns */ send(payload, _frame, type, options = {}) { const packet = { header: { id: 'pkt_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5), frame: _frame, type: type, from: this.user?.uid, timestamp: Date.now() }, payload: payload || {} }; // 存储回调 if (typeof options.callback === 'function') { this.requestCallbacks[packet.header.id] = { callback: options.callback, timeout: setTimeout(() => { if (this.requestCallbacks[packet.header.id]) { this.requestCallbacks[packet.header.id].callback({ header: { type: 'timeout' }, payload: { message: '请求超时' } }); delete this.requestCallbacks[packet.header.id]; } }, 10000) }; } if (this.isConnected && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(packet)); } else { this.messageQueue.push(packet); if (!this.isConnected) this.connect(); } return packet.header.id; } handleMessage(packet) { const frame = packet.header?.frame; // if (packet.header?.id) { // const origId = packet.header.id; // if (this.requestCallbacks[origId]) { // clearTimeout(this.requestCallbacks[origId].timeout); // this.requestCallbacks[origId].callback(packet); // delete this.requestCallbacks[origId]; // return; // } // } // 业务消息分发 switch (frame) { case 'sys': this.handleSystem(packet); break; case 'chat': this.emit('friend-apply', packet.payload); break; case 'rel': this.emit('message-read', packet.payload); break; case 'ctl': this.emit('friend-apply-result', packet.payload); break; case 'err': this.emit('group-invite', packet.payload); break; case 'auth': this.emit('auth-response', packet.payload); break; case 'heartbeat': this.emit('auth-response', packet.payload); break; default: this.emit('message', packet); } } handleSystem(packet) { const type = packet.header?.type; switch (type) { case 'heartbeat': break; case 'auth': this.user = packet.payload.content ; break; } } // 业务方法(只处理数据,不涉及渲染) sendMessage(sessionId, sessionType, content, msgType = 'text') { const message = { header: { id: 'msg_' + Date.now(), type: msgType, subtype: sessionType, from: this.user.uid, to: sessionId, timestamp: new Date().toISOString() }, payload: { content: content, } }; this.ws.send(JSON.stringify(packet)); // // 发送消息到服务器 // this.send({ // session_id: sessionId, // session_type: sessionType, // content: content, // type: msgType, // id: msgId // }, 'send_message', { // callback: (res) => { // if (res.payload?.success) { // // 更新本地消息状态为已发送 // message.read_status = 'sent'; // this.emit('message-sent', message); // } else { // message.read_status = 'failed'; // this.emit('message-failed', message); // } // } // }); // 立即触发本地消息事件(用于前端渲染) this.emit('local-message', message); return msgId; } /** * 发送图片 * @param {*} sessionId * @param {*} sessionType * @param {*} imgUrl * @returns */ sendImage(sessionId, sessionType, imgUrl) { return this.sendMessage(sessionId, sessionType, `[图片]${imgUrl}`, 'image'); } /**发送读取状态 */ sendReadReceipt(msgId, sessionId, sessionType) { this.send({ id: msgId, session_id: sessionId, session_type: sessionType }, 'read_receipt'); } /**设置会话 */ setCurrentSession(sessionId, sessionType) { this.currentSession = { id: sessionId, type: sessionType }; this.emit('session-changed', this.currentSession); } /** * 搜索用户 * @param {*} keyword * @param {*} callback */ searchUsers(keyword, callback) { // 实际项目中替换为真实API调用 setTimeout(() => { const mockUsers = [ { uid: 'u2001', nickname: '设计师小王', avatar: 'W', is_friend: false }, { uid: 'u2002', nickname: '产品经理李', avatar: 'L', is_friend: true }, { uid: 'u2003', nickname: '后端工程师赵', avatar: 'Z', is_friend: false } ].filter(u => u.nickname.includes(keyword) || u.uid.includes(keyword)); callback({ code: 0, data: mockUsers }); }, 300); } /** * 添加好友 * @param {*} targetUid * @param {*} remark * @param {*} callback */ addFriend(targetUid, remark, callback) { this.send({ target_uid: targetUid, remark: remark || '你好,加个好友吧~' }, 'add_friend_request', { callback }); } /** * 接受好友 * @param {*} applyId * @param {*} agree * @param {*} callback */ handleFriendApply(applyId, agree, callback) { this.send({ apply_id: applyId, action: agree ? 'agree' : 'reject' }, 'handle_friend_apply', { callback }); } disconnect() { this.stopHeartbeat(); if (this.ws) { this.ws.close(); this.ws = null; } this.isConnected = false; } } // 导出模块 exports('chatClient', ChatClient); });