Files
chatroom/resources/views/chat/partials/games/game-hall.blade.php

480 lines
23 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{--
文件功能:娱乐游戏大厅弹窗组件
点击工具栏「娱乐」按钮后弹出,展示所有已开启的游戏:
- 百家乐:当前场次状态 + 倒计时 + 直接参与按钮
- 老虎机:今日限额余量 + 直接打开按钮
- 神秘箱子:已投放数量 + 直接打开按钮
- 赛马竞猜:当前场次状态 + 参与按钮
- 神秘占卜:今日占卜次数 + 直接打开按钮
- 钓鱼:状态 + 打开按钮
@author ChatRoom Laravel
@version 1.0.0
--}}
{{-- ─── 服务端注入各游戏开关状态(避免前端额外请求)─── --}}
@php
$gameEnabled = [
'baccarat' => \App\Models\GameConfig::isEnabled('baccarat'),
'slot_machine' => \App\Models\GameConfig::isEnabled('slot_machine'),
'mystery_box' => \App\Models\GameConfig::isEnabled('mystery_box'),
'horse_racing' => \App\Models\GameConfig::isEnabled('horse_racing'),
'fortune_telling' => \App\Models\GameConfig::isEnabled('fortune_telling'),
'fishing' => \App\Models\GameConfig::isEnabled('fishing'),
'lottery' => \App\Models\GameConfig::isEnabled('lottery'),
'gomoku' => \App\Models\GameConfig::isEnabled('gomoku'),
];
@endphp
<script>
/** 后台游戏开关状态Blade 服务端注入1分钟缓存 */
window.GAME_ENABLED = @json($gameEnabled);
</script>
{{-- ═══════════ 游戏大厅弹窗遮罩 ═══════════ --}}
<div id="game-hall-modal"
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"
style="width:680px; max-width:96vw; max-height:88vh; border-radius:8px; overflow:hidden;
box-shadow:0 8px 32px rgba(0,0,0,.3); display:flex; flex-direction:column;
background:#fff; font-family:'Microsoft YaHei',SimSun,sans-serif;">
{{-- ─── 标题栏(与商店弹窗同风格)─── --}}
<div
style="background:linear-gradient(135deg,#336699,#5a8fc0); color:#fff;
padding:10px 16px; display:flex; align-items:center; gap:10px; flex-shrink:0;">
<div style="font-size:14px; font-weight:bold; flex:1;">🎮 娱乐大厅</div>
<div
style="font-size:12px; color:#d0e8ff; display:flex; align-items:center; gap:3px;
background:rgba(0,0,0,.2); padding:2px 8px; border-radius:10px;">
💰 <strong id="game-hall-jjb" style="color:#ffe082; font-size:13px;">--</strong> 金币
</div>
<span onclick="closeGameHall()"
style="cursor:pointer; font-size:18px; opacity:.8; line-height:1; transition:opacity .15s;"
onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=.8">&times;</span>
</div>
{{-- ─── 游戏卡片网格 ─── --}}
<div style="flex:1; overflow-y:auto; background:#f6faff; padding:12px;">
{{-- 加载状态 --}}
<div id="game-hall-loading" style="text-align:center; color:#336699; padding:40px 0; font-size:13px;">
<div style="font-size:28px; margin-bottom:8px;"></div>
加载游戏状态中…
</div>
{{-- 游戏卡片容器 --}}
<div id="game-hall-cards" style="display:none; grid-template-columns:1fr 1fr; gap:10px;">
</div>
{{-- 全部未开启提示 --}}
<div id="game-hall-empty"
style="display:none; text-align:center; color:#336699; padding:40px 0; font-size:13px;">
<div style="font-size:28px; margin-bottom:8px;">🔒</div>
暂无开启的游戏,请联系管理员
</div>
</div>
{{-- ─── 底部 ─── --}}
<div
style="background:#fff; border-top:1px solid #d0e4f5; padding:8px 16px;
display:flex; justify-content:center; flex-shrink:0;">
<button onclick="closeGameHall()"
style="padding:5px 24px; background:#f0f6ff; border:1px solid #b0d0ee; border-radius:4px;
font-size:12px; color:#336699; cursor:pointer; transition:all .15s;"
onmouseover="this.style.background='#ddeeff'" onmouseout="this.style.background='#f0f6ff'">关闭</button>
</div>
</div>
</div>
<script>
/**
* 游戏大厅模块(使用 IIFE 隔离全局作用域,防止 const 重复初始化导致脚本块失败)
*/
(function() {
/** 游戏大厅配置定义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: '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();
}
// 每次打开均实时拉取后台开关状态(避免页面不刷新时开关不同步)
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;
}
})
);
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';
}
};
/**
* 关闭游戏大厅弹窗
*/
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>