From 0953e03b73d14f6943ef2c9902d0c882868c3d84 Mon Sep 17 00:00:00 2001 From: lkddi Date: Sat, 25 Apr 2026 18:30:29 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=81=E7=A7=BB=E7=99=BE=E5=AE=B6=E4=B9=90?= =?UTF-8?q?=E4=B8=BB=E9=9D=A2=E6=9D=BF=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/js/chat-room.js | 7 + resources/js/chat-room/baccarat-panel.js | 448 ++++++++++++++++++ .../partials/games/baccarat-panel.blade.php | 355 +------------- 3 files changed, 463 insertions(+), 347 deletions(-) create mode 100644 resources/js/chat-room/baccarat-panel.js diff --git a/resources/js/chat-room.js b/resources/js/chat-room.js index 241a686..5213e2a 100644 --- a/resources/js/chat-room.js +++ b/resources/js/chat-room.js @@ -23,6 +23,7 @@ * - user-target-actions.js:处理点击用户名切换私聊目标和打开名片。 * - welcome-menu.js:处理欢迎菜单交互。 * - admin-menu.js:处理聊天室管理菜单交互。 + * - baccarat-panel.js:提供百家乐主面板 Alpine 组件和下注流程。 * - baccarat-fab.js:处理百家乐悬浮按钮拖动与打开面板。 * - baccarat-events.js:处理百家乐广播事件和页面恢复当前局。 * - baccarat-loss-cover-admin.js:处理百家乐买单活动管理弹层。 @@ -93,6 +94,7 @@ export { bindToolbarControls, runFeatureShortcut, runToolbarAction } from "./cha export { bindUserTargetActions, openUserCard, switchTarget } from "./chat-room/user-target-actions.js"; export { bindWelcomeMenuControls } from "./chat-room/welcome-menu.js"; export { bindAdminMenuControls } from "./chat-room/admin-menu.js"; +export { baccaratPanel, bindBaccaratPanelControls } from "./chat-room/baccarat-panel.js"; export { baccaratFab, bindBaccaratFabControls } from "./chat-room/baccarat-fab.js"; export { bindBaccaratEvents } from "./chat-room/baccarat-events.js"; export { @@ -260,6 +262,7 @@ import { bindToolbarControls, runFeatureShortcut, runToolbarAction } from "./cha import { bindUserTargetActions, openUserCard, switchTarget } from "./chat-room/user-target-actions.js"; import { bindWelcomeMenuControls } from "./chat-room/welcome-menu.js"; import { bindAdminMenuControls } from "./chat-room/admin-menu.js"; +import { baccaratPanel, bindBaccaratPanelControls } from "./chat-room/baccarat-panel.js"; import { baccaratFab, bindBaccaratFabControls } from "./chat-room/baccarat-fab.js"; import { bindBaccaratEvents } from "./chat-room/baccarat-events.js"; import { @@ -437,6 +440,8 @@ if (typeof window !== "undefined") { switchTarget, bindWelcomeMenuControls, bindAdminMenuControls, + baccaratPanel, + bindBaccaratPanelControls, baccaratFab, bindBaccaratFabControls, bindBaccaratEvents, @@ -602,6 +607,7 @@ if (typeof window !== "undefined") { window.scheduleRenderMobileUserList = scheduleRenderMobileUserList; window.switchMobileTab = switchMobileTab; window.switchTarget = switchTarget; + window.baccaratPanel = baccaratPanel; window.baccaratFab = baccaratFab; window.clearChatBotContext = clearChatBotContext; window.sendToChatBot = sendToChatBot; @@ -714,6 +720,7 @@ if (typeof window !== "undefined") { bindToolbarControls(); bindUserTargetActions(); bindAdminMenuControls(); + bindBaccaratPanelControls(); bindBaccaratFabControls(); bindBaccaratEvents(); bindBaccaratLossCoverAdminControls(); diff --git a/resources/js/chat-room/baccarat-panel.js b/resources/js/chat-room/baccarat-panel.js new file mode 100644 index 0000000..40542a3 --- /dev/null +++ b/resources/js/chat-room/baccarat-panel.js @@ -0,0 +1,448 @@ +// 乐彩百家乐主面板 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; +} diff --git a/resources/views/chat/partials/games/baccarat-panel.blade.php b/resources/views/chat/partials/games/baccarat-panel.blade.php index 4c84d74..d4b1a02 100644 --- a/resources/views/chat/partials/games/baccarat-panel.blade.php +++ b/resources/views/chat/partials/games/baccarat-panel.blade.php @@ -9,7 +9,13 @@ --}} {{-- 百家乐主面板 --}} -
+
- +{{-- 乐彩百家乐主面板脚本已迁移到 resources/js/chat-room/baccarat-panel.js --}}