迁移节日福利脚本

This commit is contained in:
2026-04-25 14:47:07 +08:00
parent 9f61dcc619
commit 585a6fbf5f
3 changed files with 635 additions and 528 deletions
@@ -132,525 +132,4 @@
}
</style>
<script>
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) => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;',
})[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>
{{-- 节日福利弹窗 Alpine 组件和广播监听已迁移到 resources/js/chat-room/holiday-modal.js --}}