f17f171f4b
迁移收尾修复:
- 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
239 lines
7.9 KiB
JavaScript
239 lines
7.9 KiB
JavaScript
/**
|
||
* 聊天室心跳与存点模块:定时存点、手动存点、退出房间、掉线检测。
|
||
* 从 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 };
|