Files

364 lines
9.7 KiB
JavaScript

// ==================== 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);
});