- 移除聊天室右下角浮动游戏图标(占卜、百家乐、赛马、老虎机) - 用户名片按钮区:修复已婚/已好友时按钮换行问题,统一单行显示 - 婚礼红包弹窗:重设计为喜庆鲜红背景,领取按钮改为圆形米黄样式 - 新增婚礼红包恢复接口(/wedding/pending-envelopes),刷新后自动恢复领取按钮 - 修复 Alpine :style 字符串覆盖静态 style 导致圆形按钮失效的问题 - 撤职后用户等级改为根据经验值重新计算,不再无条件重置为1 - 管理员修改用户经验值后自动重算等级,有职务用户等级锁定 - 娱乐大厅钓鱼游戏按钮直接调用 startFishing() 简化操作流程 - 新增赛马、占卜、百家乐游戏及相关后端逻辑
397 lines
17 KiB
PHP
397 lines
17 KiB
PHP
{{--
|
||
文件功能:娱乐游戏大厅弹窗组件
|
||
|
||
点击工具栏「娱乐」按钮后弹出,展示所有已开启的游戏:
|
||
- 百家乐:当前场次状态 + 倒计时 + 直接参与按钮
|
||
- 老虎机:今日限额余量 + 直接打开按钮
|
||
- 神秘箱子:已投放数量 + 直接打开按钮
|
||
- 赛马竞猜:当前场次状态 + 参与按钮
|
||
- 神秘占卜:今日占卜次数 + 直接打开按钮
|
||
- 钓鱼:状态 + 打开按钮
|
||
|
||
@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'),
|
||
];
|
||
@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">×</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>
|
||
/** 游戏大厅配置定义(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: () => '🎣 去钓鱼',
|
||
},
|
||
|
||
|
||
];
|
||
|
||
/**
|
||
* 打开游戏大厅弹窗,加载各游戏状态
|
||
*/
|
||
window.openGameHall = async function() {
|
||
document.getElementById('game-hall-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 (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;
|
||
}
|
||
})
|
||
);
|
||
|
||
renderGameCards(enabledGames, statuses);
|
||
};
|
||
|
||
/**
|
||
* 关闭游戏大厅弹窗
|
||
*/
|
||
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 => {
|
||
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 || ' '}</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);
|
||
});
|
||
|
||
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();
|
||
});
|
||
</script>
|