364 lines
9.7 KiB
JavaScript
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);
|
|
}); |