// 聊天室 WebSocket 事件监听,从 Blade 内联脚本 scripts.blade.php 迁移至 Vite 模块。 // 所有事件委托通过 window.addEventListener 注册,依赖 window.chatState 共享状态。 import { escapeHtml, normalizeSafeChatUrl } from "./html.js"; import { normalizeDailyStatus } from "./preferences-status.js"; import { enqueueChatMessage } from "./message-renderer.js"; // ── 事件注册标记 ── let chatEventsBound = false; // ── 辅助函数 ── function csrf() { return document.querySelector('meta[name="csrf-token"]')?.content ?? ""; } function getState() { return window.chatState; } /** * 启动 WebSocket 初始化(DOMContentLoaded 之后调用)。 */ function initChatWebSocket() { if (typeof window.initChat === "function" && window.chatContext?.roomId) { window.initChat(window.chatContext.roomId); } } // ── 禁言逻辑 ── function handleMutedEvent(e) { const state = getState(); const d = e.detail; 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 isMe = d.username === window.chatContext?.username; const div = document.createElement("div"); div.className = "msg-line"; div.innerHTML = `【系统】${d.message}(${timeStr})`; const targetContainer = isMe ? document.getElementById("say2") : (state?.container); if (targetContainer) { targetContainer.appendChild(div); targetContainer.scrollTop = targetContainer.scrollHeight; } if (isMe && d.mute_time > 0) { state.isMutedUntil = Date.now() + d.mute_time * 60 * 1000; const contentInput = document.getElementById("content"); const operatorName = d.operator || "管理员"; if (contentInput) { contentInput.placeholder = `${operatorName} 已将您禁言 ${d.mute_time} 分钟,解禁后方可发言...`; contentInput.disabled = true; setTimeout(() => { state.isMutedUntil = 0; contentInput.placeholder = "在这里输入聊天内容,按 Enter 发送..."; contentInput.disabled = false; const unmuteDiv = document.createElement("div"); unmuteDiv.className = "msg-line"; unmuteDiv.innerHTML = '【系统】您的禁言已解除,可以继续发言了。'; const say2 = document.getElementById("say2"); if (say2) { say2.appendChild(unmuteDiv); say2.scrollTop = say2.scrollHeight; } }, d.mute_time * 60 * 1000); } } } // ── Echo 级监听器 ── /** * 注册全员清屏监听(ScreenCleared)。 */ function setupScreenClearedListener() { if (!window.Echo || !window.chatContext) { setTimeout(setupScreenClearedListener, 500); return; } window.Echo.join(`room.${window.chatContext.roomId}`) .listen("ScreenCleared", (e) => { const operator = e.operator; const safeOperator = escapeHtml(String(operator || "")); const say1 = document.getElementById("chat-messages-container"); if (say1) say1.innerHTML = ""; const say2 = document.getElementById("chat-messages-container2"); if (say2) { const items = say2.querySelectorAll(".msg-line"); items.forEach((item) => { if (!item.querySelector(".msg-secret")) { item.remove(); } }); } const state = getState(); if (state) { state.lastAutosaveNode = say2?.querySelector('[data-autosave="1"]') || null; } const sysDiv = document.createElement("div"); sysDiv.className = "msg-line"; const now = new Date(); const timeStr = now.getHours().toString().padStart(2, "0") + ":" + now.getMinutes().toString().padStart(2, "0") + ":" + now.getSeconds().toString().padStart(2, "0"); sysDiv.innerHTML = `🧹 管理员 ${safeOperator} 已执行全员清屏(${timeStr})`; if (say1) { say1.appendChild(sysDiv); say1.scrollTop = say1.scrollHeight; } }); } /** * 注册房间级"刷新全员"监听(BrowserRefreshRequested)。 */ function setupRoomBrowserRefreshListener() { if (!window.Echo || !window.chatContext) { setTimeout(setupRoomBrowserRefreshListener, 500); return; } window.Echo.join(`room.${window.chatContext.roomId}`) .listen("BrowserRefreshRequested", (e) => { window.dispatchEvent( new CustomEvent("chat:browser-refresh-requested", { detail: e }) ); }); } /** * 注册开发日志发布通知监听(仅 Room 1)。 */ function setupChangelogPublishedListener() { if (!window.Echo || !window.chatContext) { setTimeout(setupChangelogPublishedListener, 500); return; } if (window.chatContext.roomId !== 1) return; window.Echo.join("room.1") .listen(".ChangelogPublished", (e) => { 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 safeVersion = e.safe_version ?? escapeHtml(String(e.version ?? "")); const safeTitle = e.safe_title ?? escapeHtml(String(e.title ?? "")); const changelogRoute = window.chatContext?.changelogUrl || "/changelog"; const safeUrl = escapeHtml(normalizeSafeChatUrl(e.url, changelogRoute)); const sysDiv = document.createElement("div"); sysDiv.className = "msg-line"; sysDiv.style.cssText = "background: #fffbeb; border-left: 3px solid #d97706; border-radius: 4px; padding: 5px 10px; margin: 3px 0;"; sysDiv.innerHTML = ` 📋 【版本更新】v${safeVersion} · ${safeTitle} 查看详情 → (${timeStr})`; const say1 = document.getElementById("chat-messages-container"); if (say1) { say1.appendChild(sysDiv); say1.scrollTop = say1.scrollHeight; } }); } /** * 注册五子棋 PvP 邀请通知监听。 */ function setupGomokuInviteListener() { if (!window.Echo || !window.chatContext) { setTimeout(setupGomokuInviteListener, 500); return; } window.Echo.join(`room.${window.chatContext.roomId}`) .listen(".gomoku.invite", (e) => { 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 isSelf = (e.inviter_name === window.chatContext.username); const div = document.createElement("div"); div.className = "msg-line"; div.style.cssText = "background:linear-gradient(135deg,#e8eef8,#f0f4fc); border-left:3px solid #336699; border-radius:4px; padding:6px 10px; margin:3px 0;"; const safeInviterName = escapeHtml(e.inviter_name); const gomokuGameId = Number.parseInt(e.game_id, 10) || 0; const acceptBtn = isSelf ? `` : ``; div.innerHTML = ` ♟️ 【五子棋】${safeInviterName} 发起了随机对战!${isSelf ? "(等待中)" : ""} ${acceptBtn} (${timeStr})`; const say1 = document.getElementById("chat-messages-container"); if (say1) { say1.appendChild(div); say1.scrollTop = say1.scrollHeight; } if (!isSelf) { setTimeout(() => { const btn = document.getElementById(`gomoku-accept-${e.game_id}`); if (btn) { btn.textContent = "已超时"; btn.disabled = true; btn.style.opacity = ".5"; btn.style.cursor = "not-allowed"; } }, 60000); } }) .listen(".gomoku.finished", (e) => { if (e.mode !== "pvp") return; 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 div = document.createElement("div"); div.className = "msg-line"; div.style.cssText = "background:#fffae8; border-left:3px solid #d97706; border-radius:4px; padding:5px 10px; margin:2px 0;"; const reason = { win: "获胜", draw: "平局", resign: "认输", timeout: "超时" }[e.reason] || "结束"; let text = ""; if (e.winner === 0) { text = `♟️ 五子棋对局以平局结束!`; } else { text = `♟️ ${e.winner_name} 击败 ${e.loser_name}(${reason})获得 ${e.reward_gold} 金币!`; } div.innerHTML = `${text}(${timeStr})`; const say1 = document.getElementById("chat-messages-container"); if (say1) { say1.appendChild(div); say1.scrollTop = say1.scrollHeight; } }); } // ── 主事件绑定 ── /** * 绑定所有聊天室 WebSocket 事件监听,仅执行一次。 */ export function bindChatEvents() { if (chatEventsBound || typeof document === "undefined") { return; } chatEventsBound = true; // WebSocket 初始化 document.addEventListener("DOMContentLoaded", initChatWebSocket); // chat:here — Presence 初始用户列表 window.addEventListener("chat:here", (e) => { const state = getState(); if (!state) return; const users = e.detail; state.onlineUsers = {}; users.forEach((u) => { window.hydrateOnlineUserPayload(u.username, u); }); // 注入 AI 小班长 if (window.chatContext?.chatBotEnabled && window.chatContext.botUser) { window.hydrateOnlineUserPayload("AI小班长", window.chatContext.botUser); } // 同步当前用户状态 if (typeof window.setOnlineUserDailyStatus === "function" && typeof window.getCurrentUserDailyStatus === "function") { window.setOnlineUserDailyStatus(window.chatContext?.username, window.getCurrentUserDailyStatus()); } if (typeof window.syncDailyStatusUi === "function") { window.syncDailyStatusUi(); } window.scheduleRenderUserList(0); }); // chat:bot-toggled — AI 小班长动态开关 window.addEventListener("chat:bot-toggled", (e) => { const detail = e.detail; if (window.chatContext) { window.chatContext.chatBotEnabled = detail.isOnline; } if (detail.isOnline && detail.user && detail.user.username) { window.hydrateOnlineUserPayload(detail.user.username, detail.user); if (window.chatContext) window.chatContext.botUser = detail.user; } else { const state = getState(); if (state) delete state.onlineUsers["AI小班长"]; if (window.chatContext) window.chatContext.botUser = null; } window.scheduleRenderUserList?.(); }); // chat:user-status-updated — 用户每日状态更新 window.addEventListener("chat:user-status-updated", (e) => { const username = e.detail?.username; const payload = e.detail?.user; if (!username || !payload) return; window.hydrateOnlineUserPayload(username, payload); if (username === window.chatContext?.username) { if (window.chatContext) { window.chatContext.currentDailyStatus = normalizeDailyStatus(payload); } if (typeof window.syncDailyStatusUi === "function") { window.syncDailyStatusUi(); } } window.scheduleRenderUserList?.(); }); // chat:joining — 用户进入 window.addEventListener("chat:joining", (e) => { const user = e.detail; window.hydrateOnlineUserPayload(user.username, user); window.scheduleRenderUserList?.(); }); // chat:leaving — 用户离开 window.addEventListener("chat:leaving", (e) => { const user = e.detail; const state = getState(); if (state) delete state.onlineUsers[user.username]; window.scheduleRenderUserList?.(); }); // chat:message — 新消息 window.addEventListener("chat:message", (e) => { const msg = e.detail; if (msg.is_secret && msg.from_user !== window.chatContext?.username && msg.to_user !== window.chatContext?.username) { return; } enqueueChatMessage(msg); if (msg.action === "vip_presence" && typeof window.showVipPresenceBanner === "function") { window.showVipPresenceBanner(msg); } // 若消息携带 toast_notification 字段且当前用户是接收者,弹右下角小卡片 if (msg.toast_notification && msg.to_user === window.chatContext?.username) { const t = msg.toast_notification; window.chatToast?.show({ title: t.title || "通知", message: t.message || "", icon: t.icon || "💬", color: t.color || "#336699", duration: t.duration ?? 8000, }); } }); // chat:kicked — 被踢出房间 window.addEventListener("chat:kicked", (e) => { if (e.detail.username === window.chatContext?.username) { const roomsIndexUrl = window.chatContext?.roomsIndexUrl || "/rooms"; window.chatDialog?.alert( "您已被管理员踢出房间!" + (e.detail.reason ? " 原因:" + e.detail.reason : ""), "系统通知", "#cc4444" ); window.location.href = roomsIndexUrl; } }); // chat:muted — 禁言事件 window.addEventListener("chat:muted", handleMutedEvent); // chat:title-updated — 房间标题更新 window.addEventListener("chat:title-updated", (e) => { const display = document.getElementById("room-title-display"); if (display) display.innerText = e.detail.title; }); // chat:browser-refresh-requested — 全员刷新通知 window.addEventListener("chat:browser-refresh-requested", (e) => { const detail = e.detail || {}; const operatorName = escapeHtml(String(detail.operator || "站长")); const reasonText = escapeHtml(String(detail.reason || "页面功能已更新,请重新载入。")); window.chatToast?.show({ title: "页面即将刷新", message: `${operatorName} 通知全员刷新页面。
${reasonText}`, icon: "♻️", color: "#0f766e", duration: 2200, }); window.setTimeout(() => { window.location.reload(); }, 900); }); // chat:user-browser-refresh-requested — 目标用户定向刷新 window.addEventListener("chat:user-browser-refresh-requested", (e) => { const detail = e.detail || {}; const operatorName = escapeHtml(String(detail.operator || "管理员")); const reasonText = escapeHtml(String(detail.reason || "你的权限状态已发生变化,页面即将刷新。")); window.chatToast?.show({ title: "权限同步中", message: `${operatorName} 已更新你的职务状态。
${reasonText}`, icon: "🔄", color: "#7c3aed", duration: 2600, }); window.setTimeout(() => { window.location.reload(); }, 1000); }); // chat:effect — 全屏特效事件 window.addEventListener("chat:effect", (e) => { const type = e.detail?.type; const target = e.detail?.target_username; const operator = e.detail?.operator; const myName = window.chatContext?.username; if (type && typeof EffectManager !== "undefined") { if (!target || target === myName || operator === myName) { EffectManager.play(type); } } }); // Echo 级监听器(延迟绑定,等待 Echo 就绪) document.addEventListener("DOMContentLoaded", () => { setupScreenClearedListener(); setupRoomBrowserRefreshListener(); setupChangelogPublishedListener(); setupGomokuInviteListener(); }); } export { initChatWebSocket };