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

470 lines
18 KiB
JavaScript
Raw Normal View History

// 聊天室 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 = `<span style="color: #c00; font-weight: bold;">【系统】${d.message}</span><span class="msg-time">(${timeStr})</span>`;
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 = '<span style="color: #16a34a; font-weight: bold;">【系统】您的禁言已解除,可以继续发言了。</span>';
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 = `<span style="color: #dc2626; font-weight: bold;">🧹 管理员 <b>${safeOperator}</b> 已执行全员清屏</span><span class="msg-time">(${timeStr})</span>`;
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 = `<span style="color: #b45309; font-weight: bold;">
📋 【版本更新】v${safeVersion} · ${safeTitle}
<a href="${safeUrl}" target="_blank" rel="noopener"
style="color: #7c3aed; text-decoration: underline; margin-left: 8px; font-size: 0.85em;">
查看详情 →
</a>
</span><span class="msg-time">(${timeStr})</span>`;
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
? `<button type="button" data-gomoku-open-panel class="gomoku-invite-open"
style="margin-left:10px; padding:3px 12px; border:1.5px solid #2d6096;
border-radius:12px; background:#f0f6ff; color:#2d6096; font-size:12px;
cursor:pointer; font-family:inherit; transition:all .15s;">
⤴️ 打开面板
</button>`
: `<button type="button" data-gomoku-accept-id="${gomokuGameId}" id="gomoku-accept-${gomokuGameId}" class="gomoku-invite-accept"
style="margin-left:10px; padding:3px 12px; border:1.5px solid #336699;
border-radius:12px; background:#336699; color:#fff; font-size:12px;
cursor:pointer; font-family:inherit; transition:opacity .15s;">
⚔️ 接受挑战
</button>`;
div.innerHTML = `<span style="color:#1e3a5f; font-weight:bold;">
♟️ 【五子棋】<b>${safeInviterName}</b> 发起了随机对战!${isSelf ? "(等待中)" : ""}
</span>${acceptBtn}
<span class="msg-time">(${timeStr})</span>`;
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 = `♟️ 五子棋对局以<b>平局</b>结束!`;
} else {
text = `♟️ <b>${e.winner_name}</b> 击败 <b>${e.loser_name}</b>${reason})获得 <b style="color:#b45309;">${e.reward_gold}</b> 金币!`;
}
div.innerHTML = `<span style="color:#92400e;">${text}</span><span class="msg-time">(${timeStr})</span>`;
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: `<b>${operatorName}</b> 通知全员刷新页面。<br><span style="color:#475569;">${reasonText}</span>`,
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: `<b>${operatorName}</b> 已更新你的职务状态。<br><span style="color:#475569;">${reasonText}</span>`,
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 };