diff --git a/resources/js/chat-room.js b/resources/js/chat-room.js
index 58d4516..7c0a1fb 100644
--- a/resources/js/chat-room.js
+++ b/resources/js/chat-room.js
@@ -28,7 +28,7 @@
* - game-hall.js:处理娱乐大厅弹窗和游戏入口卡片。
* - game-bootstrap.js:提供非关键游戏延迟初始化工具。
* - game-panels.js:处理通用游戏面板关闭事件。
- * - holiday-modal.js:处理节日福利弹窗和系统消息入口。
+ * - holiday-modal.js:处理节日福利弹窗、广播监听、领取状态和系统消息入口。
* - initial-state.js:恢复首屏历史消息、欢迎消息、入场特效和挂起婚姻事件。
* - bank-modal.js:处理银行弹窗、转账、排行和标签切换。
* - fishing.js:处理钓鱼抛竿、收竿、浮漂和自动钓鱼循环。
@@ -93,7 +93,13 @@ export {
export { bindGameHallControls, closeGameHall, openGameHall } from "./chat-room/game-hall.js";
export { bindGameBootstrapControls, deferChatGameBootstrap } from "./chat-room/game-bootstrap.js";
export { bindGamePanelControls } from "./chat-room/game-panels.js";
-export { bindHolidayModalControls, openHolidayRunFromSystemMessage } from "./chat-room/holiday-modal.js";
+export {
+ bindHolidayModalControls,
+ buildHolidayClaimActionButton,
+ buildHolidaySystemMessage,
+ holidayEventModal,
+ openHolidayRunFromSystemMessage,
+} from "./chat-room/holiday-modal.js";
export { bindChatInitialStateControls } from "./chat-room/initial-state.js";
export {
bankAction,
@@ -187,7 +193,13 @@ import {
import { bindGameHallControls, closeGameHall, openGameHall } from "./chat-room/game-hall.js";
import { bindGameBootstrapControls, deferChatGameBootstrap } from "./chat-room/game-bootstrap.js";
import { bindGamePanelControls } from "./chat-room/game-panels.js";
-import { bindHolidayModalControls, openHolidayRunFromSystemMessage } from "./chat-room/holiday-modal.js";
+import {
+ bindHolidayModalControls,
+ buildHolidayClaimActionButton,
+ buildHolidaySystemMessage,
+ holidayEventModal,
+ openHolidayRunFromSystemMessage,
+} from "./chat-room/holiday-modal.js";
import { bindChatInitialStateControls } from "./chat-room/initial-state.js";
import {
bankAction,
@@ -299,6 +311,9 @@ if (typeof window !== "undefined") {
deferChatGameBootstrap,
bindGamePanelControls,
bindHolidayModalControls,
+ buildHolidayClaimActionButton,
+ buildHolidaySystemMessage,
+ holidayEventModal,
openHolidayRunFromSystemMessage,
bindChatInitialStateControls,
loadAdminCurrentLossCoverEvent,
@@ -391,6 +406,9 @@ if (typeof window !== "undefined") {
window.slotPanel = slotPanel;
window.runFeatureShortcut = runFeatureShortcut;
window.runToolbarAction = runToolbarAction;
+ window.buildHolidayClaimActionButton = buildHolidayClaimActionButton;
+ window.buildHolidaySystemMessage = buildHolidaySystemMessage;
+ window.holidayEventModal = holidayEventModal;
window.openHolidayRunFromSystemMessage = openHolidayRunFromSystemMessage;
window.closeAdminBaccaratLossCoverModal = closeAdminBaccaratLossCoverModal;
window.closeCurrentBaccaratLossCoverEvent = closeCurrentBaccaratLossCoverEvent;
diff --git a/resources/js/chat-room/holiday-modal.js b/resources/js/chat-room/holiday-modal.js
index a571ab8..8f15bd3 100644
--- a/resources/js/chat-room/holiday-modal.js
+++ b/resources/js/chat-room/holiday-modal.js
@@ -1,7 +1,580 @@
-// 节日福利弹窗事件代理,承接从 Blade 内联 onclick 迁移出的公屏领取入口。
+// 聊天室节日福利弹窗模块,负责福利广播、系统消息按钮、领取弹窗和状态同步。
+
+import { escapeHtml } from "./html.js";
let holidayModalEventsBound = false;
+/**
+ * 返回第一个非空值。
+ *
+ * @param {...unknown} values
+ * @returns {unknown|null}
+ */
+function firstHolidayDefined(...values) {
+ for (const value of values) {
+ if (value !== undefined && value !== null && value !== "") {
+ return value;
+ }
+ }
+
+ return null;
+}
+
+/**
+ * 将后端字段转换为数字。
+ *
+ * @param {unknown} value
+ * @param {number} fallback
+ * @returns {number}
+ */
+function toHolidayNumber(value, fallback = 0) {
+ if (value === undefined || value === null || value === "") {
+ return fallback;
+ }
+
+ const parsedValue = Number(value);
+
+ return Number.isFinite(parsedValue) ? parsedValue : fallback;
+}
+
+/**
+ * 解析日期字符串。
+ *
+ * @param {unknown} value
+ * @returns {Date|null}
+ */
+function parseHolidayDate(value) {
+ if (!value) {
+ return null;
+ }
+
+ const parsedDate = new Date(value);
+
+ return Number.isNaN(parsedDate.getTime()) ? null : parsedDate;
+}
+
+/**
+ * 格式化福利时间。
+ *
+ * @param {unknown} value
+ * @returns {string}
+ */
+function formatHolidayDate(value) {
+ const parsedDate = parseHolidayDate(value);
+
+ if (!parsedDate) {
+ return "";
+ }
+
+ return new Intl.DateTimeFormat("zh-CN", {
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ }).format(parsedDate);
+}
+
+/**
+ * 格式化剩余有效期。
+ *
+ * @param {Date|null} expiresAt
+ * @returns {string}
+ */
+function formatHolidayRemaining(expiresAt) {
+ if (!expiresAt) {
+ return "以后端状态为准";
+ }
+
+ const remainingMilliseconds = expiresAt.getTime() - Date.now();
+
+ if (remainingMilliseconds <= 0) {
+ return "已过期";
+ }
+
+ const totalMinutes = Math.ceil(remainingMilliseconds / 60000);
+
+ if (totalMinutes < 60) {
+ return `${totalMinutes} 分钟`;
+ }
+
+ if (totalMinutes < 24 * 60) {
+ const hours = Math.floor(totalMinutes / 60);
+ const minutes = totalMinutes % 60;
+
+ return minutes > 0 ? `${hours} 小时 ${minutes} 分钟` : `${hours} 小时`;
+ }
+
+ const days = Math.floor(totalMinutes / (24 * 60));
+ const hours = Math.floor((totalMinutes % (24 * 60)) / 60);
+
+ return hours > 0 ? `${days} 天 ${hours} 小时` : `${days} 天`;
+}
+
+/**
+ * 生成福利轮次标签。
+ *
+ * @param {object} detail
+ * @returns {string}
+ */
+function buildHolidayRoundLabel(detail) {
+ const explicitLabel = firstHolidayDefined(detail.round_label, detail.batch_label, detail.run_label);
+
+ if (explicitLabel) {
+ return String(explicitLabel);
+ }
+
+ const roundNumber = toHolidayNumber(detail.round_no, 0);
+
+ if (detail.repeat_type === "yearly") {
+ return roundNumber > 0 ? `年度第 ${roundNumber} 轮福利` : "年度福利批次";
+ }
+
+ if (roundNumber > 0) {
+ return `第 ${roundNumber} 轮福利`;
+ }
+
+ return detail.scheduled_for ? "本轮定时福利" : "当前福利批次";
+}
+
+/**
+ * 生成弹窗描述文案。
+ *
+ * @param {object} detail
+ * @returns {string}
+ */
+function buildHolidayDescription(detail) {
+ if (detail.description) {
+ return String(detail.description);
+ }
+
+ const amountText = detail.distribute_type === "fixed" && detail.fixed_amount !== null
+ ? `每人固定 ${Number(detail.fixed_amount).toLocaleString()} 金币`
+ : "随机金额发放";
+ const quotaText = detail.max_claimants > 0
+ ? `前 ${detail.max_claimants} 名在线用户可领取`
+ : "在线用户均可领取";
+
+ return `本轮福利已开启,${amountText},${quotaText}。`;
+}
+
+/**
+ * 兼容后端广播字段归一化入口。
+ *
+ * @param {object|null|undefined} detail
+ * @returns {object}
+ */
+function normalizeHolidayPayload(detail) {
+ if (typeof window.normalizeHolidayBroadcastEvent === "function") {
+ return window.normalizeHolidayBroadcastEvent(detail);
+ }
+
+ return detail ?? {};
+}
+
+/**
+ * 构建公屏系统消息里的领取按钮。
+ *
+ * @param {number|string|null} runId
+ * @param {string} label
+ * @returns {string}
+ */
+export function buildHolidayClaimActionButton(runId, label = "🎁 立即领取") {
+ if (!runId) {
+ return "";
+ }
+
+ return ` `;
+}
+
+/**
+ * 构建公屏节日福利系统消息。
+ *
+ * @param {object} detail
+ * @returns {string}
+ */
+export function buildHolidaySystemMessage(detail) {
+ const quotaText = detail.max_claimants > 0
+ ? `前 ${detail.max_claimants} 名在线用户可领取`
+ : "在线用户均可领取";
+ const amountText = detail.distribute_type === "fixed" && detail.fixed_amount !== null
+ ? `每人固定 ${Number(detail.fixed_amount).toLocaleString()} 金币`
+ : "随机金额发放";
+ const scheduleText = detail.scheduled_for ? `发放时间 ${formatHolidayDate(detail.scheduled_for)}` : null;
+ const roundText = detail.round_label ? ` ${detail.round_label}` : "";
+ const runId = firstHolidayDefined(detail.run_id, detail.id);
+
+ return [
+ `🎊 【${escapeHtml(detail.name || "节日福利")}】${escapeHtml(roundText)}开始啦!`,
+ `总奖池 💰${toHolidayNumber(detail.total_amount, 0).toLocaleString()} 金币`,
+ escapeHtml(amountText),
+ escapeHtml(quotaText),
+ escapeHtml(scheduleText || ""),
+ ].filter(Boolean).join(",") + buildHolidayClaimActionButton(runId);
+}
+
+/**
+ * 创建节日福利 Alpine 弹窗组件。
+ *
+ * @returns {object}
+ */
+export function holidayEventModal() {
+ return {
+ show: false,
+ loadingStatus: false,
+ claiming: false,
+ claimable: true,
+ claimed: false,
+ expiresTimer: null,
+ autoCloseTimer: null,
+ runId: null,
+ legacyEventId: null,
+ eventName: "",
+ eventDesc: "",
+ roundLabel: "",
+ totalAmount: 0,
+ maxClaimants: 0,
+ distributeType: "random",
+ fixedAmount: null,
+ scheduledFor: null,
+ scheduledForText: "",
+ expiresAt: null,
+ expiresIn: "",
+ claimedAmount: 0,
+ statusHint: "",
+
+ /**
+ * 返回领取按钮文字。
+ *
+ * @returns {string}
+ */
+ claimButtonText() {
+ if (this.loadingStatus) {
+ return "同步领取状态中…";
+ }
+
+ if (this.claiming) {
+ return "领取中…";
+ }
+
+ if (!this.claimable) {
+ return "当前不可领取";
+ }
+
+ return "🎁 立即领取福利";
+ },
+
+ /**
+ * 打开弹窗并填充活动数据。
+ *
+ * @param {object} detail
+ * @returns {void}
+ */
+ open(detail) {
+ const holidayDetail = normalizeHolidayPayload(detail);
+
+ this.runId = holidayDetail.run_id ?? null;
+ this.legacyEventId = holidayDetail.event_id ?? null;
+ this.eventName = holidayDetail.name ?? "节日福利";
+ this.eventDesc = buildHolidayDescription(holidayDetail);
+ this.roundLabel = buildHolidayRoundLabel(holidayDetail);
+ this.totalAmount = toHolidayNumber(holidayDetail.total_amount, 0);
+ this.maxClaimants = toHolidayNumber(holidayDetail.max_claimants, 0);
+ this.distributeType = holidayDetail.distribute_type ?? "random";
+ this.fixedAmount = firstHolidayDefined(holidayDetail.fixed_amount, null);
+ this.scheduledFor = parseHolidayDate(holidayDetail.scheduled_for);
+ this.scheduledForText = formatHolidayDate(holidayDetail.scheduled_for);
+ this.expiresAt = parseHolidayDate(holidayDetail.expires_at);
+ this.claimable = true;
+ this.claimed = false;
+ this.loadingStatus = false;
+ this.claiming = false;
+ this.claimedAmount = 0;
+ this.stopAutoCloseTimer();
+ this.statusHint = holidayDetail.run_id
+ ? "当前奖励按本轮福利批次发放,请在有效期内领取。"
+ : "兼容旧活动通道,等待主线广播升级";
+ this.updateExpiresIn();
+ this.startExpiresTimer();
+ this.show = true;
+ void this.syncStatus();
+ },
+
+ /**
+ * 关闭弹窗并停止计时器。
+ *
+ * @returns {void}
+ */
+ close() {
+ this.show = false;
+ this.stopExpiresTimer();
+ this.stopAutoCloseTimer();
+ },
+
+ /**
+ * 启动有效期刷新计时器。
+ *
+ * @returns {void}
+ */
+ startExpiresTimer() {
+ this.stopExpiresTimer();
+ this.expiresTimer = window.setInterval(() => this.updateExpiresIn(), 30000);
+ },
+
+ /**
+ * 停止有效期刷新计时器。
+ *
+ * @returns {void}
+ */
+ stopExpiresTimer() {
+ if (this.expiresTimer) {
+ window.clearInterval(this.expiresTimer);
+ this.expiresTimer = null;
+ }
+ },
+
+ /**
+ * 启动领取成功后的自动关闭。
+ *
+ * @returns {void}
+ */
+ startAutoCloseTimer() {
+ this.stopAutoCloseTimer();
+ this.autoCloseTimer = window.setTimeout(() => this.close(), 3000);
+ },
+
+ /**
+ * 停止自动关闭计时器。
+ *
+ * @returns {void}
+ */
+ stopAutoCloseTimer() {
+ if (this.autoCloseTimer) {
+ window.clearTimeout(this.autoCloseTimer);
+ this.autoCloseTimer = null;
+ }
+ },
+
+ /**
+ * 更新有效期展示。
+ *
+ * @returns {void}
+ */
+ updateExpiresIn() {
+ this.expiresIn = formatHolidayRemaining(this.expiresAt);
+ },
+
+ /**
+ * 构建状态接口候选地址。
+ *
+ * @returns {string[]}
+ */
+ buildStatusUrls() {
+ const urls = [];
+
+ if (this.runId) {
+ urls.push(`/holiday/runs/${encodeURIComponent(this.runId)}/status`);
+ }
+
+ if (this.legacyEventId) {
+ urls.push(`/holiday/${encodeURIComponent(this.legacyEventId)}/status`);
+ }
+
+ return urls;
+ },
+
+ /**
+ * 构建领取接口候选地址。
+ *
+ * @returns {string[]}
+ */
+ buildClaimUrls() {
+ const urls = [];
+
+ if (this.runId) {
+ urls.push(`/holiday/runs/${encodeURIComponent(this.runId)}/claim`);
+ }
+
+ if (this.legacyEventId) {
+ urls.push(`/holiday/${encodeURIComponent(this.legacyEventId)}/claim`);
+ }
+
+ return urls;
+ },
+
+ /**
+ * 同步当前福利批次状态。
+ *
+ * @returns {Promise}
+ */
+ async syncStatus() {
+ const statusUrls = this.buildStatusUrls();
+
+ if (statusUrls.length === 0) {
+ this.claimable = false;
+ this.statusHint = "缺少 run_id,无法查询当前福利状态。";
+ return;
+ }
+
+ this.loadingStatus = true;
+
+ try {
+ for (const url of statusUrls) {
+ const response = await fetch(url, {
+ headers: {
+ Accept: "application/json",
+ "X-Requested-With": "XMLHttpRequest",
+ },
+ });
+
+ if (response.status === 404 || response.status === 405) {
+ continue;
+ }
+
+ const data = await response.json();
+ this.applyStatus(data);
+ return;
+ }
+
+ this.statusHint = "状态接口尚未切换完成,可直接尝试领取。";
+ } catch (error) {
+ this.statusHint = "状态同步失败,可直接尝试领取。";
+ } finally {
+ this.loadingStatus = false;
+ }
+ },
+
+ /**
+ * 应用后端返回的状态快照。
+ *
+ * @param {object} data
+ * @returns {void}
+ */
+ applyStatus(data) {
+ if (data.expires_at) {
+ this.expiresAt = parseHolidayDate(data.expires_at);
+ this.updateExpiresIn();
+ }
+
+ const statusValue = firstHolidayDefined(data.status, data.claim_status);
+ const alreadyClaimed = Boolean(data.claimed ?? data.has_claimed ?? false) || ["claimed", "received", "paid"].includes(statusValue);
+ const amount = toHolidayNumber(firstHolidayDefined(data.claimed_amount, data.amount), 0);
+
+ if (alreadyClaimed) {
+ this.claimed = true;
+ this.claimable = false;
+ this.claimedAmount = amount;
+ this.statusHint = data.message ?? "本轮福利已领取。";
+ return;
+ }
+
+ if (data.claimable === false || data.can_claim === false) {
+ this.claimable = false;
+ this.statusHint = data.message ?? "当前不在可领取名单或领取窗口已关闭。";
+ return;
+ }
+
+ this.claimable = true;
+ this.statusHint = data.message ?? this.statusHint;
+ },
+
+ /**
+ * 发起领取请求。
+ *
+ * @returns {Promise}
+ */
+ async doClaim() {
+ if (this.claiming || this.claimed || !this.claimable) {
+ return;
+ }
+
+ const claimUrls = this.buildClaimUrls();
+
+ if (claimUrls.length === 0) {
+ window.chatDialog?.alert?.("缺少福利批次标识,暂时无法领取。", "提示", "#f59e0b");
+ return;
+ }
+
+ this.claiming = true;
+
+ try {
+ for (const url of claimUrls) {
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+ "X-CSRF-TOKEN": document.querySelector("meta[name=csrf-token]")?.content || "",
+ },
+ });
+
+ if (response.status === 404 || response.status === 405) {
+ continue;
+ }
+
+ const data = await response.json();
+ const claimedAmount = toHolidayNumber(firstHolidayDefined(data.claimed_amount, data.amount), 0);
+ const alreadyClaimed = Boolean(data.claimed ?? data.has_claimed ?? false) || data.message?.includes("已领取");
+
+ if (data.ok || alreadyClaimed) {
+ this.claimed = true;
+ this.claimable = false;
+ this.claimedAmount = claimedAmount;
+ this.statusHint = `${data.message ?? "本轮福利已入账。"} 3 秒后自动关闭。`;
+ this.appendClaimMessage(claimedAmount);
+ this.startAutoCloseTimer();
+
+ if (window.__chatUser && data.balance !== undefined) {
+ window.__chatUser.jjb = data.balance;
+ }
+
+ return;
+ }
+
+ this.statusHint = data.message ?? "领取失败,请稍后重试。";
+ window.chatDialog?.alert?.(this.statusHint, "提示", "#f59e0b");
+
+ if (data.message?.includes("已结束") || data.message?.includes("过期")) {
+ this.claimable = false;
+ this.close();
+ } else {
+ void this.syncStatus();
+ }
+
+ return;
+ }
+
+ window.chatDialog?.alert?.("领取接口尚未切换完成,请稍后再试。", "提示", "#f59e0b");
+ } catch (error) {
+ window.chatDialog?.alert?.("网络异常,请稍后重试。", "错误", "#cc4444");
+ } finally {
+ this.claiming = false;
+ }
+ },
+
+ /**
+ * 向公屏追加领取成功消息。
+ *
+ * @param {number} claimedAmount
+ * @returns {void}
+ */
+ appendClaimMessage(claimedAmount) {
+ if (typeof window.appendSystemMessage !== "function") {
+ return;
+ }
+
+ const username = escapeHtml(window.chatContext?.username ?? "当前用户");
+ const eventName = escapeHtml(this.eventName);
+ const roundText = this.roundLabel ? `【${escapeHtml(this.roundLabel)}】` : "";
+ window.appendSystemMessage(
+ `🌟 ${username} 领取了【${eventName}】${roundText},获得 ${claimedAmount.toLocaleString()} 金币!${buildHolidayClaimActionButton(this.runId)}`,
+ );
+ },
+ };
+}
+
/**
* 从公屏系统消息中打开已缓存的节日福利批次。
*
@@ -18,7 +591,7 @@ export function openHolidayRunFromSystemMessage(runId) {
}
if (!detail) {
- window.chatDialog?.alert("当前福利批次信息未缓存,请等待下一轮广播或刷新页面后重试。", "提示", "#f59e0b");
+ window.chatDialog?.alert?.("当前福利批次信息未缓存,请等待下一轮广播或刷新页面后重试。", "提示", "#f59e0b");
return;
}
@@ -26,11 +599,47 @@ export function openHolidayRunFromSystemMessage(runId) {
}
/**
- * 绑定节日福利公屏按钮点击事件。
+ * 处理节日福利广播事件。
+ *
+ * @param {CustomEvent} event
+ * @returns {void}
+ */
+function handleHolidayStarted(event) {
+ const detail = normalizeHolidayPayload(event.detail);
+
+ if (detail.run_id !== undefined && detail.run_id !== null) {
+ window.__holidayRuns[String(detail.run_id)] = detail;
+ }
+
+ if (typeof window.appendSystemMessage === "function") {
+ window.appendSystemMessage(buildHolidaySystemMessage({
+ ...detail,
+ round_label: buildHolidayRoundLabel(detail),
+ }));
+ }
+
+ const modal = document.getElementById("holiday-event-modal");
+ if (modal && typeof window.Alpine?.$data === "function") {
+ window.Alpine.$data(modal)?.open?.(detail);
+ }
+}
+
+/**
+ * 绑定节日福利弹窗全局入口、系统消息按钮和广播事件。
*
* @returns {void}
*/
export function bindHolidayModalControls() {
+ if (typeof window === "undefined") {
+ return;
+ }
+
+ window.__holidayRuns = window.__holidayRuns || {};
+ window.holidayEventModal = holidayEventModal;
+ window.buildHolidayClaimActionButton = buildHolidayClaimActionButton;
+ window.buildHolidaySystemMessage = buildHolidaySystemMessage;
+ window.openHolidayRunFromSystemMessage = openHolidayRunFromSystemMessage;
+
if (holidayModalEventsBound || typeof document === "undefined") {
return;
}
@@ -49,4 +658,5 @@ export function bindHolidayModalControls() {
event.preventDefault();
openHolidayRunFromSystemMessage(claimButton.getAttribute("data-holiday-run-id") || "");
});
+ window.addEventListener("chat:holiday.started", handleHolidayStarted);
}
diff --git a/resources/views/chat/partials/holiday-modal.blade.php b/resources/views/chat/partials/holiday-modal.blade.php
index 4ccea1d..7a5c63d 100644
--- a/resources/views/chat/partials/holiday-modal.blade.php
+++ b/resources/views/chat/partials/holiday-modal.blade.php
@@ -132,525 +132,4 @@
}
-
+{{-- 节日福利弹窗 Alpine 组件和广播监听已迁移到 resources/js/chat-room/holiday-modal.js --}}