From fdd20917a4da20d16f16736434ebfe6da3e431c2 Mon Sep 17 00:00:00 2001 From: lkddi Date: Sat, 25 Apr 2026 14:12:48 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=81=E7=A7=BB=E5=A8=B1=E4=B9=90=E5=A4=A7?= =?UTF-8?q?=E5=8E=85=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/js/chat-room.js | 8 +- resources/js/chat-room/game-hall.js | 568 +++++++++++++++++- .../chat/partials/games/game-hall.blade.php | 480 +-------------- 3 files changed, 569 insertions(+), 487 deletions(-) diff --git a/resources/js/chat-room.js b/resources/js/chat-room.js index 2e1bc3d..45e958e 100644 --- a/resources/js/chat-room.js +++ b/resources/js/chat-room.js @@ -44,7 +44,7 @@ export { openBaccaratLossCoverModal, switchBaccaratLossCoverTab, } from "./chat-room/baccarat-loss-cover.js"; -export { bindGameHallControls } from "./chat-room/game-hall.js"; +export { bindGameHallControls, closeGameHall, openGameHall } from "./chat-room/game-hall.js"; export { bindGameBootstrapControls, deferChatGameBootstrap } from "./chat-room/game-bootstrap.js"; export { bindGamePanelControls } from "./chat-room/game-panels.js"; export { bindHolidayModalControls, openHolidayRunFromSystemMessage } from "./chat-room/holiday-modal.js"; @@ -134,7 +134,7 @@ import { openBaccaratLossCoverModal, switchBaccaratLossCoverTab, } from "./chat-room/baccarat-loss-cover.js"; -import { bindGameHallControls } from "./chat-room/game-hall.js"; +import { bindGameHallControls, closeGameHall, openGameHall } from "./chat-room/game-hall.js"; import { bindGameBootstrapControls, deferChatGameBootstrap } from "./chat-room/game-bootstrap.js"; import { bindGamePanelControls } from "./chat-room/game-panels.js"; import { bindHolidayModalControls, openHolidayRunFromSystemMessage } from "./chat-room/holiday-modal.js"; @@ -234,6 +234,8 @@ if (typeof window !== "undefined") { openBaccaratLossCoverModal, switchBaccaratLossCoverTab, bindGameHallControls, + closeGameHall, + openGameHall, bindGameBootstrapControls, deferChatGameBootstrap, bindGamePanelControls, @@ -324,8 +326,10 @@ if (typeof window !== "undefined") { window.bankLoadInfo = bankLoadInfo; window.bankShowMsg = bankShowMsg; window.closeBankModal = closeBankModal; + window.closeGameHall = closeGameHall; window.fetchBankRanking = fetchBankRanking; window.deferChatGameBootstrap = deferChatGameBootstrap; + window.openGameHall = openGameHall; window.openBankModal = openBankModal; window.switchBankTab = switchBankTab; window.toggleBankRankSort = toggleBankRankSort; diff --git a/resources/js/chat-room/game-hall.js b/resources/js/chat-room/game-hall.js index 29b8e30..7189e3a 100644 --- a/resources/js/chat-room/game-hall.js +++ b/resources/js/chat-room/game-hall.js @@ -1,16 +1,555 @@ -// 娱乐大厅弹窗事件代理,替代静态关闭按钮内联 onclick。 +// 聊天室娱乐大厅弹窗逻辑,集中加载游戏状态并渲染入口卡片。 + +import { escapeHtml } from "./html.js"; + +const GAME_HALL_CACHE_TTL = 15000; let gameHallEventsBound = false; +let gameHallStatusCache = null; +let gameHallStatusCacheAt = 0; /** - * 关闭娱乐大厅弹窗。 + * 获取游戏大厅弹窗元素。 + * + * @returns {HTMLElement|null} + */ +function getGameHallModal() { + return document.getElementById("game-hall-modal"); +} + +/** + * 读取服务端注入的游戏开关快照。 + * + * @returns {Record} + */ +function readGameEnabledSnapshot() { + const modal = getGameHallModal(); + if (!modal?.dataset.gameEnabled) { + return {}; + } + + try { + return JSON.parse(modal.dataset.gameEnabled); + } catch (error) { + console.warn("[游戏大厅] 游戏开关数据解析失败:", error); + return {}; + } +} + +/** + * 打开 Alpine 管理的游戏面板。 + * + * @param {string} panelId + * @param {string} [method] + * @returns {void} + */ +function openAlpinePanel(panelId, method = "show") { + const panel = document.getElementById(panelId); + if (!panel || !window.Alpine) { + return; + } + + const data = window.Alpine.$data(panel); + if (typeof data?.[method] === "function") { + data[method](); + return; + } + + data[method] = true; +} + +/** + * 游戏大厅配置定义。 + * + * @returns {Array} + */ +function createGameDefinitions() { + return [ + { + id: "baccarat", + name: "🎲 百家乐", + desc: "猜骰子大小,1:1 赔率,豹子 1:24", + accentColor: "#336699", + fetchUrl: "/baccarat/current", + openFn: () => { + closeGameHall(); + openAlpinePanel("baccarat-panel"); + }, + renderStatus: (data) => { + if (!data?.round) { + return { + badge: "⏸ 等待开局", + badgeStyle: "background:#e8f0f8; color:#336699; border:1px solid #b8d0e8", + detail: "下局即将开始,稍后再来", + }; + } + + const round = data.round; + if (round.status === "betting") { + return { + badge: "🟢 押注中", + badgeStyle: "background:#d1fae5; color:#065f46; border:1px solid #6ee7b7", + detail: `⏱ 剩余 ${round.seconds_left || 0} 秒 | 注池:${Number((round.total_bet_big || 0) + (round.total_bet_small || 0) + (round.total_bet_triple || 0)).toLocaleString()} 金`, + }; + } + + return { + badge: "⏳ 开奖中", + badgeStyle: "background:#fef3c7; color:#92400e; border:1px solid #fcd34d", + detail: "正在摇骰子…", + }; + }, + btnLabel: (data) => data?.round?.status === "betting" ? "🎲 立即下注" : "📊 查看详情", + }, + { + id: "baccarat_loss_cover", + name: "🎁 买单活动", + desc: "查看“你玩游戏我买单”活动,补偿领取和个人历史记录", + accentColor: "#16a34a", + fetchUrl: "/baccarat-loss-cover/summary", + openFn: () => { + closeGameHall(); + void window.openBaccaratLossCoverModal?.("overview"); + }, + renderStatus: (data) => { + const event = data?.event; + if (!event) { + return { + badge: "📭 暂无活动", + badgeStyle: "background:#e8f0f8; color:#336699; border:1px solid #b8d0e8", + detail: "当前没有进行中或待领取的买单活动", + }; + } + + const myStatus = event.my_record?.claim_status_label || "未参与"; + const total = Number(event.total_claimed_amount || 0).toLocaleString(); + + if (event.status === "active") { + return { + badge: "🟢 进行中", + badgeStyle: "background:#d1fae5; color:#065f46; border:1px solid #6ee7b7", + detail: `我的状态:${myStatus} | 开启人:${event.creator_username || ""}`, + }; + } + + if (event.status === "settlement_pending") { + return { + badge: "⏳ 结算中", + badgeStyle: "background:#fef3c7; color:#92400e; border:1px solid #fcd34d", + detail: `活动已结束,等待最后几局结算 | 我的状态:${myStatus}`, + }; + } + + if (event.status === "claimable") { + return { + badge: "💰 可领取", + badgeStyle: "background:#dcfce7; color:#166534; border:1px solid #86efac", + detail: `我的状态:${myStatus} | 已发补偿 ${total} 金币`, + }; + } + + return { + badge: "🕒 即将开始", + badgeStyle: "background:#ede9fe; color:#5b21b6; border:1px solid #c4b5fd", + detail: `开启人:${event.creator_username || ""} | 我的状态:${myStatus}`, + }; + }, + btnLabel: (data) => data?.event?.status === "claimable" ? "💰 查看并领取" : "📜 查看活动", + }, + { + id: "slot_machine", + name: "🎰 老虎机", + desc: "每日限额旋转,中奖即时到账", + accentColor: "#0891b2", + fetchUrl: null, + openFn: () => { + closeGameHall(); + openAlpinePanel("slot-panel"); + }, + renderStatus: () => ({ + badge: "✅ 随时可玩", + badgeStyle: "background:#d1fae5; color:#065f46; border:1px solid #6ee7b7", + detail: "每日限额抽奖,旋转即可", + }), + btnLabel: () => "🎰 开始旋转", + }, + { + id: "mystery_box", + name: "📦 神秘箱子", + desc: "管理员随机投放,抢到即开奖", + accentColor: "#b45309", + fetchUrl: "/mystery-box/status", + openFn: () => { + closeGameHall(); + window.dispatchEvent(new CustomEvent("open-mystery-box")); + }, + renderStatus: (data) => { + const count = data?.available_count ?? 0; + + return count > 0 ? { + badge: `🎁 ${count} 个待领`, + badgeStyle: "background:#fef3c7; color:#92400e; border:1px solid #fcd34d", + detail: "箱子已投放!快去领取", + } : { + badge: "📭 暂无箱子", + badgeStyle: "background:#e8f0f8; color:#336699; border:1px solid #b8d0e8", + detail: "等待管理员投放", + }; + }, + btnLabel: (data) => (data?.available_count ?? 0) > 0 ? "🎁 立即领取" : "📭 等待投放", + }, + { + id: "horse_racing", + name: "🐎 赛马竞猜", + desc: "彩池制赛马,押注马匹赢取奖金", + accentColor: "#336699", + fetchUrl: "/horse-race/current", + openFn: () => { + closeGameHall(); + openAlpinePanel("horse-race-panel", "openFromHall"); + }, + renderStatus: (data) => { + if (!data?.race) { + return { + badge: "⏸ 等待开赛", + badgeStyle: "background:#e8f0f8; color:#336699; border:1px solid #b8d0e8", + detail: "下场赛马即将开始", + }; + } + + const race = data.race; + if (race.status === "betting") { + return { + badge: "🟢 押注中", + badgeStyle: "background:#d1fae5; color:#065f46; border:1px solid #6ee7b7", + detail: `⏱ 剩余 ${race.seconds_left || 0} 秒 | 注池:${Number(race.total_pool || 0).toLocaleString()} 金`, + }; + } + + if (race.status === "running") { + return { + badge: "🏇 跑马中", + badgeStyle: "background:#fef3c7; color:#92400e; border:1px solid #fcd34d", + detail: "比赛进行中…", + }; + } + + return { + badge: "🏆 已结算", + badgeStyle: "background:#e8f0f8; color:#336699; border:1px solid #b8d0e8", + detail: "下场即将开始", + }; + }, + btnLabel: (data) => data?.race?.status === "betting" ? "🐎 立即押注" : "📊 查看赛况", + }, + { + id: "fortune_telling", + name: "🔮 神秘占卜", + desc: "每日签文,开启今日运势加成", + accentColor: "#6d28d9", + fetchUrl: "/fortune/today", + openFn: () => { + closeGameHall(); + openAlpinePanel("fortune-panel"); + }, + renderStatus: (data) => { + if (!data?.enabled) { + return { + badge: "🔒 未开启", + badgeStyle: "background:#e8f0f8; color:#336699; border:1px solid #b8d0e8", + detail: "此游戏暂未开启", + }; + } + + const used = data.free_used ?? 0; + const total = data.free_count ?? 1; + + return data.has_free_left ? { + badge: "✨ 免费可占", + badgeStyle: "background:#ede9fe; color:#5b21b6; border:1px solid #c4b5fd", + detail: `今日已占 ${used}/${total} 次,还有免费次数`, + } : { + badge: "💰 付费可占", + badgeStyle: "background:#e8f0f8; color:#336699; border:1px solid #b8d0e8", + detail: `今日免费次数已用完(${data.extra_cost} 金/次)`, + }; + }, + btnLabel: (data) => data?.has_free_left ? "🔮 免费占卜" : "🔮 付费占卜", + }, + { + id: "fishing", + name: "🎣 钓鱼", + desc: "消耗鱼饵钓取金币和道具。背包需有鱼饵才能出竿。", + accentColor: "#0d9488", + fetchUrl: null, + openFn: () => { + closeGameHall(); + window.startFishing?.(); + }, + renderStatus: () => ({ + badge: "🎣 随时可钓", + badgeStyle: "background:#d1fae5; color:#065f46; border:1px solid #6ee7b7", + detail: "① 点击发言框上方【🎣 钓鱼】按钮 → ② 等待浮漂出现 → ③ 看到 🪝 后立刻点击收竿!", + }), + btnLabel: () => "🎣 去钓鱼", + }, + { + id: "lottery", + name: "🎟️ 双色球", + desc: "每日20:00开奖,选3红1蓝,按奖池比例派奖,无一等奖滚存累积", + accentColor: "#dc2626", + fetchUrl: "/lottery/current", + openFn: () => { + closeGameHall(); + window.openLotteryPanel?.(); + }, + renderStatus: (data) => { + if (!data?.issue) { + return { + badge: "⏸ 等待开期", + badgeStyle: "background:#e8f0f8; color:#336699; border:1px solid #b8d0e8", + detail: "暂无进行中期次", + }; + } + + const issue = data.issue; + const pool = Number(issue.pool_amount || 0).toLocaleString(); + if (data.is_open) { + const hours = Math.floor(issue.seconds_left / 3600); + const minutes = Math.floor((issue.seconds_left % 3600) / 60); + const timeText = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`; + + return { + badge: issue.is_super_issue ? "🎊 超级期购票中" : "🟢 购票中", + badgeStyle: "background:#fef2f2; color:#b91c1c; border:1px solid #fca5a5", + detail: `💰 奖池 ${pool} 金 | 距开奖 ${timeText} | 第 ${issue.issue_no} 期`, + }; + } + + if (issue.status === "settled") { + return { + badge: "✅ 已开奖", + badgeStyle: "background:#d1fae5; color:#065f46; border:1px solid #6ee7b7", + detail: "本期已开奖,下期购票中", + }; + } + + return { + badge: "🔴 已停售", + badgeStyle: "background:#fee2e2; color:#b91c1c; border:1px solid #fecaca", + detail: `💰 奖池 ${pool} 金 | 等待开奖中…`, + }; + }, + btnLabel: (data) => data?.is_open ? "🎟️ 立即购票" : "📊 查看结果", + }, + { + id: "gomoku", + name: "♟️ 五子棋", + desc: "益智对弈,支持 PvP 随机对战和 AI 人机对战(4档难度)", + accentColor: "#1e3a5f", + fetchUrl: null, + openFn: () => { + closeGameHall(); + window.openGomokuPanel?.(); + }, + renderStatus: () => ({ + badge: "♟️ 随时对弈", + badgeStyle: "background:#e8eef8; color:#1e3a5f; border:1px solid #9db3d4", + detail: "PvP 胜利 +80 金币 | AI 专家难度胜利 +300 金币", + }), + btnLabel: () => "♟️ 开始对弈", + }, + ]; +} + +/** + * 加载游戏大厅状态,短时间内复用结果以减少重复请求。 + * + * @returns {Promise<{enabledGames:Array, statuses:Record}>} + */ +async function loadGameHallStatus() { + const now = Date.now(); + if (gameHallStatusCache && now - gameHallStatusCacheAt < GAME_HALL_CACHE_TTL) { + return gameHallStatusCache; + } + + const modal = getGameHallModal(); + let enabledMap = readGameEnabledSnapshot(); + + try { + const response = await fetch(modal?.dataset.gameEnabledUrl || "/games/enabled", { + headers: { + Accept: "application/json", + }, + }); + if (response.ok) { + enabledMap = await response.json(); + } + } catch (error) { + // 网络异常时降级使用页面注入的开关快照。 + } + + const enabledGames = createGameDefinitions().filter((game) => enabledMap[game.id] !== false); + const statuses = {}; + + await Promise.all(enabledGames.filter((game) => game.fetchUrl).map(async (game) => { + try { + const response = await fetch(game.fetchUrl); + statuses[game.id] = await response.json(); + } catch (error) { + statuses[game.id] = null; + } + })); + + gameHallStatusCache = { enabledGames, statuses }; + gameHallStatusCacheAt = Date.now(); + + return gameHallStatusCache; +} + +/** + * 关闭游戏大厅弹窗。 * * @returns {void} */ -function closeGameHallThroughGlobal() { - // 游戏大厅加载、缓存和卡片行为仍在 Blade 旧脚本内,模块阶段只统一关闭入口。 - if (typeof window.closeGameHall === "function") { - window.closeGameHall(); +export function closeGameHall() { + const modal = getGameHallModal(); + if (modal) { + modal.style.display = "none"; + } +} + +/** + * 渲染所有游戏卡片。 + * + * @param {Array} games + * @param {Record} statuses + * @returns {void} + */ +function renderGameCards(games, statuses) { + const container = document.getElementById("game-hall-cards"); + const loading = document.getElementById("game-hall-loading"); + const empty = document.getElementById("game-hall-empty"); + if (!container || !loading || !empty) { + return; + } + + container.innerHTML = ""; + + if (games.length === 0) { + loading.style.display = "none"; + empty.style.display = "block"; + return; + } + + games.forEach((game) => { + try { + const data = statuses[game.id] ?? null; + const status = game.renderStatus ? game.renderStatus(data) : { + badge: "✅ 可用", + badgeStyle: "background:#d1fae5; color:#065f46; border:1px solid #6ee7b7", + detail: "", + }; + const buttonLabel = game.btnLabel ? game.btnLabel(data) : "🎮 进入"; + + const card = document.createElement("div"); + card.style.cssText = ` + background:#fff; + border:1px solid #d0e4f5; + border-left:4px solid ${game.accentColor}; + border-radius:6px; padding:12px 14px; + cursor:default; transition:border-color .2s, box-shadow .2s; + display:flex; flex-direction:column; gap:8px; + `; + + card.innerHTML = ` +
+
+
${escapeHtml(game.name)}
+
${escapeHtml(game.desc)}
+
+ + ${escapeHtml(status.badge)} + +
+
${status.detail ? escapeHtml(status.detail) : " "}
+ + `; + + card.querySelector("button")?.addEventListener("click", (event) => { + event.stopPropagation(); + game.openFn(); + }); + + card.addEventListener("mouseenter", () => { + card.style.borderColor = game.accentColor; + card.style.boxShadow = "0 2px 8px rgba(51,102,153,.18)"; + }); + card.addEventListener("mouseleave", () => { + card.style.borderColor = "#d0e4f5"; + card.style.borderLeftColor = game.accentColor; + card.style.boxShadow = ""; + }); + + container.appendChild(card); + } catch (error) { + console.warn(`[游戏大厅] 游戏 ${game.id} 卡片渲染失败:`, error); + } + }); + + loading.style.display = "none"; + container.style.display = "grid"; +} + +/** + * 打开游戏大厅弹窗并加载各游戏状态。 + * + * @returns {Promise} + */ +export async function openGameHall() { + const modal = getGameHallModal(); + if (!modal) { + console.error("[游戏大厅] game-hall-modal 元素不存在,请检查模板加载"); + return; + } + + modal.style.display = "flex"; + + const loading = document.getElementById("game-hall-loading"); + const cards = document.getElementById("game-hall-cards"); + const empty = document.getElementById("game-hall-empty"); + if (loading) { + loading.style.display = "block"; + } + if (cards) { + cards.style.display = "none"; + } + if (empty) { + empty.style.display = "none"; + } + + const jjb = document.getElementById("game-hall-jjb"); + if (jjb && window.chatContext?.userJjb !== undefined) { + jjb.textContent = Number(window.chatContext.userJjb).toLocaleString(); + } + + const { enabledGames, statuses } = await loadGameHallStatus(); + + try { + renderGameCards(enabledGames, statuses); + } catch (error) { + console.error("[游戏大厅] 渲染游戏卡片失败:", error); + if (loading) { + loading.style.display = "none"; + } + if (empty) { + empty.style.display = "block"; + } } } @@ -25,12 +564,23 @@ export function bindGameHallControls() { } gameHallEventsBound = true; + window.openGameHall = openGameHall; + window.closeGameHall = closeGameHall; + document.addEventListener("click", (event) => { - if (!(event.target instanceof Element) || !event.target.closest("[data-game-hall-close]")) { + if (!(event.target instanceof Element)) { return; } - event.preventDefault(); - closeGameHallThroughGlobal(); + if (event.target.closest("[data-game-hall-close]")) { + event.preventDefault(); + closeGameHall(); + return; + } + + const modal = event.target.closest("#game-hall-modal"); + if (modal && event.target === modal) { + closeGameHall(); + } }); } diff --git a/resources/views/chat/partials/games/game-hall.blade.php b/resources/views/chat/partials/games/game-hall.blade.php index d0df2bb..1fb91db 100644 --- a/resources/views/chat/partials/games/game-hall.blade.php +++ b/resources/views/chat/partials/games/game-hall.blade.php @@ -9,11 +9,12 @@ - 神秘占卜:今日占卜次数 + 直接打开按钮 - 钓鱼:状态 + 打开按钮 + JS 逻辑已迁移到 resources/js/chat-room/game-hall.js。 + @author ChatRoom Laravel @version 1.0.0 --}} -{{-- ─── 服务端注入各游戏开关状态(避免前端额外请求)─── --}} @php $gameEnabled = [ 'baccarat' => \App\Models\GameConfig::isEnabled('baccarat'), @@ -26,13 +27,11 @@ 'gomoku' => \App\Models\GameConfig::isEnabled('gomoku'), ]; @endphp - {{-- ═══════════ 游戏大厅弹窗遮罩 ═══════════ --}} - -