Files
chatroom/resources/js/chat-room/baccarat-panel.js
T
2026-04-25 18:30:29 +08:00

449 lines
14 KiB
JavaScript

// 乐彩百家乐主面板 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<string, any>}
*/
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<string, any>} 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<void>}
*/
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<void>}
*/
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<string, any>} 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<void>}
*/
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;
}