2026-03-01 20:06:53 +08:00
|
|
|
|
{{--
|
|
|
|
|
|
文件功能:节日福利弹窗组件
|
|
|
|
|
|
|
2026-04-21 17:53:11 +08:00
|
|
|
|
后台配置的节日福利批次触发时,通过 WebSocket 广播到达前端,
|
2026-03-01 20:06:53 +08:00
|
|
|
|
弹出全屏福利领取弹窗,用户点击领取后金币自动入账。
|
|
|
|
|
|
|
|
|
|
|
|
WebSocket 监听:chat:holiday.started
|
2026-04-21 17:53:11 +08:00
|
|
|
|
领取接口:POST /holiday/runs/{run}/claim
|
|
|
|
|
|
状态接口:GET /holiday/runs/{run}/status
|
2026-03-01 20:06:53 +08:00
|
|
|
|
--}}
|
|
|
|
|
|
|
|
|
|
|
|
{{-- ─── 节日福利领取弹窗 ─── --}}
|
|
|
|
|
|
<div id="holiday-event-modal" x-data="holidayEventModal()" x-show="show" x-cloak>
|
|
|
|
|
|
<div x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 scale-95"
|
|
|
|
|
|
x-transition:enter-end="opacity-100 scale-100" x-transition:leave="transition ease-in duration-200"
|
|
|
|
|
|
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
|
|
|
|
|
|
style="position:fixed; inset:0; background:rgba(0,0,0,.65); z-index:9950; display:flex; align-items:center; justify-content:center;">
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
style="width:400px; max-width:95vw; border-radius:24px; overflow:hidden; text-align:center;
|
|
|
|
|
|
box-shadow:0 24px 80px rgba(245,158,11,.4);">
|
|
|
|
|
|
|
|
|
|
|
|
{{-- 顶部渐变区域 --}}
|
|
|
|
|
|
<div style="background:linear-gradient(145deg,#92400e,#b45309,#d97706); padding:28px 24px 20px;">
|
|
|
|
|
|
{{-- 主图标动效 --}}
|
|
|
|
|
|
<div style="font-size:56px; margin-bottom:10px; animation:holiday-bounce 1.2s infinite;">🎊</div>
|
2026-04-21 17:53:11 +08:00
|
|
|
|
<div x-show="roundLabel"
|
|
|
|
|
|
style="display:none; margin-bottom:10px; color:#fde68a; font-size:11px; font-weight:bold; letter-spacing:1px;"
|
|
|
|
|
|
x-text="roundLabel"></div>
|
2026-03-01 20:06:53 +08:00
|
|
|
|
<div style="color:#fef3c7; font-weight:bold; font-size:20px; margin-bottom:4px;" x-text="eventName">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="color:rgba(254,243,199,.8); font-size:13px;" x-text="eventDesc"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{{-- 主体内容 --}}
|
|
|
|
|
|
<div style="background:linear-gradient(160deg,#7c2d12,#9a3412); padding:20px 24px;">
|
|
|
|
|
|
|
|
|
|
|
|
{{-- 奖池信息 --}}
|
|
|
|
|
|
<div style="background:rgba(0,0,0,.25); border-radius:14px; padding:12px 16px; margin-bottom:16px;">
|
2026-04-21 17:53:11 +08:00
|
|
|
|
<div style="color:rgba(254,243,199,.6); font-size:11px; margin-bottom:4px;">💰 本轮节日奖池</div>
|
2026-03-01 20:06:53 +08:00
|
|
|
|
<div style="color:#fcd34d; font-size:24px; font-weight:bold;"
|
|
|
|
|
|
x-text="totalAmount.toLocaleString() + ' 金币'"></div>
|
|
|
|
|
|
<div style="color:rgba(254,243,199,.5); font-size:11px; margin-top:4px;">
|
|
|
|
|
|
<span x-show="maxClaimants > 0" x-text="'前 ' + maxClaimants + ' 名在线用户可领取'"></span>
|
|
|
|
|
|
<span x-show="maxClaimants === 0">全体在线用户均可领取</span>
|
|
|
|
|
|
</div>
|
2026-04-21 17:53:11 +08:00
|
|
|
|
<div style="color:rgba(254,243,199,.5); font-size:11px; margin-top:4px;">
|
|
|
|
|
|
<span x-show="distributeType === 'fixed' && fixedAmount !== null"
|
|
|
|
|
|
x-text="'每人固定 ' + Number(fixedAmount).toLocaleString() + ' 金币'"></span>
|
|
|
|
|
|
<span x-show="distributeType !== 'fixed' || fixedAmount === null">本轮按随机金额发放</span>
|
|
|
|
|
|
</div>
|
2026-03-01 20:06:53 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{{-- 有效期 --}}
|
|
|
|
|
|
<div style="color:rgba(254,243,199,.55); font-size:11px; margin-bottom:14px;">
|
2026-04-21 17:53:11 +08:00
|
|
|
|
<div x-show="scheduledForText" style="display:none; margin-bottom:4px;">
|
|
|
|
|
|
发放时间 <strong style="color:#fcd34d;" x-text="scheduledForText"></strong>
|
|
|
|
|
|
</div>
|
2026-03-01 20:06:53 +08:00
|
|
|
|
领取有效期 <strong style="color:#fcd34d;" x-text="expiresIn"></strong>,过期作废
|
2026-04-21 17:53:11 +08:00
|
|
|
|
<div x-show="statusHint" style="display:none; margin-top:6px;" x-text="statusHint"></div>
|
2026-03-01 20:06:53 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{{-- 领取按钮 --}}
|
|
|
|
|
|
<div x-show="!claimed">
|
2026-04-22 09:52:35 +08:00
|
|
|
|
<div style="background:rgba(0,0,0,.26); border-radius:999px; padding:10px 16px; margin-top:6px;">
|
|
|
|
|
|
<div style="display:flex; justify-content:center;">
|
|
|
|
|
|
<button x-on:click="doClaim()" :disabled="claiming || loadingStatus || !claimable"
|
|
|
|
|
|
onmouseover="if(!this.disabled)this.style.transform='translateY(-2px) scale(1.04)'"
|
|
|
|
|
|
onmouseout="this.style.transform=''"
|
|
|
|
|
|
:style="claiming || loadingStatus || !claimable
|
|
|
|
|
|
?
|
|
|
|
|
|
'min-width:220px; padding:12px 28px; border-radius:999px; border:none; cursor:not-allowed; display:inline-flex; align-items:center; justify-content:center; gap:8px; box-sizing:border-box; transition:all .25s; font-size:18px; font-weight:900; letter-spacing:1px; background:#c8b89a; color:rgba(100,50,20,.45); box-shadow:none;'
|
|
|
|
|
|
:
|
|
|
|
|
|
'min-width:220px; padding:12px 28px; border-radius:999px; border:none; cursor:pointer; display:inline-flex; align-items:center; justify-content:center; gap:8px; box-sizing:border-box; transition:all .25s; font-size:18px; font-weight:900; letter-spacing:1px; background:#f5e6c8; color:#8b3a1a; box-shadow:0 8px 28px rgba(0,0,0,.28), inset 0 -4px 10px rgba(139,58,26,.12); animation:holiday-claim-pulse 1.8s ease-in-out infinite;'">
|
|
|
|
|
|
<span style="font-size:22px; line-height:1;">🎁</span>
|
|
|
|
|
|
<span x-text="claimButtonText()"></span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-03-01 20:06:53 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{{-- 已领取 --}}
|
|
|
|
|
|
<div x-show="claimed" style="display:none;">
|
|
|
|
|
|
<div style="font-size:36px; margin-bottom:6px; color:#fcd34d; font-weight:bold;"
|
|
|
|
|
|
x-text="'+' + claimedAmount.toLocaleString() + ' 金币'"></div>
|
2026-04-21 17:53:11 +08:00
|
|
|
|
<div style="color:#fef3c7; font-size:13px;">🎉 恭喜!本轮节日福利已入账!</div>
|
|
|
|
|
|
<div style="color:rgba(254,243,199,.6); font-size:11px; margin-top:4px;">金币已自动到账,后续轮次开始时会继续提醒你 ✨</div>
|
2026-03-01 20:06:53 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{{-- 关闭按钮 --}}
|
|
|
|
|
|
<div style="background:rgba(120,40,10,.95); padding:14px 20px;">
|
|
|
|
|
|
<button x-on:click="close()"
|
|
|
|
|
|
style="padding:9px 32px; background:rgba(0,0,0,.35); border:none; border-radius:30px;
|
|
|
|
|
|
font-size:12px; color:rgba(254,243,199,.7); cursor:pointer; transition:all .2s;"
|
|
|
|
|
|
onmouseover="this.style.background='rgba(0,0,0,.5)'"
|
|
|
|
|
|
onmouseout="this.style.background='rgba(0,0,0,.35)'">
|
|
|
|
|
|
<span x-text="claimed ? '收好了 ✨' : '关闭'"></span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
|
@keyframes holiday-bounce {
|
|
|
|
|
|
|
|
|
|
|
|
0%,
|
|
|
|
|
|
100% {
|
|
|
|
|
|
transform: scale(1) rotate(0deg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
25% {
|
|
|
|
|
|
transform: scale(1.1) rotate(-5deg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
75% {
|
|
|
|
|
|
transform: scale(1.1) rotate(5deg);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-22 09:52:35 +08:00
|
|
|
|
|
|
|
|
|
|
@keyframes holiday-claim-pulse {
|
|
|
|
|
|
|
|
|
|
|
|
0%,
|
|
|
|
|
|
100% {
|
|
|
|
|
|
box-shadow: 0 8px 28px rgba(0, 0, 0, .28), inset 0 -4px 10px rgba(139, 58, 26, .12);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
50% {
|
|
|
|
|
|
box-shadow: 0 10px 34px rgba(245, 230, 200, .18), 0 12px 30px rgba(0, 0, 0, .32), inset 0 -4px 10px rgba(139, 58, 26, .16);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-01 20:06:53 +08:00
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
2026-04-21 17:53:11 +08:00
|
|
|
|
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 ?? {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-22 09:52:35 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 缓存最近广播过的节日福利批次详情,供公屏消息按钮二次打开弹窗。
|
|
|
|
|
|
*/
|
|
|
|
|
|
window.__holidayRuns = window.__holidayRuns || {};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从公屏系统消息中打开指定节日福利批次弹窗。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {number|string} runId
|
|
|
|
|
|
*/
|
|
|
|
|
|
window.openHolidayRunFromSystemMessage = function(runId) {
|
|
|
|
|
|
const normalizedRunId = String(runId);
|
|
|
|
|
|
const detail = window.__holidayRuns?.[normalizedRunId];
|
|
|
|
|
|
const el = document.getElementById('holiday-event-modal');
|
|
|
|
|
|
|
|
|
|
|
|
if (!el || typeof Alpine === 'undefined') {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!detail) {
|
|
|
|
|
|
window.chatDialog?.alert('当前福利批次信息未缓存,请等待下一轮广播或刷新页面后重试。', '提示', '#f59e0b');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Alpine.$data(el).open(detail);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 构建公屏系统消息里的领取按钮,便于多个消息场景复用一致视觉。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {number|string|null} runId
|
|
|
|
|
|
* @param {string} label
|
|
|
|
|
|
* @returns {string}
|
|
|
|
|
|
*/
|
|
|
|
|
|
function buildHolidayClaimActionButton(runId, label = '🎁 立即领取') {
|
|
|
|
|
|
if (!runId) {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return ` <button type="button"
|
|
|
|
|
|
onclick="openHolidayRunFromSystemMessage(${JSON.stringify(String(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="点击领取本轮节日福利">${label}</button>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-21 17:53:11 +08:00
|
|
|
|
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}` : '';
|
2026-04-22 09:52:35 +08:00
|
|
|
|
const runId = firstHolidayDefined(detail.run_id, detail.id);
|
|
|
|
|
|
const claimButtonHtml = buildHolidayClaimActionButton(runId);
|
2026-04-21 17:53:11 +08:00
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
`🎊 【${detail.name}】${roundText}开始啦!`,
|
|
|
|
|
|
`总奖池 💰${Number(detail.total_amount).toLocaleString()} 金币`,
|
|
|
|
|
|
amountText,
|
|
|
|
|
|
quotaText,
|
|
|
|
|
|
scheduleText,
|
2026-04-22 09:52:35 +08:00
|
|
|
|
].filter(Boolean).join(',') + claimButtonHtml;
|
2026-04-21 17:53:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-01 20:06:53 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 节日福利弹窗组件
|
|
|
|
|
|
* 监听 WebSocket 事件 holiday.started,弹出领取弹窗
|
|
|
|
|
|
*/
|
|
|
|
|
|
function holidayEventModal() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
show: false,
|
2026-04-21 17:53:11 +08:00
|
|
|
|
loadingStatus: false,
|
2026-03-01 20:06:53 +08:00
|
|
|
|
claiming: false,
|
2026-04-21 17:53:11 +08:00
|
|
|
|
claimable: true,
|
2026-03-01 20:06:53 +08:00
|
|
|
|
claimed: false,
|
2026-04-21 17:53:11 +08:00
|
|
|
|
expiresTimer: null,
|
2026-04-22 09:52:35 +08:00
|
|
|
|
autoCloseTimer: null,
|
2026-03-01 20:06:53 +08:00
|
|
|
|
|
|
|
|
|
|
// 活动数据
|
2026-04-21 17:53:11 +08:00
|
|
|
|
runId: null,
|
|
|
|
|
|
legacyEventId: null,
|
2026-03-01 20:06:53 +08:00
|
|
|
|
eventName: '',
|
|
|
|
|
|
eventDesc: '',
|
2026-04-21 17:53:11 +08:00
|
|
|
|
roundLabel: '',
|
2026-03-01 20:06:53 +08:00
|
|
|
|
totalAmount: 0,
|
|
|
|
|
|
maxClaimants: 0,
|
|
|
|
|
|
distributeType: 'random',
|
|
|
|
|
|
fixedAmount: null,
|
2026-04-21 17:53:11 +08:00
|
|
|
|
scheduledFor: null,
|
|
|
|
|
|
scheduledForText: '',
|
2026-03-01 20:06:53 +08:00
|
|
|
|
expiresAt: null,
|
|
|
|
|
|
expiresIn: '',
|
|
|
|
|
|
claimedAmount: 0,
|
2026-04-21 17:53:11 +08:00
|
|
|
|
statusHint: '',
|
|
|
|
|
|
|
|
|
|
|
|
claimButtonText() {
|
|
|
|
|
|
if (this.loadingStatus) {
|
|
|
|
|
|
return '同步领取状态中…';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (this.claiming) {
|
|
|
|
|
|
return '领取中…';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!this.claimable) {
|
|
|
|
|
|
return '当前不可领取';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return '🎁 立即领取福利';
|
|
|
|
|
|
},
|
2026-03-01 20:06:53 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 打开弹窗并填充活动数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
open(detail) {
|
2026-04-21 17:53:11 +08:00
|
|
|
|
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;
|
2026-03-01 20:06:53 +08:00
|
|
|
|
this.claimed = false;
|
2026-04-21 17:53:11 +08:00
|
|
|
|
this.loadingStatus = false;
|
2026-03-01 20:06:53 +08:00
|
|
|
|
this.claiming = false;
|
|
|
|
|
|
this.claimedAmount = 0;
|
2026-04-22 09:52:35 +08:00
|
|
|
|
this.stopAutoCloseTimer();
|
2026-04-21 17:53:11 +08:00
|
|
|
|
this.statusHint = holidayDetail.run_id
|
|
|
|
|
|
? '当前奖励按本轮福利批次发放,请在有效期内领取。'
|
|
|
|
|
|
: '兼容旧活动通道,等待主线广播升级';
|
2026-03-01 20:06:53 +08:00
|
|
|
|
this.updateExpiresIn();
|
2026-04-21 17:53:11 +08:00
|
|
|
|
this.startExpiresTimer();
|
2026-03-01 20:06:53 +08:00
|
|
|
|
this.show = true;
|
2026-04-21 17:53:11 +08:00
|
|
|
|
this.syncStatus();
|
2026-03-01 20:06:53 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 关闭弹窗
|
|
|
|
|
|
*/
|
|
|
|
|
|
close() {
|
|
|
|
|
|
this.show = false;
|
2026-04-21 17:53:11 +08:00
|
|
|
|
this.stopExpiresTimer();
|
2026-04-22 09:52:35 +08:00
|
|
|
|
this.stopAutoCloseTimer();
|
2026-04-21 17:53:11 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 启动倒计时刷新
|
|
|
|
|
|
*/
|
|
|
|
|
|
startExpiresTimer() {
|
|
|
|
|
|
this.stopExpiresTimer();
|
|
|
|
|
|
this.expiresTimer = window.setInterval(() => this.updateExpiresIn(), 30000);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 停止倒计时刷新
|
|
|
|
|
|
*/
|
|
|
|
|
|
stopExpiresTimer() {
|
|
|
|
|
|
if (this.expiresTimer) {
|
|
|
|
|
|
window.clearInterval(this.expiresTimer);
|
|
|
|
|
|
this.expiresTimer = null;
|
|
|
|
|
|
}
|
2026-03-01 20:06:53 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-04-22 09:52:35 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 启动领取成功后的自动关闭计时器。
|
|
|
|
|
|
*/
|
|
|
|
|
|
startAutoCloseTimer() {
|
|
|
|
|
|
this.stopAutoCloseTimer();
|
|
|
|
|
|
this.autoCloseTimer = window.setTimeout(() => this.close(), 3000);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 停止自动关闭计时器。
|
|
|
|
|
|
*/
|
|
|
|
|
|
stopAutoCloseTimer() {
|
|
|
|
|
|
if (this.autoCloseTimer) {
|
|
|
|
|
|
window.clearTimeout(this.autoCloseTimer);
|
|
|
|
|
|
this.autoCloseTimer = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-01 20:06:53 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 更新有效期显示文字
|
|
|
|
|
|
*/
|
|
|
|
|
|
updateExpiresIn() {
|
2026-04-21 17:53:11 +08:00
|
|
|
|
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,无法查询当前福利状态。';
|
2026-03-01 20:06:53 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-04-21 17:53:11 +08:00
|
|
|
|
|
|
|
|
|
|
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;
|
2026-03-01 20:06:53 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发起领取请求
|
|
|
|
|
|
*/
|
|
|
|
|
|
async doClaim() {
|
2026-04-21 17:53:11 +08:00
|
|
|
|
if (this.claiming || this.claimed || !this.claimable) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const claimUrls = this.buildClaimUrls();
|
|
|
|
|
|
|
|
|
|
|
|
if (claimUrls.length === 0) {
|
|
|
|
|
|
window.chatDialog?.alert('缺少福利批次标识,暂时无法领取。', '提示', '#f59e0b');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-01 20:06:53 +08:00
|
|
|
|
this.claiming = true;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-04-21 17:53:11 +08:00
|
|
|
|
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;
|
2026-04-22 09:52:35 +08:00
|
|
|
|
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();
|
2026-04-21 17:53:11 +08:00
|
|
|
|
|
|
|
|
|
|
if (window.__chatUser && data.balance !== undefined) {
|
|
|
|
|
|
window.__chatUser.jjb = data.balance;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.statusHint = data.message ?? '领取失败,请稍后重试。';
|
|
|
|
|
|
window.chatDialog?.alert(this.statusHint, '提示', '#f59e0b');
|
|
|
|
|
|
|
2026-03-01 20:06:53 +08:00
|
|
|
|
if (data.message?.includes('已结束') || data.message?.includes('过期')) {
|
2026-04-21 17:53:11 +08:00
|
|
|
|
this.claimable = false;
|
2026-03-01 20:06:53 +08:00
|
|
|
|
this.close();
|
2026-04-21 17:53:11 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
this.syncStatus();
|
2026-03-01 20:06:53 +08:00
|
|
|
|
}
|
2026-04-21 17:53:11 +08:00
|
|
|
|
|
|
|
|
|
|
return;
|
2026-03-01 20:06:53 +08:00
|
|
|
|
}
|
2026-04-21 17:53:11 +08:00
|
|
|
|
|
|
|
|
|
|
window.chatDialog?.alert('领取接口尚未切换完成,请稍后再试。', '提示', '#f59e0b');
|
2026-03-01 20:06:53 +08:00
|
|
|
|
} catch {
|
|
|
|
|
|
window.chatDialog?.alert('网络异常,请稍后重试。', '错误', '#cc4444');
|
2026-04-21 17:53:11 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
this.claiming = false;
|
2026-03-01 20:06:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─── WebSocket 事件监听:节日福利开始 ─────────────────────────
|
|
|
|
|
|
window.addEventListener('chat:holiday.started', (e) => {
|
2026-04-21 17:53:11 +08:00
|
|
|
|
const detail = normalizeHolidayPayload(e.detail);
|
2026-03-01 20:06:53 +08:00
|
|
|
|
|
2026-04-22 09:52:35 +08:00
|
|
|
|
if (detail.run_id !== undefined && detail.run_id !== null) {
|
|
|
|
|
|
window.__holidayRuns[String(detail.run_id)] = detail;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-01 20:06:53 +08:00
|
|
|
|
// 公屏追加系统消息
|
|
|
|
|
|
if (typeof appendSystemMessage === 'function') {
|
2026-04-21 17:53:11 +08:00
|
|
|
|
appendSystemMessage(buildHolidaySystemMessage({
|
|
|
|
|
|
...detail,
|
|
|
|
|
|
round_label: buildHolidayRoundLabel(detail),
|
|
|
|
|
|
}));
|
2026-03-01 20:06:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 弹出全屏领取弹窗
|
|
|
|
|
|
const el = document.getElementById('holiday-event-modal');
|
|
|
|
|
|
if (el) Alpine.$data(el).open(detail);
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|