Files
chatroom/resources/js/chat-room/baccarat-loss-cover.js
T

409 lines
15 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.
// 百乐加强买单活动前台弹窗逻辑,负责活动摘要、历史记录和补偿领取。
import { escapeHtml } from "./html.js";
let baccaratLossCoverEventsBound = false;
/**
* 获取前台买单活动弹窗。
*
* @returns {HTMLElement|null}
*/
function getLossCoverModal() {
return document.getElementById("baccarat-loss-cover-modal");
}
/**
* 读取 Blade 注入到弹窗上的接口地址。
*
* @returns {{summaryUrl:string,historyUrl:string,claimUrlTemplate:string}|null}
*/
function resolveLossCoverUrls() {
const modal = getLossCoverModal();
if (!modal) {
return null;
}
return {
summaryUrl: modal.dataset.blcSummaryUrl || "",
historyUrl: modal.dataset.blcHistoryUrl || "",
claimUrlTemplate: modal.dataset.blcClaimUrlTemplate || "",
};
}
/**
* 生成补偿领取接口地址。
*
* @param {number|string} eventId 活动 ID
* @returns {string}
*/
function resolveClaimUrl(eventId) {
const urls = resolveLossCoverUrls();
return urls?.claimUrlTemplate.replace("__EVENT__", encodeURIComponent(String(eventId))) || "";
}
/**
* 获取 CSRF Token。
*
* @returns {string}
*/
function csrf() {
return document.querySelector('meta[name="csrf-token"]')?.content || "";
}
/**
* 安全格式化活动时间。
*
* @param {string|null|undefined} value
* @returns {string}
*/
function formatTime(value) {
if (!value) {
return "—";
}
return new Date(value).toLocaleString("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
}
/**
* 同步当前用户金币到全局上下文和相关展示节点。
*
* @param {number} amount
* @returns {void}
*/
function syncUserGold(amount) {
if (!Number.isFinite(amount) || !window.chatContext) {
return;
}
window.chatContext.userJjb = Number(window.chatContext.userJjb || 0) + amount;
window.chatContext.myGold = Number(window.chatContext.myGold || 0) + amount;
const hallGold = document.getElementById("game-hall-jjb");
if (hallGold) {
hallGold.textContent = Number(window.chatContext.userJjb || 0).toLocaleString();
}
const modalGold = document.getElementById("blc-modal-jjb");
if (modalGold) {
modalGold.textContent = Number(window.chatContext.userJjb || 0).toLocaleString();
}
}
/**
* 渲染当前买单活动摘要。
*
* @param {object|null} event
* @returns {void}
*/
function renderCurrentEvent(event) {
const container = document.getElementById("blc-current-event");
if (!container) {
return;
}
if (!event) {
container.innerHTML = `
<div style="font-size:30px; margin-bottom:10px;">📭</div>
<div style="font-size:16px; font-weight:bold; color:#166534;">当前暂无进行中的买单活动</div>
<div style="font-size:12px; color:#4b5563; margin-top:8px;">可以在输入框上方的管理员按钮中创建新活动,也可以在这里查看历史记录。</div>
`;
return;
}
const myRecord = event.my_record;
const claimButton = event.status === "claimable" && myRecord?.claim_status === "pending"
? `<button type="button" data-blc-claim="${Number(event.id)}" style="padding:8px 18px;border:none;border-radius:999px;background:#16a34a;color:#fff;font-size:12px;font-weight:bold;cursor:pointer;">领取补偿</button>`
: "";
// 服务端活动字段进入 HTML 前统一转义,避免标题、说明或用户名污染 DOM。
container.innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:12px; text-align:left;">
<div style="flex:1;">
<div style="font-size:18px; font-weight:900; color:#166534;">${escapeHtml(String(event.title || ""))}</div>
<div style="font-size:12px; color:#4b5563; margin-top:6px; line-height:1.7;">${escapeHtml(String(event.description || "活动期间参与百家乐,输掉的金币可在活动结束后领取补偿。"))}</div>
</div>
<span style="padding:4px 10px; border-radius:999px; background:#dcfce7; color:#166534; font-size:12px; font-weight:bold;">${escapeHtml(String(event.status_label || ""))}</span>
</div>
<div style="display:grid; grid-template-columns:repeat(2,1fr); gap:10px; margin-top:16px; text-align:left;">
<div style="background:#f0fdf4; border-radius:8px; padding:12px;">
<div style="font-size:11px; color:#6b7280;">开启人</div>
<div style="font-size:14px; font-weight:bold; color:#166534; margin-top:3px;">${escapeHtml(String(event.creator_username || ""))}</div>
</div>
<div style="background:#f0fdf4; border-radius:8px; padding:12px;">
<div style="font-size:11px; color:#6b7280;">活动时间</div>
<div style="font-size:13px; font-weight:bold; color:#166534; margin-top:3px;">${escapeHtml(formatTime(event.starts_at))} - ${escapeHtml(formatTime(event.ends_at))}</div>
</div>
<div style="background:#f0fdf4; border-radius:8px; padding:12px;">
<div style="font-size:11px; color:#6b7280;">最终已发补偿</div>
<div style="font-size:14px; font-weight:bold; color:#166534; margin-top:3px;">${Number(event.total_claimed_amount || 0).toLocaleString()} 金币</div>
</div>
<div style="background:#f0fdf4; border-radius:8px; padding:12px;">
<div style="font-size:11px; color:#6b7280;">我的状态</div>
<div style="font-size:14px; font-weight:bold; color:#166534; margin-top:3px;">${escapeHtml(String(myRecord?.claim_status_label || "未参与"))}</div>
</div>
</div>
${myRecord ? `
<div style="margin-top:16px; background:#fffbeb; border:1px solid #fde68a; border-radius:10px; padding:14px; text-align:left;">
<div style="font-size:13px; font-weight:bold; color:#a16207; margin-bottom:8px;">我的活动记录</div>
<div style="display:grid; grid-template-columns:repeat(2,1fr); gap:8px; font-size:12px; color:#4b5563;">
<div>累计下注:<b style="color:#166534;">${Number(myRecord.total_bet_amount || 0).toLocaleString()}</b></div>
<div>累计输掉:<b style="color:#b91c1c;">${Number(myRecord.total_loss_amount || 0).toLocaleString()}</b></div>
<div>可领补偿:<b style="color:#166534;">${Number(myRecord.compensation_amount || 0).toLocaleString()}</b></div>
<div>已领补偿:<b style="color:#166534;">${Number(myRecord.claimed_amount || 0).toLocaleString()}</b></div>
</div>
${claimButton ? `<div style="margin-top:12px;">${claimButton}</div>` : ""}
</div>
` : ""}
`;
}
/**
* 渲染买单活动历史记录。
*
* @param {object[]} events
* @returns {void}
*/
function renderHistory(events) {
const container = document.getElementById("blc-history-list");
if (!container) {
return;
}
if (!events || events.length === 0) {
container.innerHTML = '<div style="text-align:center;color:#6b7280;padding:24px 0;">暂无活动记录</div>';
return;
}
container.innerHTML = events.map((event) => {
const myRecord = event.my_record;
const claimButton = event.status === "claimable" && myRecord?.claim_status === "pending"
? `<button type="button" data-blc-claim="${Number(event.id)}" style="padding:6px 14px;border:none;border-radius:999px;background:#16a34a;color:#fff;font-size:12px;font-weight:bold;cursor:pointer;">领取补偿</button>`
: "";
return `
<div style="background:#fff; border:1px solid #dcfce7; border-left:4px solid #16a34a; border-radius:8px; padding:14px;">
<div style="display:flex; justify-content:space-between; gap:10px; align-items:flex-start;">
<div style="flex:1;">
<div style="font-size:15px; font-weight:bold; color:#166534;">${escapeHtml(String(event.title || ""))}</div>
<div style="font-size:12px; color:#4b5563; margin-top:4px;">开启人:${escapeHtml(String(event.creator_username || ""))} ${escapeHtml(formatTime(event.starts_at))} - ${escapeHtml(formatTime(event.ends_at))}</div>
</div>
<span style="padding:4px 10px; border-radius:999px; background:#f0fdf4; color:#166534; font-size:12px; font-weight:bold;">${escapeHtml(String(event.status_label || ""))}</span>
</div>
<div style="display:grid; grid-template-columns:repeat(2,1fr); gap:8px; margin-top:12px; font-size:12px; color:#4b5563;">
<div>最终补偿发放:<b style="color:#166534;">${Number(event.total_claimed_amount || 0).toLocaleString()}</b></div>
<div>本次总输金币:<b style="color:#b91c1c;">${Number(event.total_loss_amount || 0).toLocaleString()}</b></div>
<div>我的状态:<b style="color:#166534;">${escapeHtml(String(myRecord?.claim_status_label || "未参与"))}</b></div>
<div>我的可领:<b style="color:#166534;">${Number(myRecord?.compensation_amount || 0).toLocaleString()}</b></div>
</div>
${myRecord ? `<div style="margin-top:8px; font-size:12px; color:#6b7280;">累计下注 ${Number(myRecord.total_bet_amount || 0).toLocaleString()} 累计输掉 ${Number(myRecord.total_loss_amount || 0).toLocaleString()} 已领 ${Number(myRecord.claimed_amount || 0).toLocaleString()}</div>` : ""}
${claimButton ? `<div style="margin-top:10px;">${claimButton}</div>` : ""}
</div>
`;
}).join("");
}
/**
* 加载当前活动摘要。
*
* @returns {Promise<void>}
*/
async function loadSummary() {
const current = document.getElementById("blc-current-event");
const urls = resolveLossCoverUrls();
if (!current || !urls?.summaryUrl) {
return;
}
current.innerHTML = "加载中…";
try {
const response = await fetch(`${urls.summaryUrl}?scene=overview`, {
headers: {
Accept: "application/json",
},
});
const data = await response.json();
renderCurrentEvent(data.event || null);
} catch (error) {
current.innerHTML = '<div style="color:#dc2626;">活动摘要加载失败,请稍后重试。</div>';
}
}
/**
* 加载活动历史记录。
*
* @returns {Promise<void>}
*/
async function loadHistory() {
const list = document.getElementById("blc-history-list");
const urls = resolveLossCoverUrls();
if (!list || !urls?.historyUrl) {
return;
}
list.innerHTML = "加载中…";
try {
const response = await fetch(urls.historyUrl, {
headers: {
Accept: "application/json",
},
});
const data = await response.json();
renderHistory(data.events || []);
} catch (error) {
list.innerHTML = '<div style="color:#dc2626;">活动历史加载失败,请稍后重试。</div>';
}
}
/**
* 切换买单活动弹窗 Tab。
*
* @param {string} tab
* @returns {void}
*/
export function switchBaccaratLossCoverTab(tab) {
const overview = document.getElementById("blc-overview-pane");
const history = document.getElementById("blc-history-pane");
const overviewButton = document.getElementById("blc-tab-overview");
const historyButton = document.getElementById("blc-tab-history");
if (!overview || !history || !overviewButton || !historyButton) {
return;
}
const showingHistory = tab === "history";
overview.style.display = showingHistory ? "none" : "block";
history.style.display = showingHistory ? "block" : "none";
overviewButton.style.background = showingHistory ? "#dcfce7" : "#15803d";
overviewButton.style.color = showingHistory ? "#166534" : "#fff";
historyButton.style.background = showingHistory ? "#15803d" : "#dcfce7";
historyButton.style.color = showingHistory ? "#fff" : "#166534";
}
/**
* 打开买单活动前台弹窗。
*
* @param {string} [tab]
* @returns {Promise<void>}
*/
export async function openBaccaratLossCoverModal(tab = "overview") {
const modal = getLossCoverModal();
if (!modal) {
return;
}
modal.style.display = "flex";
const modalGold = document.getElementById("blc-modal-jjb");
if (modalGold) {
modalGold.textContent = Number(window.chatContext?.userJjb || 0).toLocaleString();
}
switchBaccaratLossCoverTab(tab);
await Promise.all([loadSummary(), loadHistory()]);
}
/**
* 关闭买单活动前台弹窗。
*
* @returns {void}
*/
export function closeBaccaratLossCoverModal() {
const modal = getLossCoverModal();
if (modal) {
modal.style.display = "none";
}
}
/**
* 领取买单活动补偿。
*
* @param {number|string} eventId
* @returns {Promise<void>}
*/
export async function claimBaccaratLossCover(eventId) {
const claimUrl = resolveClaimUrl(eventId);
if (!claimUrl) {
return;
}
try {
const response = await fetch(claimUrl, {
method: "POST",
headers: {
"X-CSRF-TOKEN": csrf(),
Accept: "application/json",
},
});
const data = await response.json();
if (data.ok) {
syncUserGold(Number(data.amount || 0));
await window.chatDialog?.alert?.(data.message || "补偿领取成功", "系统通知", "#16a34a");
await Promise.all([loadSummary(), loadHistory()]);
return;
}
await window.chatDialog?.alert?.(data.message || "领取失败", "提示", "#f59e0b");
} catch (error) {
await window.chatDialog?.alert?.("领取失败,请稍后重试。", "提示", "#dc2626");
}
}
/**
* 绑定买单活动前台弹窗关闭、Tab 切换与领取按钮事件。
*
* @returns {void}
*/
export function bindBaccaratLossCoverControls() {
if (baccaratLossCoverEventsBound || typeof document === "undefined") {
return;
}
baccaratLossCoverEventsBound = true;
window.openBaccaratLossCoverModal = openBaccaratLossCoverModal;
window.closeBaccaratLossCoverModal = closeBaccaratLossCoverModal;
window.switchBaccaratLossCoverTab = switchBaccaratLossCoverTab;
window.claimBaccaratLossCover = claimBaccaratLossCover;
document.addEventListener("click", (event) => {
if (!(event.target instanceof Element)) {
return;
}
if (event.target.closest("[data-blc-close]")) {
event.preventDefault();
closeBaccaratLossCoverModal();
return;
}
const tabButton = event.target.closest("[data-blc-tab]");
if (tabButton) {
event.preventDefault();
switchBaccaratLossCoverTab(tabButton.getAttribute("data-blc-tab") || "overview");
return;
}
const claimButton = event.target.closest("[data-blc-claim]");
if (claimButton) {
event.preventDefault();
void claimBaccaratLossCover(claimButton.getAttribute("data-blc-claim") || "");
return;
}
const modal = event.target.closest("#baccarat-loss-cover-modal");
if (modal && event.target === modal) {
closeBaccaratLossCoverModal();
}
});
}