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

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

678 lines
32 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 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">&times;</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>