Files
chatroom/resources/js/admin/game-configs.js
T
2026-04-25 13:23:20 +08:00

478 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 游戏管理后台事件代理,逐步替代 game-configs Blade 内联脚本。
let adminGameConfigControlsBound = false;
const MYSTERY_BOX_META = {
normal: { name: "普通箱", icon: "📦" },
rare: { name: "稀有箱", icon: "💎" },
trap: { name: "黑化箱", icon: "☠️" },
};
/**
* 读取后台 layout 注入的 CSRF token。
*
* @returns {string}
*/
function getCsrfToken() {
return document.querySelector('meta[name="csrf-token"]')?.getAttribute("content") || "";
}
/**
* 生成单个统计卡片 HTML。
*
* @param {{icon:string,title:string,color:string,items:Array<{label:string,value:string|number}>}} card 统计卡片
* @returns {string}
*/
function renderStatsCard(card) {
return `
<div class="bg-white rounded-xl shadow-sm border ${card.color} p-4">
<div class="flex items-center gap-2 mb-3">
<span class="text-xl">${card.icon}</span>
<span class="font-bold text-gray-700 text-sm">${card.title}</span>
</div>
<div class="space-y-1.5">
${card.items.map((item) => `
<div class="flex justify-between text-xs">
<span class="text-gray-500">${item.label}</span>
<span class="font-bold text-gray-700">${item.value}</span>
</div>
`).join("")}
</div>
</div>
`;
}
/**
* 将游戏统计接口响应转换为顶部统计卡片。
*
* @param {Record<string, Record<string, number>>} data 统计接口响应
* @returns {string}
*/
function renderGameStats(data) {
const cards = [
{
icon: "🎲",
title: "百家乐",
items: [
{ label: "总局数", value: data.baccarat.total_rounds.toLocaleString() },
{ label: "总下注", value: `${data.baccarat.total_bets.toLocaleString()}` },
{ label: "今日局数", value: data.baccarat.today_rounds.toLocaleString() },
],
color: "border-red-200 bg-red-50",
},
{
icon: "🎰",
title: "老虎机",
items: [
{ label: "总转动", value: `${data.slot.total_spins.toLocaleString()}` },
{ label: "三7大奖", value: `${data.slot.jackpot_count.toLocaleString()}` },
{ label: "今日转动", value: data.slot.today_spins.toLocaleString() },
],
color: "border-amber-200 bg-amber-50",
},
{
icon: "🐎",
title: "赛马竞猜",
items: [
{ label: "总场次", value: data.horse.total_races.toLocaleString() },
{ label: "总注池", value: `${data.horse.total_pool.toLocaleString()}` },
{ label: "今日场次", value: data.horse.today_races.toLocaleString() },
],
color: "border-emerald-200 bg-emerald-50",
},
{
icon: "📦",
title: "神秘箱子",
items: [
{ label: "总投放", value: data.mystery_box.total_dropped.toLocaleString() },
{ label: "已领取", value: data.mystery_box.total_claimed.toLocaleString() },
{ label: "今日投放", value: data.mystery_box.today_dropped.toLocaleString() },
],
color: "border-purple-200 bg-purple-50",
},
{
icon: "🔮",
title: "神秘占卜",
items: [
{ label: "总占卜", value: `${data.fortune.total_times.toLocaleString()}` },
{ label: "吉签/凶签", value: `${data.fortune.jackpot_count} / ${data.fortune.curse_count}` },
{ label: "今日占卜", value: data.fortune.today_times.toLocaleString() },
],
color: "border-fuchsia-200 bg-fuchsia-50",
},
{
icon: "🎟️",
title: "双色球彩票",
items: [
{ label: "总期数", value: `${data.lottery.total_issues.toLocaleString()}` },
{ label: "历史彩票", value: `${data.lottery.total_bets.toLocaleString()}` },
{ label: "累计奖池", value: `${data.lottery.total_pool.toLocaleString()}` },
],
color: "border-rose-200 bg-rose-50",
},
{
icon: "♟️",
title: "五子棋",
items: [
{ label: "总对局", value: `${data.gomoku.total_games.toLocaleString()}` },
{ label: "人机/对战", value: `${data.gomoku.pve_count} / ${data.gomoku.pvp_count}` },
{ label: "今日对局", value: data.gomoku.today_games.toLocaleString() },
],
color: "border-blue-200 bg-blue-50",
},
];
return cards.map((card) => renderStatsCard(card)).join("");
}
/**
* 加载各游戏实时统计摘要并渲染到顶部面板。
*
* @param {HTMLButtonElement} button 触发按钮
* @returns {Promise<void>}
*/
async function loadGameStats(button) {
const statsUrl = button.getAttribute("data-game-stats-url") || "";
const panel = document.getElementById("game-stats-panel");
const grid = document.getElementById("game-stats-grid");
if (!statsUrl || !panel || !grid) {
return;
}
grid.innerHTML = '<div class="col-span-5 text-center text-gray-400 text-sm py-4">⏳ 加载中...</div>';
panel.classList.remove("hidden");
try {
const response = await fetch(statsUrl, {
headers: { "Accept": "application/json" },
});
const data = await response.json();
grid.innerHTML = renderGameStats(data);
} catch (error) {
grid.innerHTML = '<div class="col-span-5 text-center text-red-400 text-sm py-4">❌ 加载失败,请重试</div>';
}
}
/**
* 根据开关接口响应更新游戏卡片状态。
*
* @param {HTMLButtonElement} button 开关按钮
* @param {boolean} enabled 是否启用
* @returns {void}
*/
function updateGameToggleState(button, enabled) {
const gameKey = button.getAttribute("data-game-key") || "";
const card = document.getElementById(`game-card-${gameKey}`);
const badge = document.getElementById(`badge-${gameKey}`);
const header = card?.querySelector(".flex.items-center.justify-between");
if (badge) {
badge.textContent = enabled ? "运行中" : "已关闭";
badge.className = `text-xs px-2 py-0.5 rounded-full font-bold ${
enabled ? "bg-emerald-100 text-emerald-700" : "bg-gray-200 text-gray-500"
}`;
}
button.textContent = enabled ? "⏸ 关闭游戏" : "▶ 开启游戏";
button.className = `px-5 py-2 rounded-lg font-bold text-sm transition shadow-sm ${
enabled ? "bg-red-500 hover:bg-red-600 text-white" : "bg-emerald-500 hover:bg-emerald-600 text-white"
}`;
// 卡片头部背景是游戏状态的主要视觉反馈,需要和按钮、徽章同步。
if (header) {
header.classList.toggle("bg-emerald-50", enabled);
header.classList.toggle("bg-gray-50", !enabled);
}
}
/**
* 切换游戏开启/关闭状态。
*
* @param {HTMLButtonElement} button 开关按钮
* @returns {Promise<void>}
*/
async function toggleGame(button) {
const toggleUrl = button.getAttribute("data-game-toggle-url") || "";
if (!toggleUrl || button.disabled) {
return;
}
button.disabled = true;
try {
const response = await fetch(toggleUrl, {
method: "POST",
headers: {
"X-CSRF-TOKEN": getCsrfToken(),
"Accept": "application/json",
},
});
const data = await response.json();
if (!data?.ok) {
return;
}
updateGameToggleState(button, Boolean(data.enabled));
window.adminDialog?.alert(
data.message,
data.enabled ? "游戏已开启" : "游戏已关闭",
data.enabled ? "✅" : "⏸",
);
} finally {
button.disabled = false;
}
}
/**
* 管理员手动投放神秘箱子。
*
* @param {HTMLButtonElement} button 投放按钮
* @returns {void}
*/
function dropMysteryBox(button) {
const boxType = button.getAttribute("data-game-drop-box-type") || "normal";
const dropUrl = button.getAttribute("data-game-drop-box-url") || "";
const meta = MYSTERY_BOX_META[boxType] || MYSTERY_BOX_META.normal;
if (!dropUrl) {
return;
}
window.adminDialog?.confirm(
`确定要向 <b>#1 房间</b> 投放一个「${meta.name}」吗?<br><span style="color:#64748b; font-size:12px;">箱子投放后将立即在公屏广播暗号,用户限时领取。</span>`,
`投放${meta.name}`,
() => {
fetch(dropUrl, {
method: "POST",
headers: {
"X-CSRF-TOKEN": getCsrfToken(),
"Content-Type": "application/json",
"Accept": "application/json",
},
body: JSON.stringify({ box_type: boxType }),
})
.then((response) => response.json())
.then((data) => {
window.adminDialog?.alert(
data.message || (data.ok ? "投放成功!" : "投放失败"),
data.ok ? "投放成功" : "投放失败",
data.ok ? meta.icon : "❌",
);
})
.catch(() => window.adminDialog?.alert("网络错误,请重试", "网络错误", "🌐"));
},
meta.icon,
);
}
/**
* 加载双色球当前期次状态。
*
* @param {HTMLButtonElement} button 状态加载按钮
* @returns {Promise<void>}
*/
async function loadLotteryStatus(button) {
const currentUrl = button.getAttribute("data-game-lottery-current-url") || "";
const statusPanel = document.getElementById("lottery-issue-status");
if (!currentUrl || !statusPanel) {
return;
}
statusPanel.innerHTML = '<span class="text-gray-400">⏳ 加载中…</span>';
try {
const response = await fetch(currentUrl, {
headers: { "Accept": "application/json" },
});
const data = await response.json();
if (!data.issue) {
statusPanel.innerHTML = '<span class="text-orange-500">⚠️ 当前无进行中期次,可手动开新期</span>';
return;
}
const issue = data.issue;
const pool = Number(issue.pool_amount).toLocaleString();
const statusMap = {
open: "🟢 购票中",
closed: "🔴 已停售",
settled: "✅ 已开奖",
};
const superTag = issue.is_super_issue ? " 🎊<b>超级期</b>" : "";
const drawAt = issue.draw_at ? issue.draw_at.replace("T", " ") : "--";
statusPanel.innerHTML = `第 <b>${issue.issue_no}</b> 期${superTag} &nbsp;·&nbsp; 状态:${statusMap[issue.status] || issue.status}
&nbsp;·&nbsp; 奖池:<b style="color:#dc2626">💰 ${pool} 金币</b>
&nbsp;·&nbsp; 预计开奖:${drawAt}
&nbsp;·&nbsp; 已购:${issue.total_tickets || 0}`;
} catch (error) {
statusPanel.innerHTML = '<span class="text-red-400">❌ 加载失败</span>';
}
}
/**
* 手动开启新一期双色球。
*
* @param {HTMLButtonElement} button 开期按钮
* @returns {void}
*/
function openLotteryIssue(button) {
const openUrl = button.getAttribute("data-game-lottery-open-url") || "";
if (!openUrl) {
return;
}
window.adminDialog?.confirm(
'确定要手动开启新一期双色球吗?<br><span style="color:#64748b;font-size:12px;">仅在无进行中期次时生效,开奖时间将使用当前配置的 draw_hour:draw_minute。</span>',
"手动开新期",
() => {
fetch(openUrl, {
method: "POST",
headers: {
"X-CSRF-TOKEN": getCsrfToken(),
"Accept": "application/json",
},
})
.then((response) => response.json())
.then((data) => {
window.adminDialog?.alert(data.message, data.ok ? "操作成功" : "操作失败", data.ok ? "✅" : "❌");
if (data.ok) {
const statusButton = document.querySelector("[data-game-lottery-current-url]");
if (statusButton instanceof HTMLButtonElement) {
void loadLotteryStatus(statusButton);
}
}
})
.catch(() => window.adminDialog?.alert("网络错误,请重试", "网络错误", "🌐"));
},
"",
);
}
/**
* 强制提前开奖,用于测试或管理需要。
*
* @param {HTMLButtonElement} button 强制开奖按钮
* @returns {void}
*/
function forceLotteryDraw(button) {
const drawUrl = button.getAttribute("data-game-lottery-force-url") || "";
if (!drawUrl) {
return;
}
window.adminDialog?.confirm(
'确定要<b style="color:red">立即强制开奖</b>吗?<br><span style="color:#64748b;font-size:12px;">将对当前 closed 或 open 期次立即执行开奖,此操作不可撤销!</span>',
"强制开奖确认",
() => {
fetch(drawUrl, {
method: "POST",
headers: {
"X-CSRF-TOKEN": getCsrfToken(),
"Accept": "application/json",
},
})
.then((response) => response.json())
.then((data) => {
window.adminDialog?.alert(data.message, data.ok ? "开奖完成" : "操作失败", data.ok ? "🎊" : "❌");
if (data.ok) {
const statusButton = document.querySelector("[data-game-lottery-current-url]");
if (statusButton instanceof HTMLButtonElement) {
void loadLotteryStatus(statusButton);
}
}
})
.catch(() => window.adminDialog?.alert("网络错误,请重试", "网络错误", "🌐"));
},
"🎊",
);
}
/**
* 绑定游戏管理页通用操作按钮。
*
* @returns {void}
*/
export function bindAdminGameConfigControls() {
if (adminGameConfigControlsBound || typeof document === "undefined") {
return;
}
adminGameConfigControlsBound = true;
document.addEventListener("click", (event) => {
if (!(event.target instanceof Element)) {
return;
}
const statsButton = event.target.closest("[data-game-stats-url]");
if (statsButton instanceof HTMLButtonElement) {
event.preventDefault();
void loadGameStats(statsButton);
return;
}
const toggleButton = event.target.closest("[data-game-toggle-url]");
if (toggleButton instanceof HTMLButtonElement) {
event.preventDefault();
void toggleGame(toggleButton);
return;
}
const dropBoxButton = event.target.closest("[data-game-drop-box-type]");
if (dropBoxButton instanceof HTMLButtonElement) {
event.preventDefault();
dropMysteryBox(dropBoxButton);
return;
}
const lotteryStatusButton = event.target.closest("[data-game-lottery-current-url]");
if (lotteryStatusButton instanceof HTMLButtonElement) {
event.preventDefault();
void loadLotteryStatus(lotteryStatusButton);
return;
}
const lotteryOpenButton = event.target.closest("[data-game-lottery-open-url]");
if (lotteryOpenButton instanceof HTMLButtonElement) {
event.preventDefault();
openLotteryIssue(lotteryOpenButton);
return;
}
const lotteryForceButton = event.target.closest("[data-game-lottery-force-url]");
if (lotteryForceButton instanceof HTMLButtonElement) {
event.preventDefault();
forceLotteryDraw(lotteryForceButton);
}
});
document.addEventListener("mouseover", (event) => {
if (!(event.target instanceof Element)) {
return;
}
const hoverButton = event.target.closest("[data-game-hover-opacity]");
if (hoverButton instanceof HTMLElement) {
hoverButton.style.opacity = hoverButton.getAttribute("data-game-hover-opacity") || "";
}
});
document.addEventListener("mouseout", (event) => {
if (!(event.target instanceof Element)) {
return;
}
const hoverButton = event.target.closest("[data-game-hover-opacity]");
if (hoverButton instanceof HTMLElement) {
hoverButton.style.opacity = "1";
}
});
}