Files
chatroom/resources/js/chat-room/heartbeat.js
T

239 lines
7.9 KiB
JavaScript
Raw Normal View History

/**
* 聊天室心跳与存点模块:定时存点、手动存点、退出房间、掉线检测。
* 从 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<void>}
*/
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 = `<span style="color: #d97706; font-weight: bold;">【系统】恭喜!你的经验值已达 ${d.exp_num},等级突破至 LV.${d.user_level}!🌟</span><span class="msg-time">(${timeStr})</span>`;
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 = `<span style="color: green;">${escapeHtml(levelInfo + gainInfo)}</span><span class="msg-time">(${timeStr})</span>`;
// 手动存点和自动存点共用同一条界面占位,避免包厢窗口堆积旧存点通知
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 = '<span style="color: red;">【系统】存点失败,请稍后重试</span>';
container2.appendChild(sysDiv);
}
}
}
}
// ── 退出房间 ──
/**
* 主动退出房间并关闭页面。
*
* @returns {Promise<void>}
*/
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<void>}
*/
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 };