新增百家乐游戏:①数据库表+模型 ②OpenBaccaratRoundJob开局(广播+公屏) ③CloseBaccaratRoundJob结算(摇骰+赔付+CAS防并发) ④BaccaratController下注接口 ⑤前端弹窗(倒计时/骰子动画/历史趋势) ⑥调度器每分钟检查开局 ⑦GameConfig管控开关

This commit is contained in:
2026-03-01 20:25:09 +08:00
parent 8a74bfd639
commit ff28775635
15 changed files with 1424 additions and 0 deletions
+13
View File
@@ -101,6 +101,19 @@ export function initChat(roomId) {
window.dispatchEvent(
new CustomEvent("chat:holiday.started", { detail: e }),
);
})
// ─── 百家乐:开局 & 结算 ──────────────────────────────────
.listen(".baccarat.opened", (e) => {
console.log("百家乐开局:", e);
window.dispatchEvent(
new CustomEvent("chat:baccarat.opened", { detail: e }),
);
})
.listen(".baccarat.settled", (e) => {
console.log("百家乐结算:", e);
window.dispatchEvent(
new CustomEvent("chat:baccarat.settled", { detail: e }),
);
});
}
+2
View File
@@ -137,6 +137,8 @@
@include('chat.partials.marriage-modals')
{{-- ═══════════ 节日福利弹窗组件 ═══════════ --}}
@include('chat.partials.holiday-modal')
{{-- ═══════════ 百家乐游戏面板 ═══════════ --}}
@include('chat.partials.baccarat-panel')
{{-- 全屏特效系统:管理员烟花/下雨/雷电/下雪 --}}
<script src="/js/effects/effect-sounds.js"></script>
@@ -0,0 +1,514 @@
{{--
文件功能:百家乐前台弹窗组件
聊天室内百家乐游戏面板:
- 监听 WebSocket baccarat.opened 事件触发弹窗
- 倒计时下注(大//豹子)
- 监听 baccarat.settled 展示骰子动画 + 结果 + 个人赔付
- 展示近10局历史趋势
--}}
{{-- ─── 百家乐主面板 ─── --}}
<div id="baccarat-panel" x-data="baccaratPanel()" x-show="show" x-cloak>
<div x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100" x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
style="position:fixed; inset:0; background:rgba(0,0,0,.7); z-index:9940;
display:flex; align-items:center; justify-content:center;">
<div
style="width:480px; max-width:96vw; border-radius:24px; overflow:hidden;
box-shadow:0 24px 80px rgba(139,92,246,.5); font-family:system-ui,sans-serif;">
{{-- ─── 顶部标题 ─── --}}
<div
style="background:linear-gradient(135deg,#4c1d95,#6d28d9,#7c3aed); padding:18px 22px 14px; position:relative;">
<div style="display:flex; align-items:center; justify-content:space-between;">
<div>
<div style="color:#fff; font-weight:900; font-size:18px; letter-spacing:1px;">🎲 百家乐</div>
<div style="color:rgba(255,255,255,.6); font-size:12px; margin-top:2px;">
<span x-text="'#' + roundId"></span>
</div>
</div>
{{-- 倒计时 --}}
<div x-show="phase === 'betting'" style="text-align:center;">
<div style="color:#fbbf24; font-size:32px; font-weight:900; line-height:1;" x-text="countdown">
</div>
<div style="color:rgba(255,255,255,.5); font-size:11px;">秒后截止</div>
</div>
{{-- 骰子结果 --}}
<div x-show="phase === 'settled'" style="display:none; text-align:center;">
<div style="font-size:28px;" x-text="diceEmoji"></div>
<div style="color:#fbbf24; font-size:12px; font-weight:bold; margin-top:2px;"
x-text="resultLabel"></div>
</div>
</div>
{{-- 进度条 --}}
<div x-show="phase === 'betting'"
style="margin-top:10px; height:4px; background:rgba(255,255,255,.15); border-radius:2px; overflow:hidden;">
<div style="height:100%; background:#fbbf24; border-radius:2px; transition:width 1s linear;"
:style="'width:' + Math.max(0, (countdown / totalSeconds * 100)) + '%'"></div>
</div>
</div>
{{-- ─── 历史趋势 ─── --}}
<div
style="background:#1e1b4b; padding:8px 16px; display:flex; gap:6px; align-items:center; flex-wrap:wrap;">
<span style="color:rgba(255,255,255,.4); font-size:11px; margin-right:2px;">近期</span>
<template x-for="h in history" :key="h.id">
<span
style="width:22px; height:22px; border-radius:50%; font-size:11px; font-weight:bold;
display:flex; align-items:center; justify-content:center;"
:style="h.result === 'big' ? 'background:#1d4ed8; color:#fff' :
h.result === 'small' ? 'background:#b45309; color:#fff' :
h.result === 'triple' ? 'background:#7c3aed; color:#fff' :
'background:#374151; color:#9ca3af'"
:title="'#' + h.id + ' ' + (h.result === 'big' ? '大' : h.result === 'small' ? '小' : h
.result === 'triple' ? '豹' : '☠')"
x-text="h.result === 'big' ? '大' : h.result === 'small' ? '小' : h.result === 'triple' ? '豹' : '☠'">
</span>
</template>
<span x-show="history.length === 0" style="color:rgba(255,255,255,.3); font-size:11px;">暂无记录</span>
</div>
{{-- ─── 主体内容 ─── --}}
<div style="background:linear-gradient(180deg,#1e1b4b,#1a1035); padding:18px 20px;">
{{-- 押注阶段 --}}
<div x-show="phase === 'betting'">
{{-- 当前下注池统计 --}}
<div style="display:grid; grid-template-columns:1fr 1fr 1fr; gap:8px; margin-bottom:14px;">
<div style="background:rgba(29,78,216,.3); border-radius:10px; padding:8px; text-align:center;">
<div style="color:#60a5fa; font-size:11px;">押大</div>
<div style="color:#fff; font-weight:bold; font-size:13px;"
x-text="Number(totalBetBig).toLocaleString()"></div>
</div>
<div style="background:rgba(180,83,9,.3); border-radius:10px; padding:8px; text-align:center;">
<div style="color:#fbbf24; font-size:11px;">押小</div>
<div style="color:#fff; font-weight:bold; font-size:13px;"
x-text="Number(totalBetSmall).toLocaleString()"></div>
</div>
<div
style="background:rgba(124,58,237,.3); border-radius:10px; padding:8px; text-align:center;">
<div style="color:#c4b5fd; font-size:11px;">押豹子</div>
<div style="color:#fff; font-weight:bold; font-size:13px;"
x-text="Number(totalBetTriple).toLocaleString()"></div>
</div>
</div>
{{-- 已下注状态 / 下注表单 --}}
<div x-show="myBet">
<div
style="background:rgba(34,197,94,.15); border:1px solid rgba(34,197,94,.3); border-radius:12px;
padding:12px 16px; text-align:center; margin-bottom:12px;">
<div style="color:#4ade80; font-weight:bold; font-size:14px;">
已押注「<span x-text="betTypeLabel(myBetType)"></span>
<span x-text="Number(myBetAmount).toLocaleString()"></span> 金币
</div>
<div style="color:rgba(255,255,255,.4); font-size:11px; margin-top:4px;">等待开奖中…</div>
</div>
</div>
<div x-show="!myBet">
{{-- 押注选项 --}}
<div style="display:grid; grid-template-columns:1fr 1fr 1fr; gap:8px; margin-bottom:12px;">
{{-- --}}
<button x-on:click="selectedType='big'"
style="border:none; border-radius:12px; padding:12px 0; cursor:pointer; transition:all .15s; font-weight:bold;"
:style="selectedType === 'big' ?
'background:#1d4ed8; color:#fff; transform:scale(1.05); box-shadow:0 4px 20px rgba(29,78,216,.5)' :
'background:rgba(29,78,216,.2); color:#93c5fd;'">
<div style="font-size:20px;">🔵</div>
<div style="font-size:13px; margin-top:2px;"></div>
<div style="font-size:10px; opacity:.7;">11~17 1:1</div>
</button>
{{-- --}}
<button x-on:click="selectedType='small'"
style="border:none; border-radius:12px; padding:12px 0; cursor:pointer; transition:all .15s; font-weight:bold;"
:style="selectedType === 'small' ?
'background:#b45309; color:#fff; transform:scale(1.05); box-shadow:0 4px 20px rgba(180,83,9,.5)' :
'background:rgba(180,83,9,.2); color:#fcd34d;'">
<div style="font-size:20px;">🟡</div>
<div style="font-size:13px; margin-top:2px;"></div>
<div style="font-size:10px; opacity:.7;">4~10 1:1</div>
</button>
{{-- 豹子 --}}
<button x-on:click="selectedType='triple'"
style="border:none; border-radius:12px; padding:12px 0; cursor:pointer; transition:all .15s; font-weight:bold;"
:style="selectedType === 'triple' ?
'background:#7c3aed; color:#fff; transform:scale(1.05); box-shadow:0 4px 20px rgba(124,58,237,.5)' :
'background:rgba(124,58,237,.2); color:#c4b5fd;'">
<div style="font-size:20px;">💥</div>
<div style="font-size:13px; margin-top:2px;">豹子</div>
<div style="font-size:10px; opacity:.7;">三同 1:24</div>
</button>
</div>
{{-- 快捷金额 + 自定义 --}}
<div style="margin-bottom:10px;">
<div style="display:flex; gap:6px; margin-bottom:8px; flex-wrap:wrap;">
<template x-for="preset in [100, 500, 1000, 5000, 10000]" :key="preset">
<button x-on:click="betAmount = preset"
style="flex:1; min-width:50px; border:none; border-radius:8px; padding:6px 4px;
font-size:12px; font-weight:bold; cursor:pointer; transition:all .1s;"
:style="betAmount === preset ?
'background:#fbbf24; color:#1a1035;' :
'background:rgba(255,255,255,.1); color:rgba(255,255,255,.7);'"
x-text="preset >= 1000 ? (preset/1000)+'k' : preset">
</button>
</template>
</div>
<input type="number" x-model.number="betAmount" min="100" placeholder="自定义金额"
style="width:100%; background:rgba(255,255,255,.1); border:1px solid rgba(255,255,255,.15);
border-radius:8px; padding:8px 12px; color:#fff; font-size:13px; box-sizing:border-box;"
x-on:focus="$event.target.select()">
</div>
{{-- 下注按钮 --}}
<button x-on:click="submitBet()" :disabled="!selectedType || betAmount < 100 || submitting"
style="width:100%; border:none; border-radius:12px; padding:13px; font-size:14px;
font-weight:bold; cursor:pointer; transition:all .15s; letter-spacing:1px;"
:style="(!selectedType || betAmount < 100 || submitting) ?
'background:rgba(255,255,255,.1); color:rgba(255,255,255,.3); cursor:not-allowed;' :
'background:linear-gradient(135deg,#7c3aed,#4f46e5); color:#fff; box-shadow:0 4px 20px rgba(124,58,237,.4);'"
x-text="submitting ? '提交中…' : ('🎲 押注「' + betTypeLabel(selectedType) + '」' + (betAmount > 0 ? ' ' + Number(betAmount).toLocaleString() + ' 金币' : ''))">
</button>
</div>
{{-- 规则提示 --}}
<div style="margin-top:10px; color:rgba(255,255,255,.3); font-size:10px; text-align:center;">
☠️ 3点或18点为庄家收割,全灭无退款。豹子优先于大小判断。
</div>
</div>
{{-- 等待开奖阶段 --}}
<div x-show="phase === 'waiting'" style="display:none; text-align:center; padding:16px 0;">
<div style="font-size:40px; animation:spin 1s linear infinite; display:inline-block;">🎲</div>
<div style="color:rgba(255,255,255,.6); margin-top:8px;">正在摇骰子…</div>
</div>
{{-- 结算阶段 --}}
<div x-show="phase === 'settled'" style="display:none;">
{{-- 骰子点数展示 --}}
<div style="display:flex; justify-content:center; gap:12px; margin-bottom:12px;">
<template x-for="(d, i) in settledDice" :key="i">
<div style="width:52px; height:52px; background:rgba(255,255,255,.95); border-radius:10px;
display:flex; align-items:center; justify-content:center;
font-size:28px; box-shadow:0 4px 12px rgba(0,0,0,.4);
animation:dice-pop .4s ease-out both;"
:style="'animation-delay:' + (i * 0.15) + 's'"
x-text="['⚀','⚁','⚂','⚃','⚄','⚅'][d-1]">
</div>
</template>
</div>
<div style="text-align:center; margin-bottom:10px;">
<div style="font-size:20px; font-weight:bold; color:#fbbf24;" x-text="resultLabel"></div>
<div style="color:rgba(255,255,255,.4); font-size:12px; margin-top:2px;"
x-text="'总点数:' + settledTotal"></div>
</div>
{{-- 个人结果 --}}
<div x-show="myBet"
style="border-radius:12px; padding:12px 16px; text-align:center; margin-bottom:8px;"
:style="myWon ? 'background:rgba(34,197,94,.15); border:1px solid rgba(34,197,94,.3);' :
'background:rgba(239,68,68,.15); border:1px solid rgba(239,68,68,.3);'">
<div style="font-size:15px; font-weight:bold;"
:style="myWon ? 'color:#4ade80;' : 'color:#f87171;'"
x-text="myWon ? '🎉 恭喜!赢得 ' + Number(myPayout).toLocaleString() + ' 金币!' : '💸 本局未中奖'">
</div>
<div style="color:rgba(255,255,255,.4); font-size:11px; margin-top:2px;"
x-text="'押注:' + betTypeLabel(myBetType) + ' ' + Number(myBetAmount).toLocaleString() + ' 金币'">
</div>
</div>
</div>
</div>
{{-- ─── 底部关闭 ─── --}}
<div style="background:rgba(15,10,40,.95); padding:10px 20px; display:flex; justify-content:center;">
<button x-on:click="close()"
style="padding:7px 28px; background:rgba(255,255,255,.08); border:none; border-radius:20px;
font-size:12px; color:rgba(255,255,255,.5); cursor:pointer; transition:all .15s;"
onmouseover="this.style.background='rgba(255,255,255,.15)'"
onmouseout="this.style.background='rgba(255,255,255,.08)'">
关闭
</button>
</div>
</div>
</div>
</div>
{{-- ─── 骰子悬浮入口(游戏开启时常驻) ─── --}}
<div id="baccarat-fab" x-data="{ visible: false }" x-show="visible" x-cloak
style="position:fixed; bottom:90px; right:18px; z-index:9900;">
<button x-on:click="document.getElementById('baccarat-panel')._x_dataStack[0].show = true"
style="width:52px; height:52px; border-radius:50%; border:none; cursor:pointer;
background:linear-gradient(135deg,#7c3aed,#4f46e5);
box-shadow:0 4px 20px rgba(124,58,237,.5);
font-size:22px; display:flex; align-items:center; justify-content:center;
animation:pulse-fab 2s infinite;"
title="百家乐">🎲</button>
</div>
<style>
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes dice-pop {
0% {
transform: scale(0) rotate(-20deg);
opacity: 0;
}
70% {
transform: scale(1.15) rotate(5deg);
}
100% {
transform: scale(1) rotate(0deg);
opacity: 1;
}
}
@keyframes pulse-fab {
0%,
100% {
box-shadow: 0 4px 20px rgba(124, 58, 237, .5);
}
50% {
box-shadow: 0 4px 30px rgba(124, 58, 237, .9);
}
}
</style>
<script>
/**
* 百家乐游戏面板 Alpine 组件
*/
function baccaratPanel() {
return {
show: false,
phase: 'betting', // betting | waiting | settled
roundId: null,
totalSeconds: 60,
countdown: 60,
countdownTimer: null,
// 下注池统计
totalBetBig: 0,
totalBetSmall: 0,
totalBetTriple: 0,
// 本人下注
myBet: false,
myBetType: '',
myBetAmount: 0,
// 下注表单
selectedType: '',
betAmount: 100,
submitting: false,
// 结算结果
settledDice: [],
settledTotal: 0,
resultLabel: '',
diceEmoji: '',
myWon: false,
myPayout: 0,
// 历史记录
history: [],
/**
* 开局:填充局次数据并开始倒计时
*/
openRound(data) {
this.phase = 'betting';
this.roundId = data.round_id;
this.countdown = data.bet_seconds || 60;
this.totalSeconds = this.countdown;
this.myBet = false;
this.myBetType = '';
this.myBetAmount = 0;
this.settledDice = [];
this.selectedType = '';
this.betAmount = 100;
this.show = true;
this.loadCurrentRound();
this.startCountdown();
this.updateFab(true);
},
/**
* 从接口获取当前局的状态(我的下注、投注池)
*/
async loadCurrentRound() {
try {
const res = await fetch('/baccarat/current');
const data = await res.json();
if (data.round) {
this.totalBetBig = data.round.total_bet_big;
this.totalBetSmall = data.round.total_bet_small;
this.totalBetTriple = data.round.total_bet_triple;
if (data.round.my_bet) {
this.myBet = true;
this.myBetType = data.round.my_bet.bet_type;
this.myBetAmount = data.round.my_bet.amount;
}
}
} catch {}
},
/**
* 启动倒计时
*/
startCountdown() {
clearInterval(this.countdownTimer);
this.countdownTimer = setInterval(() => {
this.countdown--;
if (this.countdown <= 0) {
clearInterval(this.countdownTimer);
this.phase = 'waiting';
}
}, 1000);
},
/**
* 提交下注
*/
async submitBet() {
if (!this.selectedType || this.betAmount < 100 || this.submitting) return;
this.submitting = true;
try {
const res = await fetch('/baccarat/bet', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]')?.content,
},
body: JSON.stringify({
round_id: this.roundId,
bet_type: this.selectedType,
amount: this.betAmount,
}),
});
const data = await res.json();
if (data.ok) {
this.myBet = true;
this.myBetType = data.bet_type;
this.myBetAmount = data.amount;
window.chatDialog?.alert(data.message, '下注成功', '#7c3aed');
} else {
window.chatDialog?.alert(data.message || '下注失败', '提示', '#ef4444');
}
} catch {
window.chatDialog?.alert('网络异常,请稍后重试。', '错误', '#ef4444');
}
this.submitting = false;
},
/**
* 显示开奖结果动画
*/
showResult(data) {
clearInterval(this.countdownTimer);
this.settledDice = data.dice;
this.settledTotal = data.total_points;
this.resultLabel = data.result_label;
this.diceEmoji = data.dice.map(d => ['⚀', '⚁', '⚂', '⚃', '⚄', '⚅'][d - 1]).join('');
this.phase = 'settled';
this.show = true;
// 判断本人是否中奖(从后端拿到的 result 与我的下注 type 比较)
if (this.myBet && this.myBetType === data.result && data.result !== 'kill') {
this.myWon = true;
// 简单计算前端显示赔付(实际赔付以后端为准)
const payoutRate = data.result === 'triple' ? 24 : 1;
this.myPayout = this.myBetAmount * (payoutRate + 1);
} else {
this.myWon = false;
this.myPayout = 0;
}
this.updateFab(false);
this.loadHistory();
},
/**
* 加载历史趋势
*/
async loadHistory() {
try {
const res = await fetch('/baccarat/history');
const data = await res.json();
this.history = (data.history || []).reverse();
} catch {}
},
/**
* 更新悬浮按钮显示状态
*/
updateFab(visible) {
const fab = document.getElementById('baccarat-fab');
if (fab) fab._x_dataStack[0].visible = visible;
},
/**
* 关闭面板
*/
close() {
this.show = false;
if (this.phase === 'betting') {
this.updateFab(true); // 还在下注阶段时保留悬浮按钮
}
},
/**
* 押注类型中文标签
*/
betTypeLabel(type) {
return {
big: '大',
small: '小',
triple: '豹子'
} [type] || '';
},
};
}
// ─── WebSocket 监听 ──────────────────────────────────────────────
/** 收到开局事件:弹出押注面板 */
window.addEventListener('chat:baccarat.opened', (e) => {
const panel = document.getElementById('baccarat-panel');
if (panel) Alpine.$data(panel).openRound(e.detail);
});
/** 收到结算事件:展示骰子动画和结果 */
window.addEventListener('chat:baccarat.settled', (e) => {
const panel = document.getElementById('baccarat-panel');
if (panel) Alpine.$data(panel).showResult(e.detail);
});
/** 页面加载时初始化历史记录 */
document.addEventListener('DOMContentLoaded', async () => {
try {
const res = await fetch('/baccarat/history');
const data = await res.json();
const panel = document.getElementById('baccarat-panel');
if (panel) Alpine.$data(panel).history = (data.history || []).reverse();
} catch {}
});
</script>