Files
chatroom/resources/views/chat/partials/baccarat-panel.blade.php
lkddi 040dbdef3c 优化:全站金币图标由 🪙(银灰色)统一替换为 💰(金黄色)
🪙 在多数平台/字体上渲染为银灰色,与「金币」语义不符;
💰 各平台均渲染为金黄色,更直观传达金币概念。

涉及文件(43处):
- app/Jobs:百家乐、赛马结算广播
- app/Http/Controllers:管理员命令、红包、老虎机、神秘箱子
- app/Listeners
- resources/views:聊天室各游戏面板、商店、toolbar、后台页面等
2026-03-04 15:00:02 +08:00

651 lines
31 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.
{{--
文件功能:百家乐前台弹窗组件
聊天室内百家乐游戏面板:
- 监听 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,.55); z-index:9940;
display:flex; align-items:center; justify-content:center;">
<div
style="width:460px; max-width:96vw; border-radius:8px; overflow:hidden;
box-shadow:0 8px 32px rgba(0,0,0,.3); font-family:'Microsoft YaHei',SimSun,sans-serif; background:#fff;">
{{-- ─── 顶部标题 ─── --}}
<div
style="background:linear-gradient(135deg,#336699,#5a8fc0); padding:10px 16px;
display:flex; align-items:center; justify-content:space-between;">
<div>
<div style="color:#fff; font-weight:bold; font-size:14px;">🎲 百家乐</div>
<div style="color:rgba(255,255,255,.75); font-size:11px; margin-top:1px;">
<span x-text="'#' + roundId"></span>
</div>
</div>
{{-- 倒计时 --}}
<div x-show="phase === 'betting'" style="text-align:center;">
<div style="color:#fbbf24; font-size:28px; font-weight:900; line-height:1;" x-text="countdown">
</div>
<div style="color:rgba(255,255,255,.7); font-size:11px;">秒后截止</div>
</div>
{{-- 骰子结果 --}}
<div x-show="phase === 'settled'" style="display:none; text-align:center;">
<div style="font-size:24px;" 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="height:3px; background:rgba(255,255,255,.2);">
<div style="height:100%; background:#fbbf24; transition:width 1s linear;"
:style="'width:' + Math.max(0, (countdown / totalSeconds * 100)) + '%'"></div>
</div>
{{-- ─── 历史趋势 ─── --}}
<div
style="background:#f0f6ff; padding:7px 14px; display:flex; gap:5px; align-items:center;
flex-wrap:wrap; border-bottom:1px solid #d0e4f5;">
<span style="color:#99b0cc; 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; color:#fff;"
:style="h.result === 'big' ? 'background:#1d4ed8' :
h.result === 'small' ? 'background:#d97706' :
h.result === 'triple' ? 'background:#7c3aed' :
'background:#94a3b8'"
: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:#b0c4d8; font-size:11px;">暂无记录</span>
</div>
{{-- ─── 主体内容(白底) ─── --}}
<div style="background:#fff; padding:14px 16px;">
{{-- 押注阶段 --}}
<div x-show="phase === 'betting'">
{{-- 下注池统计 --}}
<div style="display:grid; grid-template-columns:1fr 1fr 1fr; gap:8px; margin-bottom:12px;">
<div
style="background:#eff6ff; border:1px solid #bfdbfe; border-radius:8px; padding:8px; text-align:center;">
<div style="color:#1d4ed8; font-size:11px; font-weight:bold;">押大</div>
<div style="color:#1e40af; font-weight:bold; font-size:13px;"
x-text="Number(totalBetBig).toLocaleString()"></div>
</div>
<div
style="background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:8px; text-align:center;">
<div style="color:#d97706; font-size:11px; font-weight:bold;">押小</div>
<div style="color:#b45309; font-weight:bold; font-size:13px;"
x-text="Number(totalBetSmall).toLocaleString()"></div>
</div>
<div
style="background:#f5f3ff; border:1px solid #ddd6fe; border-radius:8px; padding:8px; text-align:center;">
<div style="color:#7c3aed; font-size:11px; font-weight:bold;">押豹子</div>
<div style="color:#6d28d9; font-weight:bold; font-size:13px;"
x-text="Number(totalBetTriple).toLocaleString()"></div>
</div>
</div>
{{-- 已下注状态 --}}
<div x-show="myBet">
<div
style="background:#f0fdf4; border:1px solid #86efac; border-radius:10px;
padding:10px 14px; text-align:center; margin-bottom:10px;">
<div style="color:#16a34a; font-weight:bold; font-size:13px;">
已押注「<span x-text="betTypeLabel(myBetType)"></span>
<span x-text="Number(myBetAmount).toLocaleString()"></span> 金币
</div>
<div style="color:#86a896; font-size:11px; margin-top:3px;">等待开奖中…</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-radius:10px; padding:12px 0; cursor:pointer; transition:all .15s; font-weight:bold; font-family:inherit;"
:style="selectedType === 'big'
?
'border:2px solid #1d4ed8; background:#1d4ed8; color:#fff; transform:scale(1.05); box-shadow:0 4px 14px rgba(29,78,216,.3);' :
'border:2px solid #bfdbfe; background:#eff6ff; color:#1d4ed8;'">
<div style="font-size:22px;">🔵</div>
<div style="font-size:13px; margin-top:4px;"></div>
<div style="font-size:10px; margin-top:2px; opacity:.75;">11~17 1:1</div>
</button>
{{-- --}}
<button x-on:click="selectedType='small'"
style="border-radius:10px; padding:12px 0; cursor:pointer; transition:all .15s; font-weight:bold; font-family:inherit;"
:style="selectedType === 'small'
?
'border:2px solid #d97706; background:#d97706; color:#fff; transform:scale(1.05); box-shadow:0 4px 14px rgba(217,119,6,.3);' :
'border:2px solid #fde68a; background:#fffbeb; color:#b45309;'">
<div style="font-size:22px;">🟡</div>
<div style="font-size:13px; margin-top:4px;"></div>
<div style="font-size:10px; margin-top:2px; opacity:.75;">4~10 1:1</div>
</button>
{{-- 豹子 --}}
<button x-on:click="selectedType='triple'"
style="border-radius:10px; padding:12px 0; cursor:pointer; transition:all .15s; font-weight:bold; font-family:inherit;"
:style="selectedType === 'triple'
?
'border:2px solid #7c3aed; background:#7c3aed; color:#fff; transform:scale(1.05); box-shadow:0 4px 14px rgba(124,58,237,.3);' :
'border:2px solid #ddd6fe; background:#f5f3ff; color:#7c3aed;'">
<div style="font-size:22px;">💥</div>
<div style="font-size:13px; margin-top:4px;">豹子</div>
<div style="font-size:10px; margin-top:2px; opacity:.75;">三同 1:24</div>
</button>
</div>
{{-- 快捷金额 --}}
<div style="display:grid; grid-template-columns:repeat(5,1fr); gap:6px; margin-bottom:10px;">
<template x-for="preset in [100, 500, 1000, 5000, 10000]" :key="preset">
<button x-on:click="betAmount = preset"
style="border-radius:20px; padding:8px 2px; font-size:13px; font-weight:bold;
cursor:pointer; transition:all .15s; font-family:inherit; text-align:center;"
:style="betAmount === preset ?
'background:#336699; color:#fff; border:none; box-shadow:0 3px 10px rgba(51,102,153,.35); transform:translateY(-1px);' :
'background:#fff; color:#336699; border:1.5px solid #c0d8ef;'"
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:#f6faff; border:1.5px solid #d0e4f5;
border-radius:8px; padding:8px 12px; color:#225588; font-size:13px;
box-sizing:border-box; margin-bottom:10px;"
x-on:focus="$event.target.select()">
{{-- 下注按钮 --}}
<button x-on:click="submitBet()" :disabled="!selectedType || betAmount < 100 || submitting"
:style="(!selectedType || betAmount < 100 || submitting) ?
'display:block; width:100%; border:none; border-radius:12px; padding:13px 0; font-size:14px; font-weight:bold; cursor:not-allowed; transition:all .2s; background:#e0e8f0; color:#99a8b8; box-shadow:none; font-family:inherit;' :
'display:block; width:100%; border:none; border-radius:12px; padding:13px 0; font-size:14px; font-weight:bold; cursor:pointer; transition:all .2s; background:linear-gradient(135deg,#336699,#5a8fc0); color:#fff; box-shadow:0 4px 14px rgba(51,102,153,.3); font-family:inherit;'">
<span
x-text="submitting ? '提交中…' : (!selectedType ? '请先选择大/小/豹子' : '🎲 押注「' + betTypeLabel(selectedType) + '」 ' + Number(betAmount).toLocaleString() + ' 金币')"></span>
</button>
</div>
{{-- 规则提示 --}}
<div style="margin-top:10px; color:#99b0cc; font-size:10px; text-align:center;">
☠️ 3点或18点为庄家收割全灭无退款。豹子优先于大小判断。
</div>
</div>
{{-- 等待开奖阶段 --}}
<div x-show="phase === 'waiting'" style="display:none; text-align:center; padding:20px 0;">
<div style="font-size:44px; animation:spin 1s linear infinite; display:inline-block;">🎲</div>
<div style="color:#5a8fc0; margin-top:10px; font-weight:bold;">正在摇骰子…</div>
</div>
{{-- 结算阶段 --}}
<div x-show="phase === 'settled'" style="display:none;">
{{-- 骰子展示 --}}
<div style="display:flex; justify-content:center; gap:10px; margin-bottom:16px;">
<template x-for="(d, i) in settledDice" :key="i">
<div style="width:56px; height:56px; border-radius:12px; font-weight:900;
display:flex; align-items:center; justify-content:center;
font-size:26px; animation:dice-pop .4s ease-out both;
color:#1e3a5f; background:linear-gradient(145deg,#ffffff,#e8f0fb);
border:1.5px solid #d0e4f5; box-shadow:0 4px 14px rgba(51,102,153,.12);"
: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:24px; font-weight:900; letter-spacing:2px;"
:style="settledResult === 'big' ? 'color:#1d4ed8' :
settledResult === 'small' ? 'color:#d97706' :
settledResult === 'triple' ? 'color:#7c3aed' :
settledResult === 'kill' ? 'color:#dc2626' : 'color:#336699'"
x-text="resultLabel"></div>
<div style="color:#99b0cc; font-size:12px; margin-top:4px;"
x-text="'骰子总点数:' + settledTotal + ' 点'"></div>
</div>
{{-- 个人结果卡片 --}}
<div x-show="myBet">
{{-- 中奖 --}}
<div x-show="myWon"
style="border-radius:12px; overflow:hidden; margin-bottom:4px;
background:#f0fdf4; border:1px solid #86efac;">
<div style="padding:14px 16px; text-align:center;">
<div style="font-size:32px; margin-bottom:4px;">🎉</div>
<div style="color:#16a34a; font-size:18px; font-weight:900;">恭喜中奖!</div>
<div style="color:#15803d; font-size:24px; font-weight:bold; margin:6px 0;"
x-text="'+' + Number(myPayout).toLocaleString() + ' 💰'"></div>
<div style="color:#86a896; font-size:11px;"
x-text="'押「' + betTypeLabel(myBetType) + '」' + Number(myBetAmount).toLocaleString() + ' 金币 → 赢得 ' + Number(myPayout).toLocaleString() + ' 金币'">
</div>
</div>
</div>
{{-- 未中奖 --}}
<div x-show="!myWon"
style="border-radius:12px; overflow:hidden; margin-bottom:4px;
background:#fff5f5; border:1px solid #fecaca;">
<div style="padding:14px 16px; text-align:center;">
<div style="font-size:28px; margin-bottom:4px;">😔</div>
<div style="color:#dc2626; font-size:16px; font-weight:bold; margin-bottom:8px;">本局未中奖
</div>
<div
style="display:inline-flex; align-items:center; gap:8px;
background:#fef2f2; border-radius:20px; padding:5px 14px; border:1px solid #fecaca;">
<span style="color:#64748b; font-size:12px;">你押了</span>
<span style="font-weight:bold; font-size:13px; color:#d97706;"
x-text="betTypeLabel(myBetType)"></span>
<span style="color:#d0d5db; font-size:11px;">·</span>
<span style="color:#64748b; font-size:12px;">开了</span>
<span style="font-weight:bold; font-size:13px;"
:style="settledResult === 'big' ? 'color:#1d4ed8' :
settledResult === 'small' ? 'color:#d97706' :
settledResult === 'triple' ? 'color:#7c3aed' : 'color:#dc2626'"
x-text="resultLabel"></span>
</div>
<div style="color:#94a3b8; font-size:11px; margin-top:8px;"
x-text="'损失 ' + Number(myBetAmount).toLocaleString() + ' 金币'"></div>
</div>
</div>
</div>
{{-- 未下注但看结果 --}}
<div x-show="!myBet" style="text-align:center; color:#99b0cc; font-size:12px; padding:8px 0;">
本局未参与下注
</div>
</div>
</div>
{{-- 底部关闭 --}}
<div
style="background:#fff; border-top:1px solid #d0e4f5; padding:12px 16px; display:flex; justify-content:center;">
<button x-on:click="close()"
style="padding:10px 48px; min-width:140px; background:#f0f6ff; border:1px solid #b0d0ee; border-radius:12px;
font-size:14px; font-weight:bold; color:#336699; cursor:pointer; transition:all .15s; font-family:inherit;"
onmouseover="this.style.background='#ddeeff'" onmouseout="this.style.background='#f0f6ff'">
关闭
</button>
</div>
</div>
</div>
</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(51, 102, 153, .4);
}
50% {
box-shadow: 0 4px 30px rgba(51, 102, 153, .8);
}
}
</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, '下注成功', '#336699');
} 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>