// 乐彩百家乐主面板 Alpine 组件,负责当前局状态、下注、开奖结果和历史趋势。 const DEFAULT_BACCARAT_CURRENT_URL = "/baccarat/current"; const DEFAULT_BACCARAT_BET_URL = "/baccarat/bet"; const DEFAULT_BACCARAT_HISTORY_URL = "/baccarat/history"; const DEFAULT_BACCARAT_MIN_BET = 100; const DEFAULT_BACCARAT_MAX_BET = 50000; const DEFAULT_BACCARAT_TOTAL_SECONDS = 60; const RESULT_LABELS = { big: "大", small: "小", triple: "豹子", }; /** * 读取百家乐接口地址,优先使用 Blade 写入的命名路由。 * * @returns {{current: string, bet: string, history: string}} */ function baccaratUrls() { const panel = document.getElementById("baccarat-panel"); return { current: panel?.dataset.baccaratCurrentUrl || DEFAULT_BACCARAT_CURRENT_URL, bet: panel?.dataset.baccaratBetUrl || DEFAULT_BACCARAT_BET_URL, history: panel?.dataset.baccaratHistoryUrl || DEFAULT_BACCARAT_HISTORY_URL, }; } /** * 读取 CSRF Token。 * * @returns {string} */ function csrfToken() { return document.querySelector('meta[name="csrf-token"]')?.content ?? ""; } /** * 生成快捷下注金额,保持最小值、最大值和中间档位可快速选择。 * * @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; } /** * 创建百家乐游戏面板 Alpine 组件。 * * @returns {Record} */ export function baccaratPanel() { return { show: false, phase: "idle", roundId: null, totalSeconds: DEFAULT_BACCARAT_TOTAL_SECONDS, countdown: DEFAULT_BACCARAT_TOTAL_SECONDS, countdownTimer: null, totalBetBig: 0, totalBetSmall: 0, totalBetTriple: 0, betCountBig: 0, betCountSmall: 0, betCountTriple: 0, myBet: false, myBetType: "", myBetAmount: 0, selectedType: "", betAmount: DEFAULT_BACCARAT_MIN_BET, minBet: DEFAULT_BACCARAT_MIN_BET, maxBet: DEFAULT_BACCARAT_MAX_BET, submitting: false, settledDice: [], settledTotal: 0, settledResult: "", resultLabel: "", diceEmoji: "", myWon: false, myPayout: 0, history: [], autoCloseTimer: null, autoCloseCountdown: 0, /** * 同步全局聊天上下文中的金币余额,供弹窗右上角与其他面板共用。 * * @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 {number[]} */ get quickBetAmounts() { return buildQuickBetAmounts(this.minBet || DEFAULT_BACCARAT_MIN_BET, this.maxBet || DEFAULT_BACCARAT_MAX_BET); }, /** * 从大厅或通知点击打开。 * * @returns {void} */ openFromHall() { this.show = true; this.loadCurrentRound(); }, /** * 重置为未开局状态。 * * @returns {void} */ setIdleState() { clearInterval(this.countdownTimer); clearInterval(this.autoCloseTimer); this.countdownTimer = null; this.autoCloseTimer = null; this.phase = "idle"; this.roundId = null; this.countdown = 0; this.autoCloseCountdown = 0; this.totalBetBig = 0; this.totalBetSmall = 0; this.totalBetTriple = 0; this.betCountBig = 0; this.betCountSmall = 0; this.betCountTriple = 0; this.myBet = false; this.myBetType = ""; this.myBetAmount = 0; this.selectedType = ""; this.settledDice = []; this.settledTotal = 0; this.settledResult = ""; this.resultLabel = ""; this.diceEmoji = ""; this.myWon = false; this.myPayout = 0; this.updateFab(false); }, /** * 开局:填充局次数据并开始倒计时。 * * @param {Record} data 广播局次数据 * @returns {void} */ openRound(data) { this.phase = "betting"; this.roundId = data.round_id; this.countdown = data.bet_seconds || DEFAULT_BACCARAT_TOTAL_SECONDS; this.totalSeconds = this.countdown; this.myBet = false; this.myBetType = ""; this.myBetAmount = 0; this.settledDice = []; this.selectedType = ""; this.betAmount = this.minBet || DEFAULT_BACCARAT_MIN_BET; this.betCountBig = 0; this.betCountSmall = 0; this.betCountTriple = 0; this.show = true; this.loadCurrentRound(); this.startCountdown(); this.updateFab(true); }, /** * 从接口获取当前局状态,包括我的下注和投注池。 * * @returns {Promise} */ async loadCurrentRound() { try { const response = await fetch(baccaratUrls().current); const data = await response.json(); this.syncUserGold(data.jjb); if (data.round && (data.round.seconds_left || 0) > 0) { this.phase = "betting"; this.roundId = data.round.id; this.countdown = data.round.seconds_left || this.countdown || 0; this.totalBetBig = data.round.total_bet_big; this.totalBetSmall = data.round.total_bet_small; this.totalBetTriple = data.round.total_bet_triple; this.betCountBig = data.round.bet_count_big; this.betCountSmall = data.round.bet_count_small; this.betCountTriple = data.round.bet_count_triple; this.minBet = data.round.min_bet || DEFAULT_BACCARAT_MIN_BET; this.maxBet = data.round.max_bet || DEFAULT_BACCARAT_MAX_BET; if (data.round.my_bet) { this.myBet = true; this.myBetType = data.round.my_bet.bet_type; this.myBetAmount = data.round.my_bet.amount; } else { this.myBet = false; this.myBetType = ""; this.myBetAmount = 0; } return; } this.setIdleState(); } catch (error) { this.setIdleState(); } }, /** * 启动下注倒计时。 * * @returns {void} */ startCountdown() { clearInterval(this.countdownTimer); this.countdownTimer = setInterval(() => { this.countdown -= 1; if (this.countdown <= 0) { clearInterval(this.countdownTimer); this.countdownTimer = null; this.phase = "waiting"; } }, 1000); }, /** * 提交本局下注。 * * @returns {Promise} */ async submitBet() { if (!this.roundId || !this.selectedType || this.betAmount < DEFAULT_BACCARAT_MIN_BET || this.submitting) { return; } this.submitting = true; try { const response = await fetch(baccaratUrls().bet, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", "X-CSRF-TOKEN": csrfToken(), }, body: JSON.stringify({ round_id: this.roundId, bet_type: this.selectedType, amount: this.betAmount, }), }); const data = await response.json(); if (response.ok && data.ok) { this.myBet = true; this.myBetType = data.bet_type; this.myBetAmount = data.amount; return; } if (response.status === 422 && data.errors) { const firstError = Object.values(data.errors)[0][0]; window.chatDialog?.alert(firstError, "下注验证失败", "#ef4444"); return; } window.chatDialog?.alert(data.message || "下注失败", "提示", "#ef4444"); } catch (error) { window.chatDialog?.alert("网络异常,请稍后重试。", "错误", "#ef4444"); } finally { this.submitting = false; } }, /** * 显示开奖结果动画,并按本人是否下注决定是否弹出结算面板。 * * @param {Record} data 开奖广播数据 * @returns {void} */ showResult(data) { clearInterval(this.countdownTimer); this.countdownTimer = null; this.settledDice = data.dice; this.settledTotal = data.total_points; this.settledResult = data.result; this.resultLabel = data.result_label; this.phase = "settled"; if (this.myBet) { this.show = true; } if (this.myBet && this.myBetType === data.result && data.result !== "kill") { this.myWon = true; this.myPayout = this.myBetAmount * (data.result === "triple" ? 25 : 2); } else { this.myWon = false; this.myPayout = 0; } this.updateFab(false); this.loadHistory(); this.startAutoCloseCountdown(); }, /** * 加载历史趋势。 * * @returns {Promise} */ async loadHistory() { try { const response = await fetch(baccaratUrls().history); const data = await response.json(); this.history = (data.history || []).reverse(); } catch (error) { // 历史趋势失败不阻塞下注主流程。 } }, /** * 更新悬浮按钮显示状态。 * * @param {boolean} visible 是否显示 * @returns {void} */ updateFab(visible) { const fab = document.getElementById("baccarat-fab"); if (fab && typeof window.Alpine?.$data === "function") { window.Alpine.$data(fab).visible = visible; } }, /** * 关闭面板;下注中保留悬浮按钮方便再次打开。 * * @returns {void} */ close() { clearInterval(this.autoCloseTimer); this.autoCloseTimer = null; this.autoCloseCountdown = 0; this.show = false; if (this.phase === "betting") { this.updateFab(true); } }, /** * 押注类型中文标签。 * * @param {string} type 押注类型 * @returns {string} */ betTypeLabel(type) { return RESULT_LABELS[type] || ""; }, /** * 启动结算后的自动关闭倒计时。 * * @returns {void} */ startAutoCloseCountdown() { this.autoCloseCountdown = 10; clearInterval(this.autoCloseTimer); this.autoCloseTimer = setInterval(() => { this.autoCloseCountdown -= 1; if (this.autoCloseCountdown <= 0) { clearInterval(this.autoCloseTimer); this.autoCloseTimer = null; this.close(); } }, 1000); }, }; } /** * 挂载百家乐主面板全局组件名,兼容 Blade 的 x-data。 * * @returns {void} */ export function bindBaccaratPanelControls() { if (typeof window === "undefined") { return; } window.baccaratPanel = baccaratPanel; }