/** * 聊天室心跳与存点模块:定时存点、手动存点、退出房间、掉线检测。 * 从 Blade 内联脚本 scripts.blade.php 迁移至 Vite 模块。 */ import { escapeHtml } from "./html.js"; import { pruneMessageContainer } from "./message-renderer.js"; const MAX_HEARTBEAT_FAILS = 3; const HEARTBEAT_INTERVAL = 60 * 1000; let heartbeatInterval = null; let heartbeatFailCount = 0; let leaveRequestInFlight = false; /** * 获取 CSRF Token。 * * @returns {string} */ function csrf() { return document.querySelector('meta[name="csrf-token"]')?.content ?? ""; } /** * 获取共享状态对象。 * * @returns {Object|null} */ function getState() { return window.chatState; } // ── 存点功能(手动 + 自动)───────────────────── /** * 执行一次存点请求,向服务端同步在线状态并获取经验/金币。 * * @param {boolean} silent 静默模式(true=仅心跳,不显示存点提示) * @returns {Promise} */ async function saveExp(silent = false) { if (!window.chatContext?.heartbeatUrl) return; const state = getState(); try { const response = await fetch(window.chatContext.heartbeatUrl, { method: "POST", headers: { "X-CSRF-TOKEN": csrf(), "Accept": "application/json", }, }); // 检测登录态失效 if (response.status === 401 || response.status === 419) { await notifyExpiredLeave(); window.chatDialog?.alert( "⚠️ 您的登录已失效(可能超时或在其他设备登录),请重新登录。", "连接警告", "#b45309" ); window.location.href = "/"; return; } const data = await response.json(); if (response.ok && data.status === "success") { heartbeatFailCount = 0; const now = new Date(); const timeStr = now.getHours().toString().padStart(2, "0") + ":" + now.getMinutes().toString().padStart(2, "0") + ":" + now.getSeconds().toString().padStart(2, "0"); const d = data.data; const identitySummary = d.identity_summary ? `${d.identity_summary} · ` : ""; let levelInfo = ""; if (d.is_max_level) { levelInfo = `⏰ 手动存点 · ${identitySummary}LV.${d.user_level} · 经验 ${d.exp_num} · 金币 ${d.jjb} · 已满级 ✓`; } else { levelInfo = `⏰ 手动存点 · ${identitySummary}LV.${d.user_level} · 经验 ${d.exp_num} · 金币 ${d.jjb}`; } // 本次获得的奖励提示 let gainInfo = ""; if (d.exp_gain > 0 || d.jjb_gain > 0) { const parts = []; if (d.exp_gain > 0) parts.push(`经验+${d.exp_gain}`); if (d.jjb_gain > 0) parts.push(`金币+${d.jjb_gain}`); gainInfo = ` 本次获得:${parts.join(",")}`; } // 升级通知 if (data.data.leveled_up) { const upDiv = document.createElement("div"); upDiv.className = "msg-line"; upDiv.innerHTML = `【系统】恭喜!你的经验值已达 ${d.exp_num},等级突破至 LV.${d.user_level}!🌟(${timeStr})`; const container2 = state?.container2; if (container2) { container2.appendChild(upDiv); if (state?.autoScroll) container2.scrollTop = container2.scrollHeight; } } // 存点消息输出到包厢窗口 if (!silent) { const container2 = state?.container2 || document.getElementById("chat-messages-container2"); if (container2) { const detailDiv = document.createElement("div"); detailDiv.className = "msg-line"; detailDiv.dataset.autosave = "1"; detailDiv.innerHTML = `${escapeHtml(levelInfo + gainInfo)}(${timeStr})`; // 手动存点和自动存点共用同一条界面占位,避免包厢窗口堆积旧存点通知 if (state) { state.lastAutosaveNode?.remove(); state.lastAutosaveNode = detailDiv; } container2.appendChild(detailDiv); pruneMessageContainer(container2, window.chatState?.PRIVATE_MESSAGE_NODE_LIMIT || 300); if (state?.autoScroll) container2.scrollTop = container2.scrollHeight; } } } } catch (e) { console.error("存点失败", e); heartbeatFailCount++; if (heartbeatFailCount >= MAX_HEARTBEAT_FAILS) { window.chatDialog?.alert( "⚠️ 与服务器的连接已断开,请检查网络后重新登录。", "连接警告", "#b45309" ); window.location.href = "/"; return; } if (!silent) { const container2 = state?.container2 || document.getElementById("chat-messages-container2"); if (container2) { const sysDiv = document.createElement("div"); sysDiv.className = "msg-line"; sysDiv.innerHTML = '【系统】存点失败,请稍后重试'; container2.appendChild(sysDiv); } } } } // ── 退出房间 ── /** * 主动退出房间并关闭页面。 * * @returns {Promise} */ async function leaveRoom() { if (leaveRequestInFlight) return; leaveRequestInFlight = true; try { await fetch(window.chatContext.leaveUrl + "?explicit=1", { method: "POST", headers: { "X-CSRF-TOKEN": csrf(), "Accept": "application/json", }, }); } catch (e) { console.error(e); } // 弹出窗口直接关闭,如果不是弹出窗口则跳回首页 window.close(); setTimeout(() => { window.location.href = "/"; }, 500); } /** * 通知服务端登录已过期(用于 401/419 响应时)。 * * @returns {Promise} */ async function notifyExpiredLeave() { if (leaveRequestInFlight) return; leaveRequestInFlight = true; try { if (!window.chatContext?.expiredLeaveUrl) return; await fetch(window.chatContext.expiredLeaveUrl, { method: "GET", headers: { "Accept": "application/json" }, credentials: "same-origin", }); } catch (e) { console.error(e); } } // ── 定时器管理 ── /** * 启动心跳定时器(每 60 秒自动存点)。 */ export function startHeartbeat() { stopHeartbeat(); // 防止重复启动 // 首次心跳延迟 10 秒,让 WebSocket 先连接 const initialTimer = window.setTimeout(() => saveExp(true), 10000); const intervalTimer = window.setInterval(() => saveExp(true), HEARTBEAT_INTERVAL); heartbeatInterval = { initial: initialTimer, interval: intervalTimer }; } /** * 停止心跳定时器。 */ export function stopHeartbeat() { if (heartbeatInterval) { window.clearTimeout(heartbeatInterval.initial); window.clearInterval(heartbeatInterval.interval); heartbeatInterval = null; } } // ── 挂载到 window ── window.saveExp = saveExp; window.leaveRoom = leaveRoom; window.notifyExpiredLeave = notifyExpiredLeave; window.startHeartbeat = startHeartbeat; window.stopHeartbeat = stopHeartbeat; export { saveExp, leaveRoom, notifyExpiredLeave, HEARTBEAT_INTERVAL, MAX_HEARTBEAT_FAILS };