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 --}}