598 lines
20 KiB
JavaScript
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, '<')
|
|
.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 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();
|
|
})();
|