2026-07-09 19:18:58 +08:00

656 lines
21 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 () {
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, '&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 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 [
'<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 || {};
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();
})();