迁移娱乐大厅脚本

This commit is contained in:
2026-04-25 14:12:48 +08:00
parent a6bc6c61c5
commit fdd20917a4
3 changed files with 569 additions and 487 deletions
+6 -2
View File
@@ -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;
+559 -9
View File
@@ -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<string, boolean>}
*/
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<object>}
*/
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<object>, statuses:Record<string, unknown>}>}
*/
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<object>} games
* @param {Record<string, unknown>} 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 = `
<div style="display:flex; align-items:flex-start; justify-content:space-between; gap:8px;">
<div style="flex:1;">
<div style="color:#225588; font-weight:bold; font-size:13px; margin-bottom:3px;">${escapeHtml(game.name)}</div>
<div style="color:#666; font-size:11px; line-height:1.4;">${escapeHtml(game.desc)}</div>
</div>
<span style="padding:2px 8px; border-radius:10px; font-size:10px; font-weight:bold; white-space:nowrap; ${status.badgeStyle}">
${escapeHtml(status.badge)}
</span>
</div>
<div style="color:#888; font-size:10px; line-height:1.4; min-height:14px; border-top:1px dashed #e0ecf8; padding-top:6px;">${status.detail ? escapeHtml(status.detail) : "&nbsp;"}</div>
<button type="button"
style="width:100%; border:none; border-radius:4px; padding:7px 8px; font-size:12px; font-weight:bold;
cursor:pointer; color:#fff; transition:opacity .15s;
background:linear-gradient(135deg,${game.accentColor},${game.accentColor}cc);">
${escapeHtml(buttonLabel)}
</button>
`;
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<void>}
*/
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();
}
});
}
@@ -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
<script>
/** 后台游戏开关状态(Blade 服务端注入,1分钟缓存) */
window.GAME_ENABLED = @json($gameEnabled);
</script>
{{-- ═══════════ 游戏大厅弹窗遮罩 ═══════════ --}}
<div id="game-hall-modal"
data-game-enabled='@json($gameEnabled)'
data-game-enabled-url="{{ url('/games/enabled') }}"
style="display:none; position:fixed; inset:0; background:rgba(0,0,0,.55);
z-index:9998; justify-content:center; align-items:center;">
<div id="game-hall-inner"
@@ -87,474 +86,3 @@
</div>
</div>
</div>
<script>
/**
* 游戏大厅模块(使用 IIFE 隔离全局作用域,防止 const 重复初始化导致脚本块失败)
*/
(function() {
/** 游戏大厅状态短缓存,避免用户频繁开关弹窗时重复打多个状态接口 */
const GAME_HALL_CACHE_TTL = 15000;
let gameHallStatusCache = null;
let gameHallStatusCacheAt = 0;
/** 游戏大厅配置定义(ID → 展示配置) */
const GAME_HALL_GAMES = [{
id: 'baccarat',
name: '🎲 百家乐',
desc: '猜骰子大小,1:1 赔率,豹子 1:24',
accentColor: '#336699',
fetchUrl: '/baccarat/current',
openFn: () => {
closeGameHall();
const panel = document.getElementById('baccarat-panel');
if (panel) Alpine.$data(panel).show = true;
},
renderStatus: (data) => {
if (!data?.round) return {
badge: '⏸ 等待开局',
badgeStyle: 'background:#e8f0f8; color:#336699; border:1px solid #b8d0e8',
detail: '下局即将开始,稍后再来'
};
const r = data.round;
if (r.status === 'betting') {
return {
badge: '🟢 押注中',
badgeStyle: 'background:#d1fae5; color:#065f46; border:1px solid #6ee7b7',
detail: `⏱ 剩余 ${r.seconds_left || 0} 秒 | 注池:${Number((r.total_bet_big||0)+(r.total_bet_small||0)+(r.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();
if (typeof openBaccaratLossCoverModal === 'function') {
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();
const panel = document.getElementById('slot-panel');
if (panel) Alpine.$data(panel).show = true;
},
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();
const panel = document.getElementById('horse-race-panel');
if (panel) Alpine.$data(panel).openFromHall();
},
renderStatus: (data) => {
if (!data?.race) return {
badge: '⏸ 等待开赛',
badgeStyle: 'background:#e8f0f8; color:#336699; border:1px solid #b8d0e8',
detail: '下场赛马即将开始'
};
const r = data.race;
if (r.status === 'betting') {
return {
badge: '🟢 押注中',
badgeStyle: 'background:#d1fae5; color:#065f46; border:1px solid #6ee7b7',
detail: `⏱ 剩余 ${r.seconds_left || 0} 秒 | 注池:${Number(r.total_pool || 0).toLocaleString()} 金`
};
}
if (r.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();
const panel = document.getElementById('fortune-panel');
if (panel) Alpine.$data(panel).show = true;
},
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();
// 直接触发钓鱼,无需手动输入指令
if (typeof startFishing === 'function') {
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();
if (typeof openLotteryPanel === 'function') openLotteryPanel();
},
renderStatus: (data) => {
if (!data?.issue) return {
badge: '⏸ 等待开期',
badgeStyle: 'background:#e8f0f8; color:#336699; border:1px solid #b8d0e8',
detail: '暂无进行中期次'
};
const iss = data.issue;
const pool = Number(iss.pool_amount || 0).toLocaleString();
if (data.is_open) {
const h = Math.floor(iss.seconds_left / 3600);
const m = Math.floor((iss.seconds_left % 3600) / 60);
const timeStr = h > 0 ? `${h}h ${m}m` : `${m}m`;
return {
badge: iss.is_super_issue ? '🎊 超级期购票中' : '🟢 购票中',
badgeStyle: 'background:#fef2f2; color:#b91c1c; border:1px solid #fca5a5',
detail: `💰 奖池 ${pool} 金 | 距开奖 ${timeStr} | 第 ${iss.issue_no} 期`
};
}
if (iss.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();
if (typeof openGomokuPanel === 'function') openGomokuPanel();
},
renderStatus: () => ({
badge: '♟️ 随时对弈',
badgeStyle: 'background:#e8eef8; color:#1e3a5f; border:1px solid #9db3d4',
detail: 'PvP 胜利 +80 金币 | AI 专家难度胜利 +300 金币',
}),
btnLabel: () => '♟️ 开始对弈',
},
];
/**
* 打开游戏大厅弹窗,加载各游戏状态
*/
window.openGameHall = async function() {
const modal = document.getElementById('game-hall-modal');
if (!modal) {
console.error('[游戏大厅] game-hall-modal 元素不存在,请检查模板加载');
return;
}
modal.style.display = 'flex';
document.getElementById('game-hall-loading').style.display = 'block';
document.getElementById('game-hall-cards').style.display = 'none';
document.getElementById('game-hall-empty').style.display = 'none';
const jjbEl = document.getElementById('game-hall-jjb');
if (jjbEl && window.chatContext?.userJjb !== undefined) {
jjbEl.textContent = Number(window.chatContext.userJjb).toLocaleString();
}
const { enabledGames, statuses } = await loadGameHallStatus();
try {
renderGameCards(enabledGames, statuses);
} catch (err) {
console.error('[游戏大厅] 渲染游戏卡片失败:', err);
document.getElementById('game-hall-loading').style.display = 'none';
document.getElementById('game-hall-empty').style.display = 'block';
}
};
/**
* 加载游戏大厅状态,短时间内复用结果以减少重复请求。
*
* @returns {Promise<{enabledGames:Array, statuses:Object}>}
*/
async function loadGameHallStatus() {
const now = Date.now();
if (gameHallStatusCache && now - gameHallStatusCacheAt < GAME_HALL_CACHE_TTL) {
return gameHallStatusCache;
}
// 打开大厅时拉取后台开关状态;短缓存能兼顾配置同步和重复打开速度。
let enabledMap = window.GAME_ENABLED ?? {};
try {
const r = await fetch('/games/enabled', {
headers: {
'Accept': 'application/json'
}
});
if (r.ok) enabledMap = await r.json();
} catch {
/* 网络异常时降级使用页面注入值 */
}
// 过滤出后台已开启的游戏
const enabledGames = GAME_HALL_GAMES.filter(g => enabledMap[g.id] !== false);
// 并行请求有状态接口的游戏
const statuses = {};
await Promise.all(
enabledGames.filter(g => g.fetchUrl).map(async g => {
try {
const res = await fetch(g.fetchUrl);
statuses[g.id] = await res.json();
} catch {
statuses[g.id] = null;
}
})
);
gameHallStatusCache = { enabledGames, statuses };
gameHallStatusCacheAt = Date.now();
return gameHallStatusCache;
}
/**
* 关闭游戏大厅弹窗
*/
window.closeGameHall = function() {
document.getElementById('game-hall-modal').style.display = 'none';
};
/**
* 渲染所有游戏卡片(海军蓝风格)
*
* @param {Array} games 已过滤的游戏配置列表
* @param {Object} statuses 各游戏的 API 返回数据
*/
function renderGameCards(games, statuses) {
const container = document.getElementById('game-hall-cards');
container.innerHTML = '';
if (games.length === 0) {
document.getElementById('game-hall-loading').style.display = 'none';
document.getElementById('game-hall-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 btnLabel = 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 = `
<div style="display:flex; align-items:flex-start; justify-content:space-between; gap:8px;">
<div style="flex:1;">
<div style="color:#225588; font-weight:bold; font-size:13px; margin-bottom:3px;">${game.name}</div>
<div style="color:#666; font-size:11px; line-height:1.4;">${game.desc}</div>
</div>
<span style="padding:2px 8px; border-radius:10px; font-size:10px; font-weight:bold; white-space:nowrap; ${status.badgeStyle}">
${status.badge}
</span>
</div>
<div style="color:#888; font-size:10px; line-height:1.4; min-height:14px; border-top:1px dashed #e0ecf8; padding-top:6px;">${status.detail || '&nbsp;'}</div>
<button
style="width:100%; border:none; border-radius:4px; padding:7px 8px; font-size:12px; font-weight:bold;
cursor:pointer; color:#fff; transition:opacity .15s;
background:linear-gradient(135deg,${game.accentColor},${game.accentColor}cc);"
onmouseover="this.style.opacity='.85'"
onmouseout="this.style.opacity='1'">
${btnLabel}
</button>
`;
card.querySelector('button').addEventListener('click', (e) => {
e.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 (err) {
// 单个游戏卡片渲染失败不影响其他游戏展示
console.warn(`[游戏大厅] 游戏 ${game.id} 卡片渲染失败:`, err);
}
});
document.getElementById('game-hall-loading').style.display = 'none';
container.style.display = 'grid';
}
// 点击遮罩关闭弹窗
document.getElementById('game-hall-modal').addEventListener('click', function(e) {
if (e.target === this) closeGameHall();
});
})(); // end IIFE
</script>