Files
chatroom/resources/js/chat-room/heartbeat.js
T
pllx f17f171f4b fix: 修复迁移遗留的按钮无响应、头像框层级及构建错误
迁移收尾修复:
- heartbeat.js: 移除 export { } 中重复的 startHeartbeat/stopHeartbeat(已通过 export function 导出)
- scripts.blade.php: 移除 JS 注释中的 {{ }} 避免 Blade 编译为 e() 导致 PHP 解析错误
- preferences-status.js: 补全 6 个缺失的 window.* 赋值(toggleBlockMenu/toggleFeatureMenu 等),
  实现迁移中丢失的 updateDailyStatus/clearDailyStatus,修复 handleFeatureLocalClear 清屏回调
- toolbar.js: 补全 window.runFeatureShortcut 赋值

头像框样式修复(chat-decorations.css):
- z-index 互换:头像降至 1,框升至 3,使框边缘可遮挡头像外围
- 使用 CSS mask(radial-gradient)挖环形替代旧 ::before 实心圆遮挡方案
- clip-path: circle(50%) 硬裁剪确保圆形,不受 chat.css border-radius: 2px 覆盖
- 特异性提升至 .user-item .avatar-frame-wrapper .user-head

新 Vite 模块(从 Blade 迁移):
- chat-state.js / message-renderer.js / user-list.js / chat-events.js
- composer.js(重写)/ heartbeat.js / admin-commands.js
- vip-presence.js / chat-decorations.css
2026-04-27 09:19:49 +00:00

239 lines
7.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 聊天室心跳与存点模块:定时存点、手动存点、退出房间、掉线检测。
* 从 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 };