(function () { 'use strict'; var DESIGN_WIDTH = 1080; var BASE_STAGE_HEIGHT = 3273; var SECTION_STEP = 720; var MAX_VIEWPORT_WIDTH = 430; var STATUS_PATH = '/api/v1/activities/cumulative-recharge-reward'; var appViewport = document.getElementById('appViewport'); var stageWrap = document.getElementById('stageWrap'); var stage = document.getElementById('stage'); var countdown = document.getElementById('countdown'); var rewardSections = document.getElementById('rewardSections'); var profileName = document.getElementById('profileName'); var profileID = document.getElementById('profileID'); var profileRecharge = document.getElementById('profileRecharge'); var profileAvatar = document.getElementById('profileAvatar'); var pageStatus = document.getElementById('pageStatus'); 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 currentData = null; var designHeight = BASE_STAGE_HEIGHT; var countdownEndAt = 0; var timeOffsetMS = 0; var countdownTimer = 0; function queryParams() { return new URLSearchParams(window.location.search || ''); } function shouldUseMock(params) { var value = String(params.get('mock') || '').toLowerCase(); // 设计数据只允许显式 mock=1/true;生产接口失败时不得回退到看似可用的虚假奖励。 return value === '1' || value === 'true'; } function t(key, fallback) { return window.HyAppI18n && typeof window.HyAppI18n.t === 'function' ? window.HyAppI18n.t(key, fallback) : fallback || key; } function escapeHTML(value) { return String(value == null ? '' : value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function firstValue(source, keys, fallback) { source = source && typeof source === 'object' ? source : {}; for (var index = 0; index < keys.length; index += 1) { var value = source[keys[index]]; if (value !== undefined && value !== null && value !== '') return value; } return fallback; } function numberValue(source, keys, fallback) { var value = Number(firstValue(source, keys, fallback)); return Number.isFinite(value) ? value : Number(fallback || 0); } function wholeUSD(minor) { var value = Math.max(0, Number(minor || 0)) / 100; return value % 1 === 0 ? String(value) : value.toFixed(2).replace(/0+$/, '').replace(/\.$/, ''); } function tierAmount(minor) { return '$' + wholeUSD(minor); } function twoDigits(value) { return String(Math.max(0, Math.floor(value))).padStart(2, '0'); } function updateScale() { var viewportWidth = Math.min( window.innerWidth || 375, MAX_VIEWPORT_WIDTH ); var scale = viewportWidth / DESIGN_WIDTH; var renderedHeight = designHeight * scale; document.documentElement.style.setProperty( '--fami-stage-scale', String(scale) ); document.documentElement.style.setProperty( '--fami-stage-height', designHeight + 'px' ); document.documentElement.style.setProperty( '--fami-background-bottom-top', designHeight - 578 + 'px' ); appViewport.style.minHeight = renderedHeight + 'px'; stageWrap.style.height = renderedHeight + 'px'; stage.style.height = designHeight + 'px'; } function mockData(params) { var rechargeValue = Number(params.get('recharge') || 5998); var explicitEnd = Date.parse( params.get('end_at') || params.get('endAt') || '' ); var rewardItem = { item_type: 'resource', resource: { name: 'Figma reward fixture', preview_url: './fami/assets/mock-reward-822-2971.png', }, }; return { enabled: true, user: { username: params.get('name') || 'nanfangjian', display_user_id: params.get('uid') || params.get('id') || '12345678', avatar: params.get('avatar') || './fami/assets/mock-avatar-822-2960.png', }, period: { // Figma 节点没有倒计时数字;只有显式 end_at 才展示动态数字,避免凭空猜一个验收值。 end_ms: Number.isFinite(explicitEnd) ? explicitEnd : 0, }, progress: { total_usd_minor: Math.max(0, Math.round(rechargeValue * 100)), }, tiers: [ { tier_id: 1, threshold_usd_minor: 10000, reached: true, resource_group: { items: [rewardItem, rewardItem] }, }, { tier_id: 2, threshold_usd_minor: 10000, reached: true, resource_group: { items: [rewardItem, rewardItem, rewardItem], }, }, ], grants: [], server_time_ms: Date.now(), }; } function resourcePreview(item) { var resource = (item && item.resource) || {}; // preview_url 是 gateway 明确下发给静态 H5 的封面;asset_url 可能是动画,仅作为旧数据兜底。 return String( firstValue( resource, [ 'preview_url', 'previewUrl', 'asset_url', 'assetUrl', 'animation_url', 'animationUrl', ], '' ) || '' ); } function normalizeData(payload) { payload = payload && payload.status ? payload.status : payload || {}; var config = payload.config || {}; var rawTiers = Array.isArray(payload.tiers) ? payload.tiers : Array.isArray(config.tiers) ? config.tiers : []; var grants = Array.isArray(payload.grants) ? payload.grants : []; var grantByTier = {}; grants.forEach(function (grant) { var tierID = String(firstValue(grant, ['tier_id', 'tierId'], '')); if (tierID) grantByTier[tierID] = grant; }); var tiers = rawTiers .slice() .sort(function (a, b) { return ( numberValue(a, ['sort_order', 'sortOrder'], 0) - numberValue(b, ['sort_order', 'sortOrder'], 0) ); }) .map(function (tier) { var group = tier.resource_group || tier.resourceGroup || {}; var items = Array.isArray(group.items) ? group.items : []; var tierID = String( firstValue(tier, ['tier_id', 'tierId'], '') ); var grant = tier.grant || grantByTier[tierID] || null; return { tierID: tierID, thresholdUSDMinor: numberValue( tier, ['threshold_usd_minor', 'thresholdUsdMinor'], 0 ), reached: Boolean(firstValue(tier, ['reached'], false)), grant: grant, cards: items.slice(0, 3).map(function (item) { return { name: firstValue( (item && item.resource) || {}, ['name'], '' ), preview: resourcePreview(item), }; }), }; }); return { enabled: firstValue( payload, ['enabled'], firstValue(config, ['enabled'], true) ) !== false, user: payload.user || {}, period: payload.period || {}, progress: payload.progress || {}, serverTimeMS: numberValue( payload, ['server_time_ms', 'serverTimeMs'], Date.now() ), tiers: tiers, }; } function grantWasIssued(grant) { if (!grant) return false; var status = String(firstValue(grant, ['status'], '')).toLowerCase(); return ( ['granted', 'success', 'succeeded', 'completed'].indexOf(status) >= 0 ); } function cardPositions(count) { if (count <= 1) return [425.5]; if (count === 2) return [201, 650]; return [160.5, 429.5, 698.5]; } function renderCard(card, section, cardIndex, left) { var media = card.preview ? '' +
              escapeHTML(card.name) +
              '' : ''; return [ '
', '', media, '', escapeHTML(tierAmount(section.thresholdUSDMinor)), '', '
', ].join(''); } function renderSections(tiers) { designHeight = Math.max( BASE_STAGE_HEIGHT, BASE_STAGE_HEIGHT + Math.max(0, tiers.length - 2) * SECTION_STEP ); rewardSections.innerHTML = tiers .map(function (tier, sectionIndex) { var section = { thresholdUSDMinor: tier.thresholdUSDMinor, cardTop: sectionIndex === 0 ? 293 : 278, }; var positions = cardPositions(tier.cards.length); var actionKey = grantWasIssued(tier.grant) ? 'rechargeReward.claimed' : 'rechargeReward.receive'; var actionFallback = grantWasIssued(tier.grant) ? 'Claimed' : 'Receive'; return [ '
', '', '

', escapeHTML( t( 'rechargeReward.rechargeTitle', 'Recharge {amount}' ).replace( '{amount}', tierAmount(tier.thresholdUSDMinor) ) ), '

', tier.cards .map(function (card, cardIndex) { return renderCard( card, section, cardIndex, positions[cardIndex] ); }) .join(''), // 累充奖励由 wallet 充值事件自动发放,gateway 没有 claim RPC;设计中的 Receive 仅显示服务端状态,禁止伪造领取请求。 '
', '', '', escapeHTML(t(actionKey, actionFallback)), '', '
', '
', ].join(''); }) .join(''); updateScale(); } function renderProfile(data) { var user = data.user || {}; var displayID = firstValue( user, ['display_user_id', 'displayUserId', 'user_id', 'userId'], '' ); var name = firstValue(user, ['username', 'name'], ''); var avatar = String( firstValue(user, ['avatar', 'avatar_url', 'avatarUrl'], '') || '' ); var totalMinor = numberValue( data.progress || {}, ['total_usd_minor', 'totalUsdMinor'], 0 ); profileName.textContent = name; profileID.textContent = displayID ? t('rechargeReward.idPrefix', 'ID:') + ' ' + displayID : ''; profileRecharge.textContent = t('rechargeReward.accumulatedRecharge', 'Accumulated Recharge:') + wholeUSD(totalMinor); if (avatar) { profileAvatar.src = avatar; profileAvatar.hidden = false; } else { profileAvatar.removeAttribute('src'); profileAvatar.hidden = true; } } function updateCountdown() { if (!countdownEndAt) { countdown.classList.remove('has-values'); Object.keys(countNodes).forEach(function (key) { countNodes[key].textContent = ''; }); return; } var remaining = Math.max( 0, countdownEndAt - (Date.now() - timeOffsetMS) ); var totalSeconds = Math.floor(remaining / 1000); countNodes.days.textContent = twoDigits(totalSeconds / 86400); countNodes.hours.textContent = twoDigits((totalSeconds % 86400) / 3600); countNodes.minutes.textContent = twoDigits((totalSeconds % 3600) / 60); countNodes.seconds.textContent = twoDigits(totalSeconds % 60); countdown.classList.add('has-values'); } function render(data) { currentData = normalizeData(data); renderProfile(currentData); renderSections(currentData.tiers); timeOffsetMS = Date.now() - currentData.serverTimeMS; countdownEndAt = numberValue( currentData.period, ['end_ms', 'endMs'], 0 ); updateCountdown(); if (countdownTimer) window.clearInterval(countdownTimer); if (countdownEndAt) countdownTimer = window.setInterval(updateCountdown, 1000); pageStatus.hidden = true; pageStatus.textContent = ''; stage.setAttribute('aria-busy', 'false'); } function loadStatus(params) { if (shouldUseMock(params)) return Promise.resolve(mockData(params)); if (!window.HyAppAPI || typeof window.HyAppAPI.get !== 'function') { return Promise.reject(new Error('api_unavailable')); } return window.HyAppAPI.get(STATUS_PATH); } function showLoadError(error) { console.warn('[recharge-reward/fami] status load failed', error); currentData = null; rewardSections.innerHTML = ''; profileName.textContent = ''; profileID.textContent = ''; profileRecharge.textContent = ''; profileAvatar.hidden = true; pageStatus.textContent = t('rechargeReward.loadFailed', 'Load failed'); pageStatus.hidden = false; stage.setAttribute('aria-busy', 'false'); } function init() { var params = queryParams(); document.title = t('rechargeReward.pageTitle', 'Recharge Reward'); updateScale(); loadStatus(params).then(render).catch(showLoadError); window.addEventListener('resize', updateScale); window.addEventListener('hyapp:i18n-ready', function () { document.title = t('rechargeReward.pageTitle', 'Recharge Reward'); if (currentData) { // 语言包只影响 DOM 文案;复用已归一化的业务状态,不能再次按原始 API 结构解析而丢失奖励卡。 renderProfile(currentData); renderSections(currentData.tiers); updateCountdown(); } else if (!pageStatus.hidden) pageStatus.textContent = t( 'rechargeReward.loadFailed', 'Load failed' ); }); // 远程头像或奖励封面失效时只隐藏业务媒体,装饰框和真实金额状态仍保留,避免回退到伪造素材。 stage.addEventListener( 'error', function (event) { if ( event.target === profileAvatar || (event.target && event.target.classList.contains('reward-media')) ) { event.target.hidden = true; } }, true ); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init, { once: true }); } else { init(); } })();