2026-07-17 11:54:43 +08:00

496 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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
? '<img class="reward-media" src="' +
escapeHTML(card.preview) +
'" alt="' +
escapeHTML(card.name) +
'" />'
: '';
return [
'<article class="reward-card" style="left:',
left,
'px;top:',
section.cardTop,
'px" data-card-index="',
cardIndex,
'">',
'<img class="reward-card-frame" src="./fami/assets/reward-card-822-2969.png" alt="" data-node-id="822:2969" />',
media,
'<span class="reward-price">',
escapeHTML(tierAmount(section.thresholdUSDMinor)),
'</span>',
'</article>',
].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 [
'<section class="reward-section',
sectionIndex > 0 ? ' is-following' : '',
'" style="--section-top:',
1776 + sectionIndex * SECTION_STEP,
'px" data-tier-id="',
escapeHTML(tier.tierID),
'" data-node-id="',
sectionIndex === 0 ? '822:2965' : '822:2976',
'">',
'<img class="reward-panel" src="./fami/assets/reward-panel-822-2967.png" alt="" data-node-id="822:2967" />',
'<h2 class="reward-title">',
escapeHTML(
t(
'rechargeReward.rechargeTitle',
'Recharge {amount}'
).replace(
'{amount}',
tierAmount(tier.thresholdUSDMinor)
)
),
'</h2>',
tier.cards
.map(function (card, cardIndex) {
return renderCard(
card,
section,
cardIndex,
positions[cardIndex]
);
})
.join(''),
// 累充奖励由 wallet 充值事件自动发放gateway 没有 claim RPC设计中的 Receive 仅显示服务端状态,禁止伪造领取请求。
'<div class="reward-action" role="status" aria-label="',
escapeHTML(t(actionKey, actionFallback)),
'" data-node-id="',
sectionIndex === 0 ? '822:2991' : '822:2990',
'">',
'<img class="reward-action-skin" src="./fami/assets/receive-skin-822-2990.png" alt="" />',
'<span class="reward-action-label">',
escapeHTML(t(actionKey, actionFallback)),
'</span>',
'</div>',
'</section>',
].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();
}
})();