(function () { var ASSET_BASE = './assets/recharge-activity/'; var viewport = document.getElementById('appViewport'); var page = document.getElementById('page'); var stage = document.querySelector('.stage'); var rewardSections = document.getElementById('rewardSections'); var bgBottom = document.querySelector('.bg-bottom'); var countNodes = { days: document.querySelector('[data-count="days"]'), hours: document.querySelector('[data-count="hours"]'), minutes: document.querySelector('[data-count="minutes"]'), seconds: document.querySelector('[data-count="seconds"]'), }; var DESIGN_WIDTH = 1080; var VIEWPORT_WIDTH = 375; var MAX_PAGE_WIDTH = 430; var DEFAULT_DESIGN_HEIGHT = 3273; var designHeight = DEFAULT_DESIGN_HEIGHT; var countdownEndAt = resolveCountdownEndAt(); var countdownTimer = 0; var state = { user: { name: '', id: '', avatar: ASSET_BASE + 'profile-avatar.png', accumulatedRecharge: 0, }, rewards: [], claiming: {}, loading: true, error: '', }; var fallbackCards = [ { price: '$100', icon: ASSET_BASE + 'reward-icon.png', left: 111, top: 293, }, { price: '$100', icon: ASSET_BASE + 'reward-icon.png', left: 560, top: 293, }, ]; function getParams() { return new URLSearchParams(window.location.search || ''); } function t(key, fallback) { return window.HyAppI18n && window.HyAppI18n.t ? window.HyAppI18n.t(key, fallback) : fallback || key; } function escapeHTML(value) { return String(value || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function twoDigits(value) { return String(Math.max(0, value)).padStart(2, '0'); } function showToast(message) { if (window.HyAppToast && window.HyAppToast.show) { window.HyAppToast.show(message); return; } window.dispatchEvent( new CustomEvent('hyapp:toast', { detail: { message: message } }) ); } function numeric(value, fallback) { var number = Number(value); return Number.isFinite(number) ? number : fallback || 0; } function parseJSON(value) { if (!value || typeof value !== 'string') return null; try { return JSON.parse(value); } catch (_) { return null; } } function firstValue() { for (var i = 0; i < arguments.length; i += 1) { var value = arguments[i]; if (value !== undefined && value !== null && value !== '') return value; } return ''; } function formatUSD(value) { var amount = numeric(value, 0); return ( '$' + amount.toLocaleString('en-US', { maximumFractionDigits: 2, minimumFractionDigits: amount % 1 === 0 ? 0 : 2, }) ); } function monthlyResetEndAt() { var now = new Date(); return new Date( now.getFullYear(), now.getMonth() + 1, 1, 0, 0, 0, 0 ).getTime(); } function activityTimeEndAt(activityTime) { var matches = String(activityTime || '').match( /(\d{1,2})\s*\/\s*(\d{1,2})\s*$/ ); if (!matches) return 0; var now = new Date(); var day = Number(matches[1]); var month = Number(matches[2]); if (!Number.isFinite(day) || !Number.isFinite(month)) return 0; // 后端月累充返回的 activityTime 是 DD/MM - DD/MM,不带年份;结束日次日 00:00 就是月度清空点。 var endAt = new Date( now.getFullYear(), month - 1, day + 1, 0, 0, 0, 0 ).getTime(); return Number.isFinite(endAt) && endAt > Date.now() ? endAt : 0; } function resolveCountdownEndAt(statusData) { var params = getParams(); var explicitEnd = Date.parse( params.get('end_at') || params.get('endAt') || '' ); if (Number.isFinite(explicitEnd) && explicitEnd > Date.now()) return explicitEnd; var hours = Number(params.get('hours')); if (Number.isFinite(hours) && hours > 0) return Date.now() + hours * 60 * 60 * 1000; return ( activityTimeEndAt(statusData && statusData.activityTime) || monthlyResetEndAt() ); } function getPageWidth() { var viewportWidth = window.innerWidth || document.documentElement.clientWidth || VIEWPORT_WIDTH; return Math.min(Math.max(viewportWidth, 1), MAX_PAGE_WIDTH); } function updateScale() { var pageWidth = getPageWidth(); var designScale = pageWidth / DESIGN_WIDTH; var scaledHeight = Math.ceil(designHeight * designScale); // 视觉稿仍以 1080 坐标布局,只把整张背景和内容按真实 WebView 宽度等比缩放,避免 412px 等机型露出页面两侧空白。 document.documentElement.style.setProperty( '--page-width', pageWidth + 'px' ); document.documentElement.style.setProperty( '--design-scale', String(designScale) ); viewport.style.minHeight = scaledHeight + 'px'; viewport.style.height = scaledHeight + 'px'; page.style.minHeight = scaledHeight + 'px'; page.style.height = scaledHeight + 'px'; document.body.style.minHeight = Math.max(scaledHeight, window.innerHeight || 0) + 'px'; } function updateCountdown() { var diff = Math.max(0, countdownEndAt - Date.now()); var totalSeconds = Math.floor(diff / 1000); var days = Math.floor(totalSeconds / 86400); var hours = Math.floor((totalSeconds % 86400) / 3600); var minutes = Math.floor((totalSeconds % 3600) / 60); var seconds = totalSeconds % 60; countNodes.days.textContent = twoDigits(days); countNodes.hours.textContent = twoDigits(hours); countNodes.minutes.textContent = twoDigits(minutes); countNodes.seconds.textContent = twoDigits(seconds); } function normalizeUser(profile, statusData) { profile = profile || {}; var params = getParams(); return { name: firstValue( profile.userNickname, profile.username, profile.name, params.get('name'), 'Aslan' ), id: String( firstValue( profile.specialId, profile.displayUserId, profile.display_user_id, profile.account, profile.id, profile.userId, params.get('uid'), params.get('id'), '' ) ), avatar: firstValue( profile.userAvatar, profile.avatar, profile.headImg, profile.headImage, params.get('avatar'), ASSET_BASE + 'profile-avatar.png' ), accumulatedRecharge: firstValue( statusData && statusData.monthlyRecharge, params.get('recharge'), 0 ), }; } function rewardProps(resource) { var group = resource && resource.activityResource ? resource.activityResource.propsGroup || {} : {}; return Array.isArray(group.activityRewardProps) ? group.activityRewardProps : []; } function rule(resource) { return resource && resource.activityResource ? resource.activityResource.rule || {} : {}; } function thresholdAmount(resource) { var data = parseJSON(rule(resource).jsonData) || {}; return numeric(data.quantity, 0); } function cardLayout(count, index) { if (count >= 4) { return ( [ { left: 70.5, top: 278 }, { left: 292.5, top: 278 }, { left: 514.5, top: 278 }, { left: 736.5, top: 278 }, ][index] || { left: 736.5, top: 278 } ); } if (count === 3) { return ( [ { left: 70.5, top: 278 }, { left: 339.5, top: 278 }, { left: 608.5, top: 278 }, ][index] || { left: 608.5, top: 278 } ); } return ( [ { left: 111, top: 293 }, { left: 560, top: 293 }, ][index] || { left: 560, top: 293 } ); } function normalizeCard(item, threshold, count, index) { var pos = cardLayout(count, index); return { price: item && item.amount ? formatUSD(item.amount) : formatUSD(threshold), icon: (item && item.cover) || ASSET_BASE + 'reward-icon.png', left: pos.left, top: pos.top, }; } function normalizeReward(resource, index) { var currentRule = rule(resource); var ruleId = String(currentRule.id || resource.ruleId || ''); var threshold = thresholdAmount(resource); var props = rewardProps(resource).slice(0, 4); var receiveStatus = String(resource.receiveStatus || '').toUpperCase(); var claimed = receiveStatus === 'RECEIVED'; var claimable = receiveStatus === 'NOT_RECEIVED'; var cardCount = props.length || fallbackCards.length; return { ruleId: ruleId, amount: formatUSD(threshold), top: 1776 + index * 690, background: index % 2 === 0 ? ASSET_BASE + 'reward-section-a.png' : ASSET_BASE + 'reward-section-b.png', claim: { text: claimed ? t('rechargeReward.claimed', 'Claimed') : t('rechargeReward.claim', 'Claim'), claimable: claimable, claimed: claimed, loading: Boolean(state.claiming[ruleId]), }, cards: props.length ? props.map(function (item, cardIndex) { return normalizeCard( item, threshold, cardCount, cardIndex ); }) : fallbackCards.map(function (card) { return Object.assign({}, card, { price: formatUSD(threshold), }); }), }; } function normalizeRewards(statusData) { var resources = statusData && Array.isArray(statusData.activityResources) ? statusData.activityResources : []; return resources .map(function (item, index) { return normalizeReward(item, index); }) .filter(function (item) { return item.ruleId; }); } function renderProfile(user) { document.querySelector('[data-profile-avatar]').src = user.avatar || ASSET_BASE + 'profile-avatar.png'; document.querySelector('[data-profile-name]').textContent = user.name || 'Aslan'; document.querySelector('[data-profile-id]').textContent = t('rechargeReward.idPrefix', 'ID:') + ' ' + (user.id || '-'); document.querySelector('[data-profile-recharge]').textContent = t('rechargeReward.accumulatedRecharge', 'Accumulated Recharge:') + formatUSD(user.accumulatedRecharge || 0); } function renderRewardCard(card) { return [ '
', '', '', '', escapeHTML(card.price), '', '
', ].join(''); } function renderClaimButton(section) { var claim = section.claim || {}; var stateClass = claim.claimed ? ' is-claimed' : claim.claimable ? ' is-claimable' : ' is-disabled'; var disabled = !claim.claimable || claim.claimed || claim.loading; return [ '', ].join(''); } function renderRewards(rewards) { if (state.loading) { rewardSections.innerHTML = '

' + escapeHTML(t('rechargeReward.loading', 'Loading...')) + '

'; return; } if (state.error) { rewardSections.innerHTML = '

' + escapeHTML(state.error) + '

'; return; } if (!rewards.length) { rewardSections.innerHTML = '

' + escapeHTML(t('rechargeReward.noRewards', 'No rewards yet')) + '

'; return; } rewardSections.innerHTML = rewards .map(function (section) { var title = t( 'rechargeReward.rechargeTitle', 'Recharge {amount}' ).replace('{amount}', section.amount); return [ '
', '', '
', '', escapeHTML(title), '', '
', section.cards .map(function (card) { return renderRewardCard(card); }) .join(''), renderClaimButton(section), '
', ].join(''); }) .join(''); } function updateDesignHeight() { var rewardCount = Math.max(state.rewards.length, 2); designHeight = Math.max( DEFAULT_DESIGN_HEIGHT, 1776 + rewardCount * 690 + 120 ); stage.style.height = designHeight + 'px'; if (bgBottom) bgBottom.style.top = Math.max(2695, designHeight - 578) + 'px'; updateScale(); } function renderPage() { document.title = t('rechargeReward.pageTitle', 'Recharge Reward'); renderProfile(state.user); updateDesignHeight(); renderRewards(state.rewards); } function mockData() { var params = getParams(); var claimed = params.get('claimed') === '1'; return Promise.resolve({ profile: { userNickname: params.get('name') || 'Aslan', account: params.get('uid') || '12345678', userAvatar: params.get('avatar') || ASSET_BASE + 'profile-avatar.png', }, status: { monthlyRecharge: numeric(params.get('recharge'), 5998), activityResources: [ { receiveStatus: claimed ? 'RECEIVED' : 'NOT_RECEIVED', activityResource: { rule: { id: '1000000000000000001', jsonData: '{"quantity":100}', }, propsGroup: { activityRewardProps: [] }, }, }, { receiveStatus: 'INELIGIBLE', activityResource: { rule: { id: '1000000000000000002', jsonData: '{"quantity":200}', }, propsGroup: { activityRewardProps: [] }, }, }, ], }, }); } function loadRemoteData() { if (!window.AslanCommon) { return Promise.reject(new Error('Aslan bridge unavailable')); } return Promise.all([ window.AslanCommon.user.profile().catch(function (error) { console.warn('Aslan profile failed', error); return null; }), window.AslanCommon.rechargeReward.status(), ]).then(function (result) { return { profile: result[0], status: result[1] }; }); } function loadData() { state.loading = true; state.error = ''; renderPage(); var params = getParams(); var loader = params.get('mock') === '1' || params.get('mock') === 'true' ? mockData() : loadRemoteData(); return loader .then(function (payload) { var statusData = payload.status || {}; countdownEndAt = resolveCountdownEndAt(statusData); updateCountdown(); state.user = normalizeUser(payload.profile, statusData); state.rewards = normalizeRewards(statusData); state.loading = false; state.error = ''; renderPage(); }) .catch(function (error) { console.warn('Aslan recharge reward load failed', error); state.loading = false; state.error = (error && error.message) || t('rechargeReward.loadFailed', 'Load failed'); renderPage(); }); } function markClaimed(ruleId) { state.rewards = state.rewards.map(function (item) { if (item.ruleId !== ruleId) return item; item.claim.claimed = true; item.claim.claimable = false; item.claim.loading = false; item.claim.text = t('rechargeReward.claimed', 'Claimed'); return item; }); } function claimReward(ruleId) { if (!ruleId || state.claiming[ruleId] || !window.AslanCommon) return; state.claiming[ruleId] = true; renderPage(); // 领取必须只提交后端规则 Long 的字符串形式,避免 JS Number 精度损失导致发错档位。 window.AslanCommon.rechargeReward .claim(ruleId) .then(function () { delete state.claiming[ruleId]; markClaimed(ruleId); renderPage(); showToast(t('rechargeReward.claimSuccess', 'Claimed')); return loadData(); }) .catch(function (error) { delete state.claiming[ruleId]; renderPage(); showToast( (error && error.message) || t('rechargeReward.claimFailed', 'Claim failed') ); }); } function bindEvents() { rewardSections.addEventListener('click', function (event) { var button = event.target.closest('[data-rule-id]'); if (!button || button.disabled) return; claimReward(button.getAttribute('data-rule-id')); }); window.addEventListener('resize', updateScale); window.addEventListener('orientationchange', updateScale); window.addEventListener('hyapp:i18n-ready', function () { renderPage(); }); } function init() { updateScale(); updateCountdown(); renderPage(); bindEvents(); loadData(); if (countdownTimer) window.clearInterval(countdownTimer); countdownTimer = window.setInterval(updateCountdown, 1000); } init(); })();