- 替换所有文件中的品牌名称引用,包括App.vue、pages.json、uni.scss等 - 更新全局样式、导航栏标题和组件中的文案显示 - 修改分享功能中的应用名称显示 - 调整聊天页面的输入栏键盘适配逻辑 - 在多个页面组件中添加事件发射声明以支持交互功能
254 lines
6.3 KiB
JavaScript
254 lines
6.3 KiB
JavaScript
/* 碰盏日记 - WebSocket 管理器
|
|
* 单例模式,负责私信实时通信
|
|
* 支持心跳、自动重连、消息队列
|
|
*/
|
|
|
|
const WS_HOST = 'wss://dev.wash-painting.cn/ws'
|
|
|
|
class WebSocketManager {
|
|
constructor() {
|
|
this.socketTask = null
|
|
this.isConnected = false
|
|
this.isConnecting = false
|
|
this.listeners = {} // 事件订阅 { event: [callbacks] }
|
|
this.messageQueue = [] // 断线期间暂存消息
|
|
this.heartbeatTimer = null
|
|
this.heartbeatTimeout = null
|
|
this.reconnectTimer = null
|
|
this.reconnectAttempts = 0
|
|
this.maxReconnectDelay = 30000
|
|
this.heartbeatInterval = 30000 // 30s 发一次 ping
|
|
this.heartbeatWait = 60000 // 60s 无 pong 则重连
|
|
this.manualClose = false
|
|
this.token = ''
|
|
}
|
|
|
|
/**
|
|
* 建立 WebSocket 连接
|
|
* @param {string} token - 用户认证 token
|
|
*/
|
|
connect(token) {
|
|
if (this.isConnected || this.isConnecting) return
|
|
this.token = token || uni.getStorageSync('auth_token') || ''
|
|
if (!this.token) {
|
|
console.warn('[WS] 无 token,跳过连接')
|
|
return
|
|
}
|
|
this.manualClose = false
|
|
this.isConnecting = true
|
|
|
|
const url = `${WS_HOST}?token=${this.token}`
|
|
console.log('[WS] 正在连接...', url)
|
|
|
|
this.socketTask = uni.connectSocket({
|
|
url,
|
|
complete: () => {}
|
|
})
|
|
|
|
this.socketTask.onOpen(() => {
|
|
console.log('[WS] 连接成功')
|
|
this.isConnected = true
|
|
this.isConnecting = false
|
|
this.reconnectAttempts = 0
|
|
this._startHeartbeat()
|
|
this._flushQueue()
|
|
this._emit('connect', {})
|
|
})
|
|
|
|
this.socketTask.onMessage((res) => {
|
|
try {
|
|
const msg = JSON.parse(res.data)
|
|
this._handleMessage(msg)
|
|
} catch (e) {
|
|
console.warn('[WS] 消息解析失败', res.data)
|
|
}
|
|
})
|
|
|
|
this.socketTask.onClose(() => {
|
|
console.log('[WS] 连接关闭')
|
|
this.isConnected = false
|
|
this.isConnecting = false
|
|
this._stopHeartbeat()
|
|
this._emit('disconnect', {})
|
|
if (!this.manualClose) {
|
|
this._scheduleReconnect()
|
|
}
|
|
})
|
|
|
|
this.socketTask.onError((err) => {
|
|
console.warn('[WS] 连接错误', err)
|
|
this.isConnected = false
|
|
this.isConnecting = false
|
|
})
|
|
}
|
|
|
|
/** 主动断开连接 */
|
|
disconnect() {
|
|
this.manualClose = true
|
|
this._stopHeartbeat()
|
|
this._clearReconnect()
|
|
if (this.socketTask) {
|
|
this.socketTask.close()
|
|
this.socketTask = null
|
|
}
|
|
this.isConnected = false
|
|
}
|
|
|
|
/**
|
|
* 发送消息
|
|
* @param {string} type - 消息类型: chat | typing | read | ping
|
|
* @param {object} data - 消息数据
|
|
*/
|
|
send(type, data = {}) {
|
|
const payload = JSON.stringify({ type, data, ts: Date.now() })
|
|
if (this.isConnected && this.socketTask) {
|
|
this.socketTask.send({ data: payload })
|
|
} else {
|
|
// 断线暂存队列
|
|
if (type !== 'ping') {
|
|
this.messageQueue.push(payload)
|
|
}
|
|
// 尝试重连
|
|
if (!this.isConnecting && !this.manualClose) {
|
|
this._scheduleReconnect()
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 订阅事件
|
|
* @param {string} event - message | typing | read | ack | connect | disconnect
|
|
* @param {function} callback
|
|
*/
|
|
on(event, callback) {
|
|
if (!this.listeners[event]) {
|
|
this.listeners[event] = []
|
|
}
|
|
this.listeners[event].push(callback)
|
|
}
|
|
|
|
/**
|
|
* 取消订阅
|
|
* @param {string} event
|
|
* @param {function} callback
|
|
*/
|
|
off(event, callback) {
|
|
if (!this.listeners[event]) return
|
|
if (callback) {
|
|
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback)
|
|
} else {
|
|
delete this.listeners[event]
|
|
}
|
|
}
|
|
|
|
// ========== 内部方法 ==========
|
|
|
|
/** 处理收到的消息 */
|
|
_handleMessage(msg) {
|
|
const { type, data } = msg
|
|
switch (type) {
|
|
case 'pong':
|
|
this._onPong()
|
|
break
|
|
case 'chat':
|
|
this._emit('message', data)
|
|
break
|
|
case 'typing':
|
|
this._emit('typing', data)
|
|
break
|
|
case 'read':
|
|
this._emit('read', data)
|
|
break
|
|
case 'ack':
|
|
this._emit('ack', data)
|
|
break
|
|
default:
|
|
console.log('[WS] 未知消息类型', type)
|
|
}
|
|
}
|
|
|
|
/** 触发事件 */
|
|
_emit(event, data) {
|
|
const cbs = this.listeners[event]
|
|
if (cbs) {
|
|
cbs.forEach(cb => {
|
|
try { cb(data) } catch (e) { console.warn('[WS] 事件回调异常', e) }
|
|
})
|
|
}
|
|
}
|
|
|
|
/** 启动心跳 */
|
|
_startHeartbeat() {
|
|
this._stopHeartbeat()
|
|
this.heartbeatTimer = setInterval(() => {
|
|
this.send('ping')
|
|
// 设置超时检测
|
|
this.heartbeatTimeout = setTimeout(() => {
|
|
console.warn('[WS] 心跳超时,触发重连')
|
|
if (this.socketTask) {
|
|
this.socketTask.close()
|
|
}
|
|
}, this.heartbeatWait)
|
|
}, this.heartbeatInterval)
|
|
}
|
|
|
|
/** 收到 pong,清除超时 */
|
|
_onPong() {
|
|
if (this.heartbeatTimeout) {
|
|
clearTimeout(this.heartbeatTimeout)
|
|
this.heartbeatTimeout = null
|
|
}
|
|
}
|
|
|
|
/** 停止心跳 */
|
|
_stopHeartbeat() {
|
|
if (this.heartbeatTimer) {
|
|
clearInterval(this.heartbeatTimer)
|
|
this.heartbeatTimer = null
|
|
}
|
|
if (this.heartbeatTimeout) {
|
|
clearTimeout(this.heartbeatTimeout)
|
|
this.heartbeatTimeout = null
|
|
}
|
|
}
|
|
|
|
/** 指数退避重连 */
|
|
_scheduleReconnect() {
|
|
if (this.manualClose || this.reconnectTimer) return
|
|
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), this.maxReconnectDelay)
|
|
this.reconnectAttempts++
|
|
console.log(`[WS] ${delay}ms 后尝试第 ${this.reconnectAttempts} 次重连`)
|
|
this.reconnectTimer = setTimeout(() => {
|
|
this.reconnectTimer = null
|
|
this.isConnecting = false
|
|
this.connect(this.token)
|
|
}, delay)
|
|
}
|
|
|
|
/** 清除重连计时器 */
|
|
_clearReconnect() {
|
|
if (this.reconnectTimer) {
|
|
clearTimeout(this.reconnectTimer)
|
|
this.reconnectTimer = null
|
|
}
|
|
this.reconnectAttempts = 0
|
|
}
|
|
|
|
/** 重连成功后重发队列消息 */
|
|
_flushQueue() {
|
|
if (!this.messageQueue.length) return
|
|
console.log(`[WS] 重发 ${this.messageQueue.length} 条暂存消息`)
|
|
const queue = [...this.messageQueue]
|
|
this.messageQueue = []
|
|
queue.forEach(payload => {
|
|
if (this.socketTask && this.isConnected) {
|
|
this.socketTask.send({ data: payload })
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// 导出全局单例
|
|
const wsManager = new WebSocketManager()
|
|
export default wsManager
|