🪙 在多数平台/字体上渲染为银灰色,与「金币」语义不符; 💰 各平台均渲染为金黄色,更直观传达金币概念。 涉及文件(43处): - app/Jobs:百家乐、赛马结算广播 - app/Http/Controllers:管理员命令、红包、老虎机、神秘箱子 - app/Listeners - resources/views:聊天室各游戏面板、商店、toolbar、后台页面等
678 lines
32 KiB
PHP
678 lines
32 KiB
PHP
{{--
|
||
文件功能:赛马竞猜前台弹窗组件
|
||
|
||
聊天室内赛马竞猜游戏面板:
|
||
- 监听 WebSocket horse.opened 事件触发弹窗
|
||
- 展示参赛马匹列表和实时注池赔率
|
||
- 倒计时押注后进入跑马阶段(动态进度条)
|
||
- 监听 horse.progress 更新赛道动画
|
||
- 监听 horse.settled 展示结果 + 个人赔付
|
||
- 展示近10场历史趋势
|
||
--}}
|
||
|
||
{{-- ─── 赛马主面板 ─── --}}
|
||
<div id="horse-race-panel" x-data="horseRacePanel()" 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:9941;
|
||
display:flex; align-items:center; justify-content:center;">
|
||
|
||
<div
|
||
style="width:500px; 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; gap:10px;">
|
||
<div style="font-size:14px; font-weight:bold; color:#fff; flex:1;">
|
||
🐎 赛马竞猜
|
||
<span style="font-size:11px; font-weight:normal; color:rgba(255,255,255,.6); margin-left:4px;"
|
||
x-text="raceId ? '#' + raceId + ' 场' : ''"></span>
|
||
</div>
|
||
{{-- 倒计时(押注阶段) --}}
|
||
<div x-show="phase === 'betting'"
|
||
style="font-size:12px; color:#fff; background:rgba(255,255,255,.2); padding:4px 12px; border-radius:20px; border:1px solid rgba(255,255,255,.3); box-shadow:0 2px 4px rgba(0,0,0,.1) inset; display:flex; align-items:center; gap:4px;">
|
||
⏳ 剩 <span x-text="countdown" style="font-weight:bold; color:#fef08a; font-size:14px;"></span> 秒
|
||
</div>
|
||
{{-- 跑马阶段 --}}
|
||
<div x-show="phase === 'running'" style="display:none;"
|
||
style="font-size:12px; color:rgba(255,255,255,.8); background:rgba(0,0,0,.2); padding:2px 10px; border-radius:10px;">
|
||
🏇 跑马中…
|
||
</div>
|
||
{{-- 结算 --}}
|
||
<div x-show="phase === 'settled'" style="display:none;"
|
||
style="font-size:12px; color:#ffe082; font-weight:bold;">🏆 已结算</div>
|
||
<span onclick="Alpine.$data(document.getElementById('horse-race-panel')).close()"
|
||
style="cursor:pointer; font-size:18px; color:rgba(255,255,255,.8); line-height:1; transition:opacity .15s;"
|
||
onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=.8">×</span>
|
||
</div>
|
||
|
||
{{-- 押注进度条(蓝色风格) --}}
|
||
<div x-show="phase === 'betting'" style="height:3px; background:#d0e4f5; overflow:hidden;">
|
||
<div style="height:100%; background:#336699; transition:width 1s linear;"
|
||
:style="'width:' + Math.max(0, (countdown / totalSeconds * 100)) + '%'"></div>
|
||
</div>
|
||
|
||
{{-- ─── 历史趋势(蓝白色系)─── --}}
|
||
<div
|
||
style="background:#f6faff; padding:6px 16px; border-bottom:1px solid #d0e4f5;
|
||
display:flex; gap:5px; align-items:center; flex-wrap:wrap; min-height:32px;">
|
||
<span style="color:#336699; font-size:11px; margin-right:2px; font-weight:bold;">近期冒涨:</span>
|
||
<template x-for="h in history" :key="h.id">
|
||
<span
|
||
style="padding:1px 8px; border-radius:10px; font-size:10px; font-weight:bold;
|
||
background:#e8f0f8; color:#336699; border:1px solid #b8d0e8;"
|
||
:title="'#' + h.id + ' 冠军:' + h.winner_name" x-text="h.winner_name"></span>
|
||
</template>
|
||
<span x-show="history.length === 0" style="color:#aaa; font-size:11px;">暂无记录</span>
|
||
</div>
|
||
|
||
{{-- ─── 主体内容(白底)─── --}}
|
||
<div style="background:#fff; padding:14px 16px;">
|
||
|
||
{{-- ── 押注阶段 ── --}}
|
||
<div x-show="phase === 'betting'">
|
||
{{-- 注池统计 --}}
|
||
<div
|
||
style="color:#336699; font-size:11px; margin-bottom:8px; text-align:center; background:#e8f0f8; border-radius:4px; padding:4px 0;">
|
||
注池总额:<span style="color:#b45309; font-weight:bold;"
|
||
x-text="Number(totalPool).toLocaleString() + ' 金币'"></span>
|
||
</div>
|
||
|
||
{{-- 马匹列表 --}}
|
||
<div style="display:flex; flex-direction:column; gap:8px; margin-bottom:12px;">
|
||
<template x-for="horse in horses" :key="horse.id">
|
||
<div style="border-radius:12px; padding:10px 14px; cursor:pointer; transition:all .15s; border:2px solid transparent;"
|
||
:style="selectedHorse === horse.id ?
|
||
'background:#e8f0f8; border-color:#336699;' :
|
||
'background:#f6faff; border-color:#d0e4f5;'"
|
||
@click="myBet ? null : selectedHorse = horse.id">
|
||
<div style="display:flex; align-items:center; justify-content:space-between;">
|
||
<div style="display:flex; align-items:center; gap:10px;">
|
||
{{-- 选中勾选 --}}
|
||
<div style="width:20px; height:20px; border-radius:50%; border:2px solid; display:flex; align-items:center; justify-content:center; font-size:10px; flex-shrink:0;"
|
||
:style="selectedHorse === horse.id ?
|
||
'border-color:#336699; background:#336699; color:#fff' :
|
||
'border-color:#b0c8e0; color:transparent'">
|
||
✓</div>
|
||
<div style="font-size:22px;" x-text="horse.emoji"></div>
|
||
<div>
|
||
<div style="color:#225588; font-weight:bold; font-size:13px;"
|
||
x-text="horse.name"></div>
|
||
<div style="color:#888; font-size:10px;">
|
||
注池:<span x-text="Number(horse.pool || 0).toLocaleString()"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{{-- 实时赔率 --}}
|
||
<div style="text-align:right;">
|
||
<div style="color:#b45309; font-weight:900; font-size:15px;"
|
||
x-text="horse.odds ? '×' + horse.odds : '—'"></div>
|
||
<div style="color:#999; font-size:10px;">赔率</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
{{-- 已下注状态 --}}
|
||
<div x-show="myBet">
|
||
<div
|
||
style="background:#e8fde8; border:1px solid #a3e6b0; border-radius:6px;
|
||
padding:10px 14px; text-align:center; margin-bottom:8px;">
|
||
<div style="color:#16a34a; font-weight:bold; font-size:13px;">
|
||
✅ 已押注「<span x-text="myBetHorseName"></span>」
|
||
<span x-text="Number(myBetAmount).toLocaleString()"></span> 金币
|
||
</div>
|
||
<div style="color:#888; font-size:11px; margin-top:3px;">等待开跑…</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- 下注区 --}}
|
||
<div x-show="!myBet">
|
||
{{-- 快捷金额 --}}
|
||
<div style="display:flex; gap:6px; margin-bottom:8px;">
|
||
<template x-for="preset in [100, 500, 1000, 5000, 10000]" :key="preset">
|
||
<button @click="betAmount = preset"
|
||
style="flex:1; border:1px solid #b0d0ee; border-radius:6px; padding:8px 0;
|
||
font-size:13px; font-weight:bold; cursor:pointer; transition:all .15s;"
|
||
:style="betAmount === preset ?
|
||
'background:linear-gradient(135deg,#336699,#5a8fc0); color:#fff; border-color:#2a5580; box-shadow:0 3px 8px rgba(51,102,153,.3);' :
|
||
'background:#f6faff; color:#336699;'"
|
||
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:1px solid #d0e4f5; border-radius:8px;
|
||
padding:10px 14px; color:#333; font-size:13px; box-sizing:border-box; margin-bottom:12px; outline:none; transition:all .15s;"
|
||
onfocus="this.style.borderColor='#336699'; this.style.background='#fff'; this.style.boxShadow='0 0 0 2px rgba(51,102,153,.1)';"
|
||
onblur="this.style.borderColor='#d0e4f5'; this.style.background='#f6faff'; this.style.boxShadow='none';">
|
||
|
||
{{-- 下注按钮 --}}
|
||
<button @click="submitBet()" :disabled="!selectedHorse || betAmount < 100 || submitting"
|
||
style="display:block; width:100%; border:none; border-radius:12px; padding:12px 0;
|
||
font-size:14px; font-weight:bold; cursor:pointer; transition:all .2s; box-shadow:0 4px 12px rgba(51,102,153,.2);"
|
||
:style="(!selectedHorse || betAmount < 100 || submitting) ?
|
||
'background:#e0e8f0; color:#99a8b8; cursor:not-allowed; box-shadow:none;' :
|
||
'background:linear-gradient(135deg,#336699,#5a8fc0); color:#fff;'">
|
||
<span
|
||
x-text="submitting ? '提交中…' : (!selectedHorse ? '请先选择马匹' : '🐎 确认押注「' + myBetHorsePreviewName + '」 ' + Number(betAmount).toLocaleString() + ' 金币')"></span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ── 跑马阶段 ── --}}
|
||
<div x-show="phase === 'running'" style="display:none;">
|
||
<div
|
||
style="margin-bottom:8px; color:#336699; font-size:11px; font-weight:bold; text-align:center; background:#e8f0f8; border-radius:4px; padding:4px;">
|
||
🏁 赛道实况
|
||
</div>
|
||
<div style="display:flex; flex-direction:column; gap:8px;">
|
||
<template x-for="horse in horses" :key="horse.id">
|
||
<div style="display:flex; align-items:center; gap:8px;">
|
||
<div style="width:30px; text-align:center; font-size:18px;" x-text="horse.emoji"></div>
|
||
<div style="flex:1; position:relative;">
|
||
{{-- 赛道背景 --}}
|
||
<div
|
||
style="height:24px; background:#e8f0f8; border-radius:10px; overflow:hidden; position:relative;">
|
||
{{-- 进度条 --}}
|
||
<div style="height:100%; border-radius:20px; transition:width .9s ease-out;"
|
||
:style="'width:' + (positions[horse.id] || 0) + '%; background:' +
|
||
(leaderId === horse.id ? '#336699' :
|
||
'#b8d0e8')">
|
||
</div>
|
||
{{-- 马匹图标(跟随进度) --}}
|
||
<div style="position:absolute; top:50%; transform:translateY(-50%); font-size:16px; transition:left .9s ease-out; pointer-events:none;"
|
||
:style="'left:' + Math.max(0, (positions[horse.id] || 0) - 5) + '%'">
|
||
<span x-text="horse.emoji"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{{-- 进度数字 --}}
|
||
<div style="width:38px; text-align:right; color:#336699; font-size:10px; font-weight:bold;"
|
||
x-text="(positions[horse.id] || 0) + '%'"></div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<div style="margin-top:10px; text-align:center; color:#aaa; font-size:10px; text-align:center;">
|
||
🏁 终点线
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ── 结算阶段(蓝白风格)── --}}
|
||
<div x-show="phase === 'settled'" style="display:none;">
|
||
{{-- 获胜马匹 --}}
|
||
<div
|
||
style="text-align:center; padding:12px 0 10px; border-bottom:1px solid #d0e4f5; margin-bottom:10px;">
|
||
<div style="font-size:36px; margin-bottom:4px;" x-text="winnerEmoji"></div>
|
||
<div style="color:#336699; font-size:17px; font-weight:bold;"
|
||
x-text="'🏆 ' + winnerName + ' 夺冠!'">
|
||
</div>
|
||
<div style="color:#888; font-size:11px; margin-top:3px;"
|
||
x-text="'注池总额:' + Number(totalPool).toLocaleString() + ' 金币'"></div>
|
||
</div>
|
||
|
||
{{-- 个人结果 --}}
|
||
<div x-show="myBet">
|
||
{{-- 中奖 --}}
|
||
<div x-show="myWon"
|
||
style="border-radius:6px; padding:12px 14px; text-align:center; margin-bottom:4px;
|
||
background:#e8fde8; border:1px solid #a3e6b0;">
|
||
<div style="font-size:24px; margin-bottom:4px;">🎉</div>
|
||
<div style="color:#16a34a; font-size:16px; font-weight:bold;">恭喜中奖!</div>
|
||
<div style="color:#15803d; font-size:18px; font-weight:bold; margin:4px 0;"
|
||
x-text="'+' + Number(myPayout).toLocaleString() + ' 💰'"></div>
|
||
<div style="color:#888; font-size:10px;"
|
||
x-text="'押「' + myBetHorseName + '」' + Number(myBetAmount).toLocaleString() + ' 金币 → 赢得 ' + Number(myPayout).toLocaleString() + ' 金币'">
|
||
</div>
|
||
</div>
|
||
{{-- 未中奖 --}}
|
||
<div x-show="!myWon"
|
||
style="border-radius:6px; padding:12px 14px; text-align:center;
|
||
background:#fff0f0; border:1px solid #fca5a5;">
|
||
<div style="font-size:20px; margin-bottom:4px;">😔</div>
|
||
<div style="color:#dc2626; font-size:13px; font-weight:bold; margin-bottom:4px;">本场未中奖
|
||
</div>
|
||
<div style="color:#888; font-size:11px;"
|
||
x-text="'押了「' + myBetHorseName + '」' + Number(myBetAmount).toLocaleString() + ' 金币,冠军是「' + winnerName + '」'">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div x-show="!myBet" style="text-align:center; color:#aaa; font-size:12px; padding:8px 0;">
|
||
本场未参与下注
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ─── 底部关闭 ─── --}}
|
||
<div
|
||
style="background:#fff; border-top:1px solid #d0e4f5; padding:14px 16px; display:flex; justify-content:center;">
|
||
<button @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;"
|
||
onmouseover="this.style.background='#ddeeff'" onmouseout="this.style.background='#f0f6ff'">
|
||
关闭
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
|
||
<style>
|
||
@keyframes horse-run {
|
||
0% {
|
||
transform: translateX(-2px);
|
||
}
|
||
|
||
100% {
|
||
transform: translateX(2px);
|
||
}
|
||
}
|
||
|
||
@keyframes pulse-horse {
|
||
|
||
0%,
|
||
100% {
|
||
box-shadow: 0 4px 20px rgba(245, 158, 11, .5);
|
||
}
|
||
|
||
50% {
|
||
box-shadow: 0 4px 32px rgba(245, 158, 11, .9);
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
/**
|
||
* 赛马竞猜悬浮按钮 Alpine 组件(拖动 + localStorage 位置持久化)
|
||
*/
|
||
function horseRaceFab() {
|
||
const STORAGE_KEY = 'horse_race_fab_pos';
|
||
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null');
|
||
return {
|
||
visible: false,
|
||
posX: saved?.x ?? 80,
|
||
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;
|
||
// right 定位:往右拖 dx>0 → right 减小;bottom 定位:往下拖 dy>0 → bottom 减小
|
||
this.posX = Math.max(4, Math.min(window.innerWidth - 132, this._origX - dx));
|
||
this.posY = Math.max(4, Math.min(window.innerHeight - 132, 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('horse-race-panel');
|
||
if (panel) Alpine.$data(panel).openFromHall();
|
||
},
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 赛马竞猜主面板 Alpine 组件
|
||
*/
|
||
function horseRacePanel() {
|
||
return {
|
||
show: false,
|
||
phase: 'betting', // betting | running | settled
|
||
|
||
raceId: null,
|
||
totalSeconds: 90,
|
||
countdown: 90,
|
||
countdownTimer: null,
|
||
|
||
// 马匹列表(含实时赔率)
|
||
horses: [],
|
||
positions: {}, // 跑马进度 {horse_id: 0~100}
|
||
leaderId: null,
|
||
|
||
// 注池
|
||
totalPool: 0,
|
||
|
||
// 本人下注
|
||
myBet: false,
|
||
myBetHorseId: null,
|
||
myBetHorseName: '',
|
||
myBetAmount: 0,
|
||
|
||
// 下注表单
|
||
selectedHorse: null,
|
||
betAmount: 100,
|
||
submitting: false,
|
||
|
||
// 结算结果
|
||
winnerName: '',
|
||
winnerEmoji: '',
|
||
myWon: false,
|
||
myPayout: 0,
|
||
|
||
// 历史记录
|
||
history: [],
|
||
|
||
/**
|
||
* 获取当前选中马匹的预览名称(用于按钮文字)
|
||
*/
|
||
get myBetHorsePreviewName() {
|
||
if (!this.selectedHorse) return '';
|
||
const h = this.horses.find(h => h.id === this.selectedHorse);
|
||
return h ? h.emoji + h.name : '';
|
||
},
|
||
|
||
/**
|
||
* 开赛:填充场次数据并开始倒计时
|
||
*/
|
||
openRace(data) {
|
||
this.phase = 'betting';
|
||
this.raceId = data.race_id;
|
||
this.countdown = data.bet_seconds || 90;
|
||
this.totalSeconds = this.countdown;
|
||
this.horses = data.horses || [];
|
||
this.myBet = false;
|
||
this.myBetHorseId = null;
|
||
this.myBetHorseName = '';
|
||
this.myBetAmount = 0;
|
||
this.selectedHorse = null;
|
||
this.betAmount = 100;
|
||
this.positions = {};
|
||
this.leaderId = null;
|
||
this.show = true;
|
||
|
||
this.loadCurrentRace();
|
||
this.startCountdown();
|
||
this.updateFab(true);
|
||
},
|
||
|
||
/**
|
||
* 从接口获取当前场次状态(我的下注、注池赔率)
|
||
*/
|
||
async loadCurrentRace() {
|
||
try {
|
||
const res = await fetch('/horse-race/current');
|
||
const data = await res.json();
|
||
if (data.race) {
|
||
this.horses = data.race.horses || this.horses;
|
||
this.totalPool = data.race.total_pool || 0;
|
||
if (data.race.my_bet) {
|
||
this.myBet = true;
|
||
this.myBetHorseId = data.race.my_bet.horse_id;
|
||
this.myBetAmount = data.race.my_bet.amount;
|
||
const h = this.horses.find(h => h.id === this.myBetHorseId);
|
||
this.myBetHorseName = h ? h.emoji + h.name : '';
|
||
}
|
||
}
|
||
} catch {}
|
||
},
|
||
|
||
/**
|
||
* 启动倒计时
|
||
*/
|
||
startCountdown() {
|
||
clearInterval(this.countdownTimer);
|
||
this.countdownTimer = setInterval(() => {
|
||
this.countdown--;
|
||
if (this.countdown <= 0) {
|
||
clearInterval(this.countdownTimer);
|
||
this.phase = 'running';
|
||
}
|
||
}, 1000);
|
||
},
|
||
|
||
/**
|
||
* 提交下注
|
||
*/
|
||
async submitBet() {
|
||
if (!this.selectedHorse || this.betAmount < 100 || this.submitting) return;
|
||
this.submitting = true;
|
||
|
||
try {
|
||
const res = await fetch('/horse-race/bet', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]')?.content,
|
||
},
|
||
body: JSON.stringify({
|
||
race_id: this.raceId,
|
||
horse_id: this.selectedHorse,
|
||
amount: this.betAmount,
|
||
}),
|
||
});
|
||
const data = await res.json();
|
||
|
||
if (data.ok) {
|
||
this.myBet = true;
|
||
this.myBetHorseId = data.horse_id;
|
||
this.myBetAmount = data.amount;
|
||
const h = this.horses.find(h => h.id === data.horse_id);
|
||
this.myBetHorseName = h ? h.emoji + h.name : '';
|
||
window.chatDialog?.alert(data.message, '下注成功', '#f59e0b');
|
||
} else {
|
||
window.chatDialog?.alert(data.message || '下注失败', '提示', '#ef4444');
|
||
}
|
||
} catch {
|
||
window.chatDialog?.alert('网络异常,请稍后重试。', '错误', '#ef4444');
|
||
}
|
||
|
||
this.submitting = false;
|
||
},
|
||
|
||
/**
|
||
* 接收跑马进度更新
|
||
*/
|
||
updateProgress(data) {
|
||
this.phase = 'running';
|
||
this.positions = data.positions || {};
|
||
this.leaderId = data.leader_id;
|
||
},
|
||
|
||
/**
|
||
* 显示结算结果
|
||
*/
|
||
showResult(data) {
|
||
clearInterval(this.countdownTimer);
|
||
this.phase = 'settled';
|
||
this.show = true;
|
||
|
||
// 找出获胜马匹信息
|
||
const winner = this.horses.find(h => h.id === data.winner_horse_id);
|
||
this.winnerName = winner ? winner.emoji + winner.name : data.winner_name || '未知';
|
||
this.winnerEmoji = winner ? winner.emoji : '🐎';
|
||
|
||
// 判断本人是否中奖
|
||
if (this.myBet && this.myBetHorseId === data.winner_horse_id) {
|
||
this.myWon = true;
|
||
// 赔付前端显示估算(实际以后端为准,后端 WebSocket 无返回赔付金额)
|
||
this.myPayout = 0; // 无法前端计算,等用户看下一次余额或后端私信
|
||
} else {
|
||
this.myWon = false;
|
||
this.myPayout = 0;
|
||
}
|
||
|
||
this.updateFab(false);
|
||
this.loadHistory();
|
||
},
|
||
|
||
/**
|
||
* 加载历史记录
|
||
*/
|
||
async loadHistory() {
|
||
try {
|
||
const res = await fetch('/horse-race/history');
|
||
const data = await res.json();
|
||
this.history = (data.history || []).reverse();
|
||
} catch {}
|
||
},
|
||
|
||
/**
|
||
* 更新悬浮按钮显示状态
|
||
*/
|
||
updateFab(visible) {
|
||
const fab = document.getElementById('horse-race-fab');
|
||
if (fab) Alpine.$data(fab).visible = visible;
|
||
},
|
||
|
||
/**
|
||
* 关闭面板
|
||
*/
|
||
close() {
|
||
this.show = false;
|
||
if (this.phase === 'betting') {
|
||
this.updateFab(true);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 从游戏大厅入口打开面板:先重新请求当前场次最新状态,再显示面板。
|
||
* 解决游戏大厅展示‚押注中‚但面板状态降旧导致提交报错的问题。
|
||
*/
|
||
async openFromHall() {
|
||
try {
|
||
const res = await fetch('/horse-race/current');
|
||
const data = await res.json();
|
||
if (data.race) {
|
||
const race = data.race;
|
||
this.raceId = race.id;
|
||
this.horses = race.horses || [];
|
||
this.totalPool = race.total_pool || 0;
|
||
|
||
// 更新本人下注状态
|
||
if (race.my_bet) {
|
||
this.myBet = true;
|
||
this.myBetHorseId = race.my_bet.horse_id;
|
||
this.myBetAmount = race.my_bet.amount;
|
||
const h = this.horses.find(h => h.id === race.my_bet.horse_id);
|
||
this.myBetHorseName = h ? h.emoji + h.name : '';
|
||
} else {
|
||
this.myBet = false;
|
||
this.myBetHorseId = null;
|
||
this.myBetHorseName = '';
|
||
this.myBetAmount = 0;
|
||
}
|
||
|
||
// 同步阶段和倒计时
|
||
if (race.status === 'betting' && (race.seconds_left ?? 0) > 0) {
|
||
this.phase = 'betting';
|
||
this.countdown = race.seconds_left;
|
||
this.totalSeconds = race.seconds_left;
|
||
this.startCountdown();
|
||
} else if (race.status === 'running') {
|
||
this.phase = 'running';
|
||
} else {
|
||
this.phase = 'settled';
|
||
}
|
||
} else {
|
||
// 当前无进行中场次,重置状态
|
||
this.raceId = null;
|
||
this.horses = [];
|
||
this.phase = 'betting';
|
||
this.countdown = 0;
|
||
}
|
||
} catch (e) {
|
||
console.warn('[\u8d5b\u9a6c] openFromHall 失\u8d25', e);
|
||
}
|
||
this.show = true;
|
||
},
|
||
};
|
||
}
|
||
|
||
// ─── WebSocket 监听 ──────────────────────────────────────────────
|
||
|
||
/** 收到开赛事件:弹出押注面板 */
|
||
window.addEventListener('chat:horse.opened', (e) => {
|
||
const panel = document.getElementById('horse-race-panel');
|
||
if (panel) Alpine.$data(panel).openRace(e.detail);
|
||
});
|
||
|
||
/** 收到跑马进度事件:更新赛道 */
|
||
window.addEventListener('chat:horse.progress', (e) => {
|
||
const panel = document.getElementById('horse-race-panel');
|
||
if (panel) Alpine.$data(panel).updateProgress(e.detail);
|
||
});
|
||
|
||
/** 收到结算事件:展示结果 */
|
||
window.addEventListener('chat:horse.settled', (e) => {
|
||
const panel = document.getElementById('horse-race-panel');
|
||
if (panel) Alpine.$data(panel).showResult(e.detail);
|
||
});
|
||
|
||
/** 页面加载时恢复进行中的场次 */
|
||
document.addEventListener('DOMContentLoaded', async () => {
|
||
try {
|
||
const histRes = await fetch('/horse-race/history');
|
||
const histData = await histRes.json();
|
||
const panel = document.getElementById('horse-race-panel');
|
||
const fab = document.getElementById('horse-race-fab');
|
||
|
||
if (panel) {
|
||
Alpine.$data(panel).history = (histData.history || []).reverse();
|
||
}
|
||
|
||
const curRes = await fetch('/horse-race/current');
|
||
const curData = await curRes.json();
|
||
|
||
// 游戏可访问则常驻显示 FAB(与占卜一致)
|
||
if (fab) Alpine.$data(fab).visible = true;
|
||
|
||
if (curData.race && panel) {
|
||
const race = curData.race;
|
||
const seconds = race.seconds_left || 0;
|
||
const panelData = Alpine.$data(panel);
|
||
|
||
panelData.raceId = race.id;
|
||
panelData.horses = race.horses || [];
|
||
panelData.totalPool = race.total_pool || 0;
|
||
|
||
if (race.my_bet) {
|
||
panelData.myBet = true;
|
||
panelData.myBetHorseId = race.my_bet.horse_id;
|
||
panelData.myBetAmount = race.my_bet.amount;
|
||
const h = panelData.horses.find(h => h.id === race.my_bet.horse_id);
|
||
panelData.myBetHorseName = h ? h.emoji + h.name : '';
|
||
}
|
||
|
||
if (race.status === 'betting' && seconds > 0) {
|
||
panelData.phase = 'betting';
|
||
panelData.countdown = seconds;
|
||
} else if (race.status === 'running') {
|
||
panelData.phase = 'running';
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.warn('[赛马] 初始化失败', e);
|
||
}
|
||
});
|
||
</script>
|