// 聊天室娱乐大厅弹窗逻辑,集中加载游戏状态并渲染入口卡片。 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} */ 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"; } } } /** * 绑定娱乐大厅基础控件事件。 * * @returns {void} */ export function bindGameHallControls() { if (gameHallEventsBound || typeof document === "undefined") { return; } gameHallEventsBound = true; window.openGameHall = openGameHall; window.closeGameHall = closeGameHall; document.addEventListener("click", (event) => { if (!(event.target instanceof Element)) { return; } 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(); } }); }