迁移节日福利脚本
This commit is contained in:
@@ -28,7 +28,7 @@
|
|||||||
* - game-hall.js:处理娱乐大厅弹窗和游戏入口卡片。
|
* - game-hall.js:处理娱乐大厅弹窗和游戏入口卡片。
|
||||||
* - game-bootstrap.js:提供非关键游戏延迟初始化工具。
|
* - game-bootstrap.js:提供非关键游戏延迟初始化工具。
|
||||||
* - game-panels.js:处理通用游戏面板关闭事件。
|
* - game-panels.js:处理通用游戏面板关闭事件。
|
||||||
* - holiday-modal.js:处理节日福利弹窗和系统消息入口。
|
* - holiday-modal.js:处理节日福利弹窗、广播监听、领取状态和系统消息入口。
|
||||||
* - initial-state.js:恢复首屏历史消息、欢迎消息、入场特效和挂起婚姻事件。
|
* - initial-state.js:恢复首屏历史消息、欢迎消息、入场特效和挂起婚姻事件。
|
||||||
* - bank-modal.js:处理银行弹窗、转账、排行和标签切换。
|
* - bank-modal.js:处理银行弹窗、转账、排行和标签切换。
|
||||||
* - fishing.js:处理钓鱼抛竿、收竿、浮漂和自动钓鱼循环。
|
* - fishing.js:处理钓鱼抛竿、收竿、浮漂和自动钓鱼循环。
|
||||||
@@ -93,7 +93,13 @@ export {
|
|||||||
export { bindGameHallControls, closeGameHall, openGameHall } from "./chat-room/game-hall.js";
|
export { bindGameHallControls, closeGameHall, openGameHall } from "./chat-room/game-hall.js";
|
||||||
export { bindGameBootstrapControls, deferChatGameBootstrap } from "./chat-room/game-bootstrap.js";
|
export { bindGameBootstrapControls, deferChatGameBootstrap } from "./chat-room/game-bootstrap.js";
|
||||||
export { bindGamePanelControls } from "./chat-room/game-panels.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 { bindChatInitialStateControls } from "./chat-room/initial-state.js";
|
||||||
export {
|
export {
|
||||||
bankAction,
|
bankAction,
|
||||||
@@ -187,7 +193,13 @@ import {
|
|||||||
import { bindGameHallControls, closeGameHall, openGameHall } from "./chat-room/game-hall.js";
|
import { bindGameHallControls, closeGameHall, openGameHall } from "./chat-room/game-hall.js";
|
||||||
import { bindGameBootstrapControls, deferChatGameBootstrap } from "./chat-room/game-bootstrap.js";
|
import { bindGameBootstrapControls, deferChatGameBootstrap } from "./chat-room/game-bootstrap.js";
|
||||||
import { bindGamePanelControls } from "./chat-room/game-panels.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 { bindChatInitialStateControls } from "./chat-room/initial-state.js";
|
||||||
import {
|
import {
|
||||||
bankAction,
|
bankAction,
|
||||||
@@ -299,6 +311,9 @@ if (typeof window !== "undefined") {
|
|||||||
deferChatGameBootstrap,
|
deferChatGameBootstrap,
|
||||||
bindGamePanelControls,
|
bindGamePanelControls,
|
||||||
bindHolidayModalControls,
|
bindHolidayModalControls,
|
||||||
|
buildHolidayClaimActionButton,
|
||||||
|
buildHolidaySystemMessage,
|
||||||
|
holidayEventModal,
|
||||||
openHolidayRunFromSystemMessage,
|
openHolidayRunFromSystemMessage,
|
||||||
bindChatInitialStateControls,
|
bindChatInitialStateControls,
|
||||||
loadAdminCurrentLossCoverEvent,
|
loadAdminCurrentLossCoverEvent,
|
||||||
@@ -391,6 +406,9 @@ if (typeof window !== "undefined") {
|
|||||||
window.slotPanel = slotPanel;
|
window.slotPanel = slotPanel;
|
||||||
window.runFeatureShortcut = runFeatureShortcut;
|
window.runFeatureShortcut = runFeatureShortcut;
|
||||||
window.runToolbarAction = runToolbarAction;
|
window.runToolbarAction = runToolbarAction;
|
||||||
|
window.buildHolidayClaimActionButton = buildHolidayClaimActionButton;
|
||||||
|
window.buildHolidaySystemMessage = buildHolidaySystemMessage;
|
||||||
|
window.holidayEventModal = holidayEventModal;
|
||||||
window.openHolidayRunFromSystemMessage = openHolidayRunFromSystemMessage;
|
window.openHolidayRunFromSystemMessage = openHolidayRunFromSystemMessage;
|
||||||
window.closeAdminBaccaratLossCoverModal = closeAdminBaccaratLossCoverModal;
|
window.closeAdminBaccaratLossCoverModal = closeAdminBaccaratLossCoverModal;
|
||||||
window.closeCurrentBaccaratLossCoverEvent = closeCurrentBaccaratLossCoverEvent;
|
window.closeCurrentBaccaratLossCoverEvent = closeCurrentBaccaratLossCoverEvent;
|
||||||
|
|||||||
@@ -1,7 +1,580 @@
|
|||||||
// 节日福利弹窗事件代理,承接从 Blade 内联 onclick 迁移出的公屏领取入口。
|
// 聊天室节日福利弹窗模块,负责福利广播、系统消息按钮、领取弹窗和状态同步。
|
||||||
|
|
||||||
|
import { escapeHtml } from "./html.js";
|
||||||
|
|
||||||
let holidayModalEventsBound = false;
|
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 ` <button type="button"
|
||||||
|
data-holiday-run-id="${escapeHtml(runId)}"
|
||||||
|
style="display:inline-flex; align-items:center; gap:4px; margin-left:8px; padding:3px 10px; border:none; border-radius:999px; background:linear-gradient(135deg,#f59e0b,#d97706); color:#fff; font-size:12px; font-weight:bold; cursor:pointer; box-shadow:0 2px 6px rgba(0,0,0,.22); vertical-align:middle;"
|
||||||
|
title="点击领取本轮节日福利">${escapeHtml(label)}</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建公屏节日福利系统消息。
|
||||||
|
*
|
||||||
|
* @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<void>}
|
||||||
|
*/
|
||||||
|
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<void>}
|
||||||
|
*/
|
||||||
|
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(
|
||||||
|
`🌟 <b>${username}</b> 领取了【${eventName}】${roundText},获得 <b>${claimedAmount.toLocaleString()}</b> 金币!${buildHolidayClaimActionButton(this.runId)}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从公屏系统消息中打开已缓存的节日福利批次。
|
* 从公屏系统消息中打开已缓存的节日福利批次。
|
||||||
*
|
*
|
||||||
@@ -18,7 +591,7 @@ export function openHolidayRunFromSystemMessage(runId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!detail) {
|
if (!detail) {
|
||||||
window.chatDialog?.alert("当前福利批次信息未缓存,请等待下一轮广播或刷新页面后重试。", "提示", "#f59e0b");
|
window.chatDialog?.alert?.("当前福利批次信息未缓存,请等待下一轮广播或刷新页面后重试。", "提示", "#f59e0b");
|
||||||
return;
|
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}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
export function bindHolidayModalControls() {
|
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") {
|
if (holidayModalEventsBound || typeof document === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -49,4 +658,5 @@ export function bindHolidayModalControls() {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
openHolidayRunFromSystemMessage(claimButton.getAttribute("data-holiday-run-id") || "");
|
openHolidayRunFromSystemMessage(claimButton.getAttribute("data-holiday-run-id") || "");
|
||||||
});
|
});
|
||||||
|
window.addEventListener("chat:holiday.started", handleHolidayStarted);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,525 +132,4 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
{{-- 节日福利弹窗 Alpine 组件和广播监听已迁移到 resources/js/chat-room/holiday-modal.js --}}
|
||||||
function firstHolidayDefined(...values) {
|
|
||||||
for (const value of values) {
|
|
||||||
if (value !== undefined && value !== null && value !== '') {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toHolidayNumber(value, fallback = 0) {
|
|
||||||
if (value === undefined || value === null || value === '') {
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedValue = Number(value);
|
|
||||||
|
|
||||||
return Number.isFinite(parsedValue) ? parsedValue : fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseHolidayDate(value) {
|
|
||||||
if (!value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedDate = new Date(value);
|
|
||||||
|
|
||||||
return Number.isNaN(parsedDate.getTime()) ? null : parsedDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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} 天`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildHolidayRoundLabel(detail) {
|
|
||||||
const explicitLabel = firstHolidayDefined(detail.round_label, detail.batch_label, detail.run_label);
|
|
||||||
|
|
||||||
if (explicitLabel) {
|
|
||||||
return 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 ? '本轮定时福利' : '当前福利批次';
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildHolidayDescription(detail) {
|
|
||||||
if (detail.description) {
|
|
||||||
return 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}。`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeHolidayPayload(detail) {
|
|
||||||
if (typeof window.normalizeHolidayBroadcastEvent === 'function') {
|
|
||||||
return window.normalizeHolidayBroadcastEvent(detail);
|
|
||||||
}
|
|
||||||
|
|
||||||
return detail ?? {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 缓存最近广播过的节日福利批次详情,供公屏消息按钮二次打开弹窗。
|
|
||||||
*/
|
|
||||||
window.__holidayRuns = window.__holidayRuns || {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 转义按钮属性值,避免福利批次 ID 写入 data 属性时破坏 HTML。
|
|
||||||
*
|
|
||||||
* @param {number|string} value
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function escapeHolidayAttribute(value) {
|
|
||||||
return String(value).replace(/[&<>"']/g, (char) => ({
|
|
||||||
'&': '&',
|
|
||||||
'<': '<',
|
|
||||||
'>': '>',
|
|
||||||
'"': '"',
|
|
||||||
"'": ''',
|
|
||||||
})[char]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建公屏系统消息里的领取按钮,便于多个消息场景复用一致视觉。
|
|
||||||
*
|
|
||||||
* @param {number|string|null} runId
|
|
||||||
* @param {string} label
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function buildHolidayClaimActionButton(runId, label = '🎁 立即领取') {
|
|
||||||
if (!runId) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const safeRunId = escapeHolidayAttribute(runId);
|
|
||||||
|
|
||||||
return ` <button type="button"
|
|
||||||
data-holiday-run-id="${safeRunId}"
|
|
||||||
style="display:inline-flex; align-items:center; gap:4px; margin-left:8px; padding:3px 10px; border:none; border-radius:999px; background:linear-gradient(135deg,#f59e0b,#d97706); color:#fff; font-size:12px; font-weight:bold; cursor:pointer; box-shadow:0 2px 6px rgba(0,0,0,.22); vertical-align:middle;"
|
|
||||||
title="点击领取本轮节日福利">${label}</button>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
const claimButtonHtml = buildHolidayClaimActionButton(runId);
|
|
||||||
|
|
||||||
return [
|
|
||||||
`🎊 【${detail.name}】${roundText}开始啦!`,
|
|
||||||
`总奖池 💰${Number(detail.total_amount).toLocaleString()} 金币`,
|
|
||||||
amountText,
|
|
||||||
quotaText,
|
|
||||||
scheduleText,
|
|
||||||
].filter(Boolean).join(',') + claimButtonHtml;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 节日福利弹窗组件
|
|
||||||
* 监听 WebSocket 事件 holiday.started,弹出领取弹窗
|
|
||||||
*/
|
|
||||||
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: '',
|
|
||||||
|
|
||||||
claimButtonText() {
|
|
||||||
if (this.loadingStatus) {
|
|
||||||
return '同步领取状态中…';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.claiming) {
|
|
||||||
return '领取中…';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.claimable) {
|
|
||||||
return '当前不可领取';
|
|
||||||
}
|
|
||||||
|
|
||||||
return '🎁 立即领取福利';
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 打开弹窗并填充活动数据
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
this.syncStatus();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 关闭弹窗
|
|
||||||
*/
|
|
||||||
close() {
|
|
||||||
this.show = false;
|
|
||||||
this.stopExpiresTimer();
|
|
||||||
this.stopAutoCloseTimer();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 启动倒计时刷新
|
|
||||||
*/
|
|
||||||
startExpiresTimer() {
|
|
||||||
this.stopExpiresTimer();
|
|
||||||
this.expiresTimer = window.setInterval(() => this.updateExpiresIn(), 30000);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 停止倒计时刷新
|
|
||||||
*/
|
|
||||||
stopExpiresTimer() {
|
|
||||||
if (this.expiresTimer) {
|
|
||||||
window.clearInterval(this.expiresTimer);
|
|
||||||
this.expiresTimer = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 启动领取成功后的自动关闭计时器。
|
|
||||||
*/
|
|
||||||
startAutoCloseTimer() {
|
|
||||||
this.stopAutoCloseTimer();
|
|
||||||
this.autoCloseTimer = window.setTimeout(() => this.close(), 3000);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 停止自动关闭计时器。
|
|
||||||
*/
|
|
||||||
stopAutoCloseTimer() {
|
|
||||||
if (this.autoCloseTimer) {
|
|
||||||
window.clearTimeout(this.autoCloseTimer);
|
|
||||||
this.autoCloseTimer = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新有效期显示文字
|
|
||||||
*/
|
|
||||||
updateExpiresIn() {
|
|
||||||
this.expiresIn = formatHolidayRemaining(this.expiresAt);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建状态接口候选地址
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建领取接口候选地址
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 读取当前批次领取状态
|
|
||||||
*/
|
|
||||||
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 {
|
|
||||||
this.statusHint = '状态同步失败,可直接尝试领取。';
|
|
||||||
} finally {
|
|
||||||
this.loadingStatus = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用后端返回的状态快照
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发起领取请求
|
|
||||||
*/
|
|
||||||
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 res = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status === 404 || res.status === 405) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await res.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 秒后自动关闭。`;
|
|
||||||
|
|
||||||
if (typeof appendSystemMessage === 'function') {
|
|
||||||
const username = window.chatContext?.username ?? '当前用户';
|
|
||||||
const roundText = this.roundLabel ? `【${this.roundLabel}】` : '';
|
|
||||||
appendSystemMessage(
|
|
||||||
`🌟 <b>${username}</b> 领取了【${this.eventName}】${roundText},获得 <b>${claimedAmount.toLocaleString()}</b> 金币!${buildHolidayClaimActionButton(this.runId)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
this.syncStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.chatDialog?.alert('领取接口尚未切换完成,请稍后再试。', '提示', '#f59e0b');
|
|
||||||
} catch {
|
|
||||||
window.chatDialog?.alert('网络异常,请稍后重试。', '错误', '#cc4444');
|
|
||||||
} finally {
|
|
||||||
this.claiming = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── WebSocket 事件监听:节日福利开始 ─────────────────────────
|
|
||||||
window.addEventListener('chat:holiday.started', (e) => {
|
|
||||||
const detail = normalizeHolidayPayload(e.detail);
|
|
||||||
|
|
||||||
if (detail.run_id !== undefined && detail.run_id !== null) {
|
|
||||||
window.__holidayRuns[String(detail.run_id)] = detail;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 公屏追加系统消息
|
|
||||||
if (typeof appendSystemMessage === 'function') {
|
|
||||||
appendSystemMessage(buildHolidaySystemMessage({
|
|
||||||
...detail,
|
|
||||||
round_label: buildHolidayRoundLabel(detail),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 弹出全屏领取弹窗
|
|
||||||
const el = document.getElementById('holiday-event-modal');
|
|
||||||
if (el) Alpine.$data(el).open(detail);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user