## 新功能 - 神秘箱子系统(MysteryBox)完整实现: - 新增 MysteryBox / MysteryBoxClaim 模型及迁移文件 - DropMysteryBoxJob / ExpireMysteryBoxJob 队列作业 - MysteryBoxController(/mystery-box/status + /mystery-box/claim) - 支持三种类型:普通箱(500~2000金)/ 稀有箱(5000~20000金)/ 黑化箱(陷阱扣200~1000金) - 调度器自动投放 + 管理员手动投放 - CurrencySource 新增 MYSTERY_BOX / MYSTERY_BOX_TRAP 枚举 - 婚姻状态弹窗(工具栏「婚姻」按钮): - 工具栏「呼叫」改为「婚姻」,点击打开婚姻状态弹窗 - 动态渲染三种状态:单身 / 求婚中 / 已婚 - 被求婚方可直接「答应 / 婉拒」;已婚可申请离婚(含二次确认) ## 优化修复 - frame.blade.php:Alpine.js CDN 补加 defer,修复所有组件初始化报错 - scripts.blade.php:神秘箱子暗号主动拦截(不依赖轮询),领取成功后弹 chatDialog 展示结果,更新金币余额 - MysteryBoxController:claim() 时 change() 补传 room_id 记录来源房间 - 后台游戏管理页(game-configs):投放箱子按钮颜色修复;弹窗替换为 window.adminDialog - admin/layouts:新增全局 adminDialog 弹窗组件(替代原生 alert/confirm) - baccarat-panel:FAB 拖动重构为 Alpine.js baccaratFab() 组件,与 slotFab 一致 - GAMES_TODO.md:神秘箱子移入已完成区,补全修复记录
670 lines
32 KiB
PHP
670 lines
32 KiB
PHP
{{--
|
||
文件功能:百家乐前台弹窗组件
|
||
|
||
聊天室内百家乐游戏面板:
|
||
- 监听 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:14px;
|
||
font-size:15px; font-weight:bold; cursor:pointer; transition:all .2s;
|
||
letter-spacing:1px;"
|
||
:style="(!selectedType || betAmount < 100 || submitting) ? {
|
||
background: '#f1f5f9',
|
||
color: '#94a3b8',
|
||
cursor: 'not-allowed',
|
||
boxShadow: 'none'
|
||
} : {
|
||
background: 'linear-gradient(135deg,#6d28d9,#7c3aed,#8b5cf6)',
|
||
color: '#fff',
|
||
cursor: 'pointer',
|
||
boxShadow: '0 4px 16px rgba(124,58,237,0.5)'
|
||
}">
|
||
<span
|
||
x-text="submitting ? '提交中…' : (!selectedType ? '请先选择大/小/豹子' : '🎲 押注「' + betTypeLabel(selectedType) + '」 ' + Number(betAmount).toLocaleString() + ' 金币')"></span>
|
||
</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:10px; margin-bottom:14px;">
|
||
<template x-for="(d, i) in settledDice" :key="i">
|
||
<div style="width:54px; height:54px; border-radius:12px; font-weight:900;
|
||
display:flex; align-items:center; justify-content:center;
|
||
font-size:26px; box-shadow:0 6px 20px rgba(0,0,0,.5);
|
||
animation:dice-pop .4s ease-out both; color:#1e1b4b;
|
||
background:linear-gradient(145deg,#fff,#e0e7ff);"
|
||
:style="'animation-delay:' + (i * 0.18) + 's'" x-text="d">
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
{{-- 结果标签 --}}
|
||
<div style="text-align:center; margin-bottom:14px;">
|
||
<div style="font-size:26px; font-weight:900; letter-spacing:2px;"
|
||
:style="settledResult === 'big' ? 'color:#60a5fa' :
|
||
settledResult === 'small' ? 'color:#fbbf24' :
|
||
settledResult === 'triple' ? 'color:#c4b5fd' :
|
||
settledResult === 'kill' ? 'color:#f87171' : 'color:#fbbf24'"
|
||
x-text="resultLabel"></div>
|
||
<div style="color:rgba(255,255,255,.35); font-size:12px; margin-top:4px;"
|
||
x-text="'骰子总点数:' + settledTotal + ' 点'"></div>
|
||
</div>
|
||
|
||
{{-- 个人结果卡片 --}}
|
||
<div x-show="myBet">
|
||
{{-- 中奖 --}}
|
||
<div x-show="myWon"
|
||
style="border-radius:14px; overflow:hidden; margin-bottom:4px;
|
||
background:linear-gradient(135deg,rgba(16,185,129,.25),rgba(5,150,105,.15));
|
||
border:1px solid rgba(52,211,153,.4);">
|
||
<div style="padding:14px 16px; text-align:center;">
|
||
<div style="font-size:32px; margin-bottom:4px;">🎉</div>
|
||
<div style="color:#34d399; font-size:18px; font-weight:900;">恭喜中奖!</div>
|
||
<div style="color:#6ee7b7; font-size:24px; font-weight:bold; margin:6px 0;"
|
||
x-text="'+' + Number(myPayout).toLocaleString() + ' 🪙'"></div>
|
||
<div style="color:rgba(255,255,255,.4); font-size:11px;"
|
||
x-text="'押「' + betTypeLabel(myBetType) + '」' + Number(myBetAmount).toLocaleString() + ' 金币 → 赢得 ' + Number(myPayout).toLocaleString() + ' 金币'">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- 未中奖 --}}
|
||
<div x-show="!myWon"
|
||
style="border-radius:14px; overflow:hidden; margin-bottom:4px;
|
||
background:linear-gradient(135deg,rgba(239,68,68,.15),rgba(185,28,28,.1));
|
||
border:1px solid rgba(248,113,113,.25);">
|
||
<div style="padding:14px 16px; text-align:center;">
|
||
<div style="font-size:28px; margin-bottom:4px;">😔</div>
|
||
<div style="color:#f87171; font-size:16px; font-weight:bold; margin-bottom:6px;">本局未中奖
|
||
</div>
|
||
<div
|
||
style="display:inline-flex; align-items:center; gap:8px;
|
||
background:rgba(0,0,0,.2); border-radius:20px; padding:5px 14px;">
|
||
<span style="color:rgba(255,255,255,.5); font-size:12px;">你押了</span>
|
||
<span style="font-weight:bold; font-size:13px; color:#fbbf24;"
|
||
x-text="betTypeLabel(myBetType)"></span>
|
||
<span style="color:rgba(255,255,255,.3); font-size:11px;">·</span>
|
||
<span style="color:rgba(255,255,255,.5); font-size:12px;">开了</span>
|
||
<span style="font-weight:bold; font-size:13px;"
|
||
:style="settledResult === 'big' ? 'color:#60a5fa' :
|
||
settledResult === 'small' ? 'color:#fbbf24' :
|
||
settledResult === 'triple' ? 'color:#c4b5fd' : 'color:#f87171'"
|
||
x-text="resultLabel"></span>
|
||
</div>
|
||
<div style="color:rgba(255,255,255,.3); font-size:11px; margin-top:8px;"
|
||
x-text="'损失 ' + Number(myBetAmount).toLocaleString() + ' 金币'"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- 未下注但看结果 --}}
|
||
<div x-show="!myBet"
|
||
style="text-align:center; color:rgba(255,255,255,.3); font-size:12px; padding:8px 0;">
|
||
本局未参与下注
|
||
</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="baccaratFab()" x-show="visible" x-cloak
|
||
:style="'position:fixed; right:' + posX + 'px; bottom:' + posY + 'px; z-index:9900; touch-action:none; user-select:none;'"
|
||
@pointerdown.prevent="startDrag($event)" @pointermove.window="onDrag($event)" @pointerup.window="endDrag($event)"
|
||
@pointercancel.window="endDrag($event)">
|
||
<button
|
||
style="width:52px; height:52px; border-radius:50%; border:none;
|
||
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; user-select:none;"
|
||
:style="dragging ? 'cursor:grabbing;' : 'cursor:grab;'" title="百家乐下注中(可拖动)">🎲</button>
|
||
</div>
|
||
|
||
<script>
|
||
/**
|
||
* 百家乐骨骰悬浮按钮 Alpine 组件(拖动 + localStorage 位置持久化)
|
||
*/
|
||
function baccaratFab() {
|
||
const STORAGE_KEY = 'baccarat_fab_pos';
|
||
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null');
|
||
return {
|
||
visible: false,
|
||
posX: saved?.x ?? 18,
|
||
posY: saved?.y ?? 90,
|
||
dragging: false,
|
||
_startX: 0,
|
||
_startY: 0,
|
||
_origX: 0,
|
||
_origY: 0,
|
||
_moved: false,
|
||
|
||
startDrag(e) {
|
||
this.dragging = true;
|
||
this._moved = false;
|
||
this._startX = e.clientX;
|
||
this._startY = e.clientY;
|
||
this._origX = this.posX;
|
||
this._origY = this.posY;
|
||
e.currentTarget.setPointerCapture?.(e.pointerId);
|
||
},
|
||
|
||
onDrag(e) {
|
||
if (!this.dragging) return;
|
||
const dx = e.clientX - this._startX;
|
||
const dy = e.clientY - this._startY;
|
||
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) this._moved = true;
|
||
this.posX = Math.max(4, Math.min(window.innerWidth - 60, this._origX - dx));
|
||
this.posY = Math.max(4, Math.min(window.innerHeight - 60, this._origY + dy));
|
||
},
|
||
|
||
endDrag(e) {
|
||
if (!this.dragging) return;
|
||
this.dragging = false;
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
||
x: this.posX,
|
||
y: this.posY
|
||
}));
|
||
if (!this._moved) this.openPanel();
|
||
},
|
||
|
||
openPanel() {
|
||
const panel = document.getElementById('baccarat-panel');
|
||
if (!panel) return;
|
||
const p = Alpine.$data(panel);
|
||
p.show = true;
|
||
if (p.phase === 'betting' && p.countdown > 0 && !p.countdownTimer) {
|
||
p.startCountdown();
|
||
}
|
||
},
|
||
};
|
||
}
|
||
</script>
|
||
|
||
<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,
|
||
settledResult: '',
|
||
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.settledResult = data.result;
|
||
this.resultLabel = data.result_label;
|
||
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) Alpine.$data(fab).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 histRes = await fetch('/baccarat/history');
|
||
const histData = await histRes.json();
|
||
const panel = document.getElementById('baccarat-panel');
|
||
if (panel) {
|
||
Alpine.$data(panel).history = (histData.history || []).reverse();
|
||
}
|
||
|
||
// 再检查是否有正在进行的局
|
||
const curRes = await fetch('/baccarat/current');
|
||
const curData = await curRes.json();
|
||
|
||
if (curData.round && panel) {
|
||
const round = curData.round;
|
||
const seconds = round.seconds_left || 0;
|
||
const panelData = Alpine.$data(panel);
|
||
|
||
if (seconds > 0) {
|
||
// 有进行中的局且还在押注时间内 → 恢复押注面板
|
||
panelData.phase = 'betting';
|
||
panelData.roundId = round.id;
|
||
panelData.totalSeconds = 60; // 服务端配置的窗口
|
||
panelData.countdown = seconds;
|
||
panelData.totalBetBig = round.total_bet_big;
|
||
panelData.totalBetSmall = round.total_bet_small;
|
||
panelData.totalBetTriple = round.total_bet_triple;
|
||
|
||
if (round.my_bet) {
|
||
panelData.myBet = true;
|
||
panelData.myBetType = round.my_bet.bet_type;
|
||
panelData.myBetAmount = round.my_bet.amount;
|
||
}
|
||
|
||
// 只显示悬浮按钮,不自动弹出全屏(避免打扰刚进入的用户)
|
||
panelData.updateFab(true);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.warn('[百家乐] 初始化失败', e);
|
||
}
|
||
});
|
||
</script>
|