迁移赛马主面板脚本

This commit is contained in:
2026-04-25 18:33:08 +08:00
parent 0953e03b73
commit 1f1c329085
3 changed files with 568 additions and 402 deletions
@@ -20,7 +20,13 @@
</style>
{{-- ─── 赛马主面板 ─── --}}
<div id="horse-race-panel" x-data="horseRacePanel()" x-show="show" x-cloak>
<div id="horse-race-panel"
x-data="horseRacePanel()"
x-show="show"
x-cloak
data-horse-race-current-url="{{ route('horse-race.current') }}"
data-horse-race-bet-url="{{ route('horse-race.bet') }}"
data-horse-race-history-url="{{ route('horse-race.history') }}">
<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"
@@ -327,405 +333,5 @@
}
</style>
<script>
{{-- 赛马竞猜悬浮按钮脚本已迁移到 resources/js/chat-room/horse-race-fab.js --}}
/**
* 赛马竞猜主面板 Alpine 组件
*/
function horseRacePanel() {
return {
show: false,
phase: 'idle', // idle | betting | running | settled
raceId: null,
totalSeconds: 90,
countdown: 90,
countdownTimer: null,
settledCountdown: 10,
settledTimer: null,
// 马匹列表(含实时赔率)
horses: [],
positions: {}, // 跑马进度 {horse_id: 0~100}
leaderId: null,
// 注池
totalPool: 0,
// 本人下注
myBet: false,
myBetHorseId: null,
myBetHorseName: '',
myBetAmount: 0,
// 下注表单
selectedHorse: null,
betAmount: 100,
minBet: 100,
maxBet: 100000,
submitting: false,
// 结算结果
winnerName: '',
winnerEmoji: '',
myWon: false,
myPayout: 0,
// 历史记录
history: [],
/**
* 读取赛马接口 JSON;若后端返回了 HTML/警告页,则抛出可诊断错误。
*
* @param {string} url 请求地址
* @param {RequestInit} options fetch 选项
* @returns {Promise<any>}
*/
async requestJson(url, options = {}) {
const requestUrl = new URL(url, window.location.origin);
requestUrl.searchParams.set('_ts', Date.now().toString());
const response = await fetch(requestUrl.toString(), {
cache: 'no-store',
headers: {
'Accept': 'application/json',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
...(options.headers || {}),
},
...options,
});
const rawText = await response.text();
try {
return JSON.parse(rawText);
} catch (error) {
const preview = rawText.slice(0, 160).replace(/\s+/g, ' ').trim();
throw new Error(`赛马接口未返回 JSON${response.status}: ${preview}`);
}
},
/**
* 同步全局聊天上下文中的金币余额,供弹窗右上角与其他面板共用。
*/
syncUserGold(jjb) {
if (jjb === undefined || jjb === null) return;
if (!window.chatContext) return;
window.chatContext.userJjb = Number(jjb);
window.chatContext.myGold = Number(jjb);
},
/**
* 获取当前选中马匹的预览名称(用于按钮文字)
*/
get myBetHorsePreviewName() {
if (!this.selectedHorse) return '';
const h = this.horses.find(h => h.id === this.selectedHorse);
return h ? h.emoji + h.name : '';
},
/**
* 获取快捷下注金额数组
*/
get quickBetAmounts() {
const min = this.minBet || 100;
const max = this.maxBet || 100000;
// 预设候选倍数,尽量生成美观的数字
const candidates = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
let steps = candidates.map(m => min * m).filter(v => v >= min && v < max);
steps.push(max);
steps = [...new Set(steps)].sort((a, b) => a - b);
if (steps.length >= 5) {
// 如果候选值足够多,均匀采样,确保首尾是最小值和最大值
return [
steps[0],
steps[Math.floor((steps.length - 1) * 0.25)],
steps[Math.floor((steps.length - 1) * 0.5)],
steps[Math.floor((steps.length - 1) * 0.75)],
steps[steps.length - 1]
];
} else {
// 如果候选值不足5个(范围太小),通过线性插值补齐
while (steps.length < 5) {
let maxGap = 0;
let insertIdx = -1;
for (let i = 0; i < steps.length - 1; i++) {
if (steps[i+1] - steps[i] > maxGap) {
maxGap = steps[i+1] - steps[i];
insertIdx = i;
}
}
if (insertIdx === -1) break;
let newVal = Math.floor((steps[insertIdx] + steps[insertIdx+1]) / 2);
if (newVal > 100) newVal = Math.floor(newVal / 10) * 10;
steps.splice(insertIdx + 1, 0, newVal);
}
return steps;
}
},
/**
* 开赛:填充场次数据并开始倒计时
*/
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.totalPool = data.total_pool || 0;
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 data = await this.requestJson('/horse-race/current');
// 每次打开或刷新当前场次时,都先同步右上角金币余额。
this.syncUserGold(data.jjb);
if (data.race) {
this.horses = data.race.horses || this.horses;
this.totalPool = data.race.total_pool || 0;
this.minBet = data.race.min_bet || 100;
this.maxBet = data.race.max_bet || 100000;
if (data.race.my_bet) {
this.myBet = true;
this.myBetHorseId = data.race.my_bet.horse_id;
this.myBetAmount = data.race.my_bet.amount;
this.selectedHorse = data.race.my_bet.horse_id;
const h = this.horses.find(h => h.id === this.myBetHorseId);
this.myBetHorseName = h ? h.emoji + h.name : '';
}
} else {
this.phase = 'idle';
this.raceId = null;
this.horses = [];
this.totalPool = 0;
this.countdown = 0;
}
} 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.selectedHorse = 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 : '';
await this.loadCurrentRace();
} else {
window.chatDialog?.alert(data.message || '下注失败', '提示', '#ef4444');
}
} catch {
window.chatDialog?.alert('网络异常,请稍后重试。', '错误', '#ef4444');
}
this.submitting = false;
},
/**
* 接收跑马进度更新
*/
updateProgress(data) {
this.phase = 'running';
// 确保响应式更新
this.positions = {
...this.positions,
...(data.positions || {})
};
this.leaderId = data.leader_id;
},
/**
* 显示结算结果
*/
showResult(data) {
clearInterval(this.countdownTimer);
clearInterval(this.settledTimer);
this.phase = 'settled';
this.show = true;
this.totalPool = data.total_pool || this.totalPool;
// 找出获胜马匹信息
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;
// 结算广播已携带冠军注池与可派奖池,这里按后端同公式还原个人赔付展示值。
const winnerPool = Number(data.winner_pool || 0);
const distributablePool = Number(data.distributable_pool || 0);
this.myPayout = winnerPool > 0
? Math.round(distributablePool * (Number(this.myBetAmount || 0) / winnerPool))
: 0;
} else {
this.myWon = false;
this.myPayout = 0;
}
this.updateFab(false);
this.loadHistory();
// 10秒倒计时自动关闭结果弹窗
this.settledCountdown = 10;
this.settledTimer = setInterval(() => {
this.settledCountdown--;
if (this.settledCountdown <= 0) {
clearInterval(this.settledTimer);
this.close();
}
}, 1000);
},
/**
* 加载历史记录
*/
async loadHistory() {
try {
const data = await this.requestJson('/horse-race/history');
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;
clearInterval(this.settledTimer);
if (this.phase === 'betting') {
this.updateFab(true);
}
},
/**
* 从游戏大厅入口打开面板:先重新请求当前场次最新状态,再显示面板。
* 解决游戏大厅展示‚押注中‚但面板状态降旧导致提交报错的问题。
*/
async openFromHall() {
try {
const data = await this.requestJson('/horse-race/current');
this.syncUserGold(data.jjb);
if (data.race) {
const race = data.race;
this.raceId = race.id;
this.horses = race.horses || [];
this.totalPool = race.total_pool || 0;
this.minBet = race.min_bet || 100;
this.maxBet = race.max_bet || 100000;
// 更新本人下注状态
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 {
// 当前无进行中场次,重置状态
clearInterval(this.countdownTimer);
this.raceId = null;
this.horses = [];
this.totalPool = 0;
this.myBet = false;
this.myBetHorseId = null;
this.myBetHorseName = '';
this.myBetAmount = 0;
this.selectedHorse = null;
this.phase = 'idle';
this.countdown = 0;
}
} catch (e) {
console.warn('[\u8d5b\u9a6c] openFromHall 失\u8d25', e);
}
this.show = true;
},
};
}
{{-- 赛马广播监听和页面恢复逻辑已迁移到 resources/js/chat-room/horse-race-events.js --}}
</script>
{{-- 赛马竞猜主面板脚本已迁移到 resources/js/chat-room/horse-race-panel.js --}}