迁移赛马主面板脚本
This commit is contained in:
@@ -32,6 +32,7 @@
|
|||||||
* - game-bootstrap.js:提供非关键游戏延迟初始化工具。
|
* - game-bootstrap.js:提供非关键游戏延迟初始化工具。
|
||||||
* - game-panels.js:处理通用游戏面板关闭事件。
|
* - game-panels.js:处理通用游戏面板关闭事件。
|
||||||
* - gomoku-controls.js:处理五子棋外部打开和接受邀请入口。
|
* - gomoku-controls.js:处理五子棋外部打开和接受邀请入口。
|
||||||
|
* - horse-race-panel.js:提供赛马竞猜主面板 Alpine 组件和下注流程。
|
||||||
* - horse-race-fab.js:处理赛马竞猜悬浮按钮拖动与打开面板。
|
* - horse-race-fab.js:处理赛马竞猜悬浮按钮拖动与打开面板。
|
||||||
* - horse-race-events.js:处理赛马广播事件和页面恢复当前场次。
|
* - horse-race-events.js:处理赛马广播事件和页面恢复当前场次。
|
||||||
* - holiday-modal.js:处理节日福利弹窗、广播监听、领取状态和系统消息入口。
|
* - holiday-modal.js:处理节日福利弹窗、广播监听、领取状态和系统消息入口。
|
||||||
@@ -116,6 +117,7 @@ export { bindGameHallControls, closeGameHall, openGameHall } from "./chat-room/g
|
|||||||
export { bindGameBootstrapControls, deferChatGameBootstrap } from "./chat-room/game-bootstrap.js";
|
export { bindGameBootstrapControls, deferChatGameBootstrap } from "./chat-room/game-bootstrap.js";
|
||||||
export { bindGamePanelControls } from "./chat-room/game-panels.js";
|
export { bindGamePanelControls } from "./chat-room/game-panels.js";
|
||||||
export { acceptGomokuInvite, bindGomokuControls, openGomokuPanel } from "./chat-room/gomoku-controls.js";
|
export { acceptGomokuInvite, bindGomokuControls, openGomokuPanel } from "./chat-room/gomoku-controls.js";
|
||||||
|
export { bindHorseRacePanelControls, horseRacePanel, requestHorseRaceJson } from "./chat-room/horse-race-panel.js";
|
||||||
export { bindHorseRaceFabControls, horseRaceFab } from "./chat-room/horse-race-fab.js";
|
export { bindHorseRaceFabControls, horseRaceFab } from "./chat-room/horse-race-fab.js";
|
||||||
export { bindHorseRaceEvents } from "./chat-room/horse-race-events.js";
|
export { bindHorseRaceEvents } from "./chat-room/horse-race-events.js";
|
||||||
export {
|
export {
|
||||||
@@ -284,6 +286,7 @@ import { bindGameHallControls, closeGameHall, openGameHall } from "./chat-room/g
|
|||||||
import { bindGameBootstrapControls, deferChatGameBootstrap } from "./chat-room/game-bootstrap.js";
|
import { bindGameBootstrapControls, deferChatGameBootstrap } from "./chat-room/game-bootstrap.js";
|
||||||
import { bindGamePanelControls } from "./chat-room/game-panels.js";
|
import { bindGamePanelControls } from "./chat-room/game-panels.js";
|
||||||
import { acceptGomokuInvite, bindGomokuControls, openGomokuPanel } from "./chat-room/gomoku-controls.js";
|
import { acceptGomokuInvite, bindGomokuControls, openGomokuPanel } from "./chat-room/gomoku-controls.js";
|
||||||
|
import { bindHorseRacePanelControls, horseRacePanel, requestHorseRaceJson } from "./chat-room/horse-race-panel.js";
|
||||||
import { bindHorseRaceFabControls, horseRaceFab } from "./chat-room/horse-race-fab.js";
|
import { bindHorseRaceFabControls, horseRaceFab } from "./chat-room/horse-race-fab.js";
|
||||||
import { bindHorseRaceEvents } from "./chat-room/horse-race-events.js";
|
import { bindHorseRaceEvents } from "./chat-room/horse-race-events.js";
|
||||||
import {
|
import {
|
||||||
@@ -462,6 +465,9 @@ if (typeof window !== "undefined") {
|
|||||||
acceptGomokuInvite,
|
acceptGomokuInvite,
|
||||||
bindGomokuControls,
|
bindGomokuControls,
|
||||||
openGomokuPanel,
|
openGomokuPanel,
|
||||||
|
bindHorseRacePanelControls,
|
||||||
|
horseRacePanel,
|
||||||
|
requestHorseRaceJson,
|
||||||
bindHorseRaceFabControls,
|
bindHorseRaceFabControls,
|
||||||
horseRaceFab,
|
horseRaceFab,
|
||||||
bindHorseRaceEvents,
|
bindHorseRaceEvents,
|
||||||
@@ -641,6 +647,7 @@ if (typeof window !== "undefined") {
|
|||||||
window.openGameHall = openGameHall;
|
window.openGameHall = openGameHall;
|
||||||
window.acceptGomokuInvite = acceptGomokuInvite;
|
window.acceptGomokuInvite = acceptGomokuInvite;
|
||||||
window.openGomokuPanel = openGomokuPanel;
|
window.openGomokuPanel = openGomokuPanel;
|
||||||
|
window.horseRacePanel = horseRacePanel;
|
||||||
window.horseRaceFab = horseRaceFab;
|
window.horseRaceFab = horseRaceFab;
|
||||||
window.openLotteryPanel = openLotteryPanel;
|
window.openLotteryPanel = openLotteryPanel;
|
||||||
window.openBankModal = openBankModal;
|
window.openBankModal = openBankModal;
|
||||||
@@ -729,6 +736,7 @@ if (typeof window !== "undefined") {
|
|||||||
bindGameBootstrapControls();
|
bindGameBootstrapControls();
|
||||||
bindGamePanelControls();
|
bindGamePanelControls();
|
||||||
bindGomokuControls();
|
bindGomokuControls();
|
||||||
|
bindHorseRacePanelControls();
|
||||||
bindHorseRaceFabControls();
|
bindHorseRaceFabControls();
|
||||||
bindHorseRaceEvents();
|
bindHorseRaceEvents();
|
||||||
bindHolidayModalControls();
|
bindHolidayModalControls();
|
||||||
|
|||||||
@@ -0,0 +1,552 @@
|
|||||||
|
// 赛马竞猜主面板 Alpine 组件,负责当前场次、下注、跑马进度和结算展示。
|
||||||
|
|
||||||
|
const DEFAULT_HORSE_RACE_CURRENT_URL = "/horse-race/current";
|
||||||
|
const DEFAULT_HORSE_RACE_BET_URL = "/horse-race/bet";
|
||||||
|
const DEFAULT_HORSE_RACE_HISTORY_URL = "/horse-race/history";
|
||||||
|
const DEFAULT_HORSE_RACE_MIN_BET = 100;
|
||||||
|
const DEFAULT_HORSE_RACE_MAX_BET = 100000;
|
||||||
|
const DEFAULT_HORSE_RACE_TOTAL_SECONDS = 90;
|
||||||
|
const SETTLED_AUTO_CLOSE_SECONDS = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取赛马接口地址,优先使用 Blade 写入的命名路由。
|
||||||
|
*
|
||||||
|
* @returns {{current: string, bet: string, history: string}}
|
||||||
|
*/
|
||||||
|
function horseRaceUrls() {
|
||||||
|
const panel = document.getElementById("horse-race-panel");
|
||||||
|
|
||||||
|
return {
|
||||||
|
current: panel?.dataset.horseRaceCurrentUrl || DEFAULT_HORSE_RACE_CURRENT_URL,
|
||||||
|
bet: panel?.dataset.horseRaceBetUrl || DEFAULT_HORSE_RACE_BET_URL,
|
||||||
|
history: panel?.dataset.horseRaceHistoryUrl || DEFAULT_HORSE_RACE_HISTORY_URL,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取 CSRF Token。
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function csrfToken() {
|
||||||
|
return document.querySelector('meta[name="csrf-token"]')?.content ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成快捷下注金额,保持固定 5 档按钮稳定渲染。
|
||||||
|
*
|
||||||
|
* @param {number} min 最小下注额
|
||||||
|
* @param {number} max 最大下注额
|
||||||
|
* @returns {number[]}
|
||||||
|
*/
|
||||||
|
function buildQuickBetAmounts(min, max) {
|
||||||
|
const candidates = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
|
||||||
|
const steps = candidates
|
||||||
|
.map((multiplier) => min * multiplier)
|
||||||
|
.filter((value) => value >= min && value < max);
|
||||||
|
let sortedSteps = [...new Set([...steps, max])].sort((a, b) => a - b);
|
||||||
|
|
||||||
|
if (sortedSteps.length >= 5) {
|
||||||
|
return [
|
||||||
|
sortedSteps[0],
|
||||||
|
sortedSteps[Math.floor((sortedSteps.length - 1) * 0.25)],
|
||||||
|
sortedSteps[Math.floor((sortedSteps.length - 1) * 0.5)],
|
||||||
|
sortedSteps[Math.floor((sortedSteps.length - 1) * 0.75)],
|
||||||
|
sortedSteps[sortedSteps.length - 1],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下注范围很小时补齐中间档位,避免按钮数量变化造成布局跳动。
|
||||||
|
while (sortedSteps.length < 5) {
|
||||||
|
let maxGap = 0;
|
||||||
|
let insertIndex = -1;
|
||||||
|
|
||||||
|
for (let index = 0; index < sortedSteps.length - 1; index += 1) {
|
||||||
|
if (sortedSteps[index + 1] - sortedSteps[index] > maxGap) {
|
||||||
|
maxGap = sortedSteps[index + 1] - sortedSteps[index];
|
||||||
|
insertIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (insertIndex === -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newValue = Math.floor((sortedSteps[insertIndex] + sortedSteps[insertIndex + 1]) / 2);
|
||||||
|
if (newValue > 100) {
|
||||||
|
newValue = Math.floor(newValue / 10) * 10;
|
||||||
|
}
|
||||||
|
sortedSteps.splice(insertIndex + 1, 0, newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortedSteps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取赛马接口 JSON;若后端返回 HTML/警告页,则抛出可诊断错误。
|
||||||
|
*
|
||||||
|
* @param {string} url 请求地址
|
||||||
|
* @param {RequestInit} options fetch 选项
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
export async function requestHorseRaceJson(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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建赛马竞猜主面板 Alpine 组件。
|
||||||
|
*
|
||||||
|
* @returns {Record<string, any>}
|
||||||
|
*/
|
||||||
|
export function horseRacePanel() {
|
||||||
|
return {
|
||||||
|
show: false,
|
||||||
|
phase: "idle",
|
||||||
|
raceId: null,
|
||||||
|
totalSeconds: DEFAULT_HORSE_RACE_TOTAL_SECONDS,
|
||||||
|
countdown: DEFAULT_HORSE_RACE_TOTAL_SECONDS,
|
||||||
|
countdownTimer: null,
|
||||||
|
settledCountdown: SETTLED_AUTO_CLOSE_SECONDS,
|
||||||
|
settledTimer: null,
|
||||||
|
horses: [],
|
||||||
|
positions: {},
|
||||||
|
leaderId: null,
|
||||||
|
totalPool: 0,
|
||||||
|
myBet: false,
|
||||||
|
myBetHorseId: null,
|
||||||
|
myBetHorseName: "",
|
||||||
|
myBetAmount: 0,
|
||||||
|
selectedHorse: null,
|
||||||
|
betAmount: DEFAULT_HORSE_RACE_MIN_BET,
|
||||||
|
minBet: DEFAULT_HORSE_RACE_MIN_BET,
|
||||||
|
maxBet: DEFAULT_HORSE_RACE_MAX_BET,
|
||||||
|
submitting: false,
|
||||||
|
winnerName: "",
|
||||||
|
winnerEmoji: "",
|
||||||
|
myWon: false,
|
||||||
|
myPayout: 0,
|
||||||
|
history: [],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兼容事件模块调用的 JSON 请求入口。
|
||||||
|
*
|
||||||
|
* @param {string} url 请求地址
|
||||||
|
* @param {RequestInit} options fetch 选项
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
requestJson(url, options = {}) {
|
||||||
|
return requestHorseRaceJson(url, options);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步全局聊天上下文中的金币余额,供弹窗右上角与其他面板共用。
|
||||||
|
*
|
||||||
|
* @param {number|string|null|undefined} jjb 最新金币余额
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
syncUserGold(jjb) {
|
||||||
|
if (jjb === undefined || jjb === null || !window.chatContext) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.chatContext.userJjb = Number(jjb);
|
||||||
|
window.chatContext.myGold = Number(jjb);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前选中马匹的预览名称。
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
get myBetHorsePreviewName() {
|
||||||
|
if (!this.selectedHorse) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const horse = this.horses.find((item) => item.id === this.selectedHorse);
|
||||||
|
return horse ? `${horse.emoji}${horse.name}` : "";
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取快捷下注金额数组。
|
||||||
|
*
|
||||||
|
* @returns {number[]}
|
||||||
|
*/
|
||||||
|
get quickBetAmounts() {
|
||||||
|
return buildQuickBetAmounts(this.minBet || DEFAULT_HORSE_RACE_MIN_BET, this.maxBet || DEFAULT_HORSE_RACE_MAX_BET);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开赛:填充场次数据并开始倒计时。
|
||||||
|
*
|
||||||
|
* @param {Record<string, any>} data 开赛广播数据
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
openRace(data) {
|
||||||
|
this.phase = "betting";
|
||||||
|
this.raceId = data.race_id;
|
||||||
|
this.countdown = data.bet_seconds || DEFAULT_HORSE_RACE_TOTAL_SECONDS;
|
||||||
|
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 = this.minBet || DEFAULT_HORSE_RACE_MIN_BET;
|
||||||
|
this.positions = {};
|
||||||
|
this.leaderId = null;
|
||||||
|
this.show = true;
|
||||||
|
|
||||||
|
this.loadCurrentRace();
|
||||||
|
this.startCountdown();
|
||||||
|
this.updateFab(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从接口获取当前场次状态,包括本人下注、注池和赔率。
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async loadCurrentRace() {
|
||||||
|
try {
|
||||||
|
const data = await this.requestJson(horseRaceUrls().current);
|
||||||
|
this.syncUserGold(data.jjb);
|
||||||
|
|
||||||
|
if (data.race) {
|
||||||
|
this.applyRaceState(data.race);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resetToIdleState();
|
||||||
|
} catch (error) {
|
||||||
|
// 当前场次刷新失败不弹窗,避免影响用户关闭或查看面板。
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动下注倒计时。
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
startCountdown() {
|
||||||
|
clearInterval(this.countdownTimer);
|
||||||
|
this.countdownTimer = setInterval(() => {
|
||||||
|
this.countdown -= 1;
|
||||||
|
if (this.countdown <= 0) {
|
||||||
|
clearInterval(this.countdownTimer);
|
||||||
|
this.countdownTimer = null;
|
||||||
|
this.phase = "running";
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交本场下注。
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async submitBet() {
|
||||||
|
if (!this.selectedHorse || this.betAmount < DEFAULT_HORSE_RACE_MIN_BET || this.submitting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submitting = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(horseRaceUrls().bet, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
"X-CSRF-TOKEN": csrfToken(),
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
race_id: this.raceId,
|
||||||
|
horse_id: this.selectedHorse,
|
||||||
|
amount: this.betAmount,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.ok) {
|
||||||
|
this.myBet = true;
|
||||||
|
this.myBetHorseId = data.horse_id;
|
||||||
|
this.selectedHorse = data.horse_id;
|
||||||
|
this.myBetAmount = data.amount;
|
||||||
|
this.setMyBetHorseName(data.horse_id);
|
||||||
|
await this.loadCurrentRace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.chatDialog?.alert(data.message || "下注失败", "提示", "#ef4444");
|
||||||
|
} catch (error) {
|
||||||
|
window.chatDialog?.alert("网络异常,请稍后重试。", "错误", "#ef4444");
|
||||||
|
} finally {
|
||||||
|
this.submitting = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收跑马进度更新。
|
||||||
|
*
|
||||||
|
* @param {Record<string, any>} data 进度广播数据
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
updateProgress(data) {
|
||||||
|
this.phase = "running";
|
||||||
|
this.positions = {
|
||||||
|
...this.positions,
|
||||||
|
...(data.positions || {}),
|
||||||
|
};
|
||||||
|
this.leaderId = data.leader_id;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示结算结果。
|
||||||
|
*
|
||||||
|
* @param {Record<string, any>} data 结算广播数据
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
showResult(data) {
|
||||||
|
clearInterval(this.countdownTimer);
|
||||||
|
clearInterval(this.settledTimer);
|
||||||
|
this.countdownTimer = null;
|
||||||
|
this.settledTimer = null;
|
||||||
|
this.phase = "settled";
|
||||||
|
this.show = true;
|
||||||
|
this.totalPool = data.total_pool || this.totalPool;
|
||||||
|
|
||||||
|
const winner = this.horses.find((horse) => horse.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;
|
||||||
|
this.myPayout = this.calculatePayout(data);
|
||||||
|
} else {
|
||||||
|
this.myWon = false;
|
||||||
|
this.myPayout = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateFab(false);
|
||||||
|
this.loadHistory();
|
||||||
|
this.startSettledCountdown();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载历史记录。
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async loadHistory() {
|
||||||
|
try {
|
||||||
|
const data = await this.requestJson(horseRaceUrls().history);
|
||||||
|
this.history = (data.history || []).reverse();
|
||||||
|
} catch (error) {
|
||||||
|
// 历史趋势失败不影响下注主流程。
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新赛马悬浮按钮显示状态。
|
||||||
|
*
|
||||||
|
* @param {boolean} visible 是否显示
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
updateFab(visible) {
|
||||||
|
const fab = document.getElementById("horse-race-fab");
|
||||||
|
|
||||||
|
if (fab && typeof window.Alpine?.$data === "function") {
|
||||||
|
window.Alpine.$data(fab).visible = visible;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭面板;下注中保留悬浮按钮方便再次打开。
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
close() {
|
||||||
|
this.show = false;
|
||||||
|
clearInterval(this.settledTimer);
|
||||||
|
this.settledTimer = null;
|
||||||
|
|
||||||
|
if (this.phase === "betting") {
|
||||||
|
this.updateFab(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从游戏大厅入口打开面板,先同步当前场次最新状态。
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async openFromHall() {
|
||||||
|
try {
|
||||||
|
const data = await this.requestJson(horseRaceUrls().current);
|
||||||
|
this.syncUserGold(data.jjb);
|
||||||
|
|
||||||
|
if (data.race) {
|
||||||
|
this.applyRaceState(data.race);
|
||||||
|
this.syncPhaseFromRace(data.race);
|
||||||
|
} else {
|
||||||
|
this.resetToIdleState();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[赛马] openFromHall 失败", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.show = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 套用接口返回的场次基础状态。
|
||||||
|
*
|
||||||
|
* @param {Record<string, any>} race 当前场次
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
applyRaceState(race) {
|
||||||
|
this.raceId = race.id;
|
||||||
|
this.horses = race.horses || this.horses;
|
||||||
|
this.totalPool = race.total_pool || 0;
|
||||||
|
this.minBet = race.min_bet || DEFAULT_HORSE_RACE_MIN_BET;
|
||||||
|
this.maxBet = race.max_bet || DEFAULT_HORSE_RACE_MAX_BET;
|
||||||
|
|
||||||
|
if (race.my_bet) {
|
||||||
|
this.myBet = true;
|
||||||
|
this.myBetHorseId = race.my_bet.horse_id;
|
||||||
|
this.myBetAmount = race.my_bet.amount;
|
||||||
|
this.selectedHorse = race.my_bet.horse_id;
|
||||||
|
this.setMyBetHorseName(race.my_bet.horse_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.myBet = false;
|
||||||
|
this.myBetHorseId = null;
|
||||||
|
this.myBetHorseName = "";
|
||||||
|
this.myBetAmount = 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据接口状态同步面板阶段和倒计时。
|
||||||
|
*
|
||||||
|
* @param {Record<string, any>} race 当前场次
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
syncPhaseFromRace(race) {
|
||||||
|
if (race.status === "betting" && (race.seconds_left ?? 0) > 0) {
|
||||||
|
this.phase = "betting";
|
||||||
|
this.countdown = race.seconds_left;
|
||||||
|
this.totalSeconds = race.seconds_left;
|
||||||
|
this.startCountdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (race.status === "running") {
|
||||||
|
this.phase = "running";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.phase = "settled";
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置为没有进行中场次的空状态。
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
resetToIdleState() {
|
||||||
|
clearInterval(this.countdownTimer);
|
||||||
|
this.countdownTimer = null;
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据马匹 ID 回填本人下注马匹名称。
|
||||||
|
*
|
||||||
|
* @param {number|string} horseId 马匹 ID
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
setMyBetHorseName(horseId) {
|
||||||
|
const horse = this.horses.find((item) => item.id === horseId);
|
||||||
|
this.myBetHorseName = horse ? `${horse.emoji}${horse.name}` : "";
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按后端同公式还原个人赔付展示值。
|
||||||
|
*
|
||||||
|
* @param {Record<string, any>} data 结算广播数据
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
calculatePayout(data) {
|
||||||
|
const winnerPool = Number(data.winner_pool || 0);
|
||||||
|
const distributablePool = Number(data.distributable_pool || 0);
|
||||||
|
|
||||||
|
return winnerPool > 0
|
||||||
|
? Math.round(distributablePool * (Number(this.myBetAmount || 0) / winnerPool))
|
||||||
|
: 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动结算弹窗自动关闭倒计时。
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
startSettledCountdown() {
|
||||||
|
this.settledCountdown = SETTLED_AUTO_CLOSE_SECONDS;
|
||||||
|
clearInterval(this.settledTimer);
|
||||||
|
this.settledTimer = setInterval(() => {
|
||||||
|
this.settledCountdown -= 1;
|
||||||
|
if (this.settledCountdown <= 0) {
|
||||||
|
clearInterval(this.settledTimer);
|
||||||
|
this.settledTimer = null;
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 挂载赛马主面板全局组件名,兼容 Blade 的 x-data。
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function bindHorseRacePanelControls() {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.horseRacePanel = horseRacePanel;
|
||||||
|
}
|
||||||
@@ -20,7 +20,13 @@
|
|||||||
</style>
|
</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"
|
<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: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"
|
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
|
||||||
@@ -327,405 +333,5 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
|
||||||
{{-- 赛马竞猜悬浮按钮脚本已迁移到 resources/js/chat-room/horse-race-fab.js --}}
|
|
||||||
|
|
||||||
/**
|
{{-- 赛马竞猜主面板脚本已迁移到 resources/js/chat-room/horse-race-panel.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>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user