2026-07-09 18:15:08 +08:00

598 lines
20 KiB
JavaScript

(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 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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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 resolveCountdownEndAt() {
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;
// 后端月累充接口当前只返回活动文案周期,不返回毫秒级结束时间;倒计时仍支持 URL 显式覆盖,默认只作为页面视觉计时。
return Date.now() + 25 * 60 * 60 * 1000 + 61 * 1000;
}
function updateScale() {
var scale = Math.min(window.innerWidth / VIEWPORT_WIDTH, 1);
var scaledHeight = Math.ceil(
designHeight * (VIEWPORT_WIDTH / DESIGN_WIDTH)
);
document.documentElement.style.setProperty(
'--app-scale',
String(scale)
);
viewport.style.minHeight = scaledHeight + 'px';
page.style.minHeight = scaledHeight + 'px';
document.body.style.minHeight = Math.ceil(scaledHeight * scale) + '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 [
'<div class="reward-card" style="left:',
card.left,
'px;top:',
card.top,
'px">',
'<img class="reward-frame" src="',
ASSET_BASE,
'reward-card-frame.png',
'" alt="" />',
'<img class="reward-icon" src="',
escapeHTML(card.icon),
'" alt="" />',
'<span class="reward-price">',
escapeHTML(card.price),
'</span>',
'</div>',
].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 [
'<button class="claim-button',
stateClass,
'" type="button" data-rule-id="',
escapeHTML(section.ruleId),
'"',
disabled ? ' disabled' : '',
'>',
'<img class="claim-button-bg" src="',
ASSET_BASE,
'receive-button-bg.png" alt="" />',
'<span>',
escapeHTML(
claim.loading
? t('rechargeReward.claiming', 'Claiming...')
: claim.text || t('rechargeReward.claim', 'Claim')
),
'</span>',
'</button>',
].join('');
}
function renderRewards(rewards) {
if (state.loading) {
rewardSections.innerHTML =
'<p class="reward-message">' +
escapeHTML(t('rechargeReward.loading', 'Loading...')) +
'</p>';
return;
}
if (state.error) {
rewardSections.innerHTML =
'<p class="reward-message is-error">' +
escapeHTML(state.error) +
'</p>';
return;
}
if (!rewards.length) {
rewardSections.innerHTML =
'<p class="reward-message">' +
escapeHTML(t('rechargeReward.noRewards', 'No rewards yet')) +
'</p>';
return;
}
rewardSections.innerHTML = rewards
.map(function (section) {
var title = t(
'rechargeReward.rechargeTitle',
'Recharge {amount}'
).replace('{amount}', section.amount);
return [
'<section class="reward-section" style="--section-top:',
section.top,
'px">',
'<img class="reward-section-bg" src="',
escapeHTML(section.background),
'" alt="" />',
'<div class="title-pill">',
'<span>',
escapeHTML(title),
'</span>',
'</div>',
section.cards
.map(function (card) {
return renderRewardCard(card);
})
.join(''),
renderClaimButton(section),
'</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 || {};
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('hyapp:i18n-ready', function () {
renderPage();
});
}
function init() {
updateScale();
updateCountdown();
renderPage();
bindEvents();
loadData();
if (countdownTimer) window.clearInterval(countdownTimer);
countdownTimer = window.setInterval(updateCountdown, 1000);
}
init();
})();