2026-07-18 11:13:12 +08:00

1275 lines
44 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 RANKING_LIMIT = 30;
var DEFAULT_TIME_ZONE = 'Asia/Riyadh';
var state = {
detail: null,
activityId: '',
currentView: 'tasks',
currentPeriod: 'DAILY',
serverOffsetMs: 0,
countdownTimer: null,
rankings: {},
rankingLoading: {},
claimRequestIds: {},
claiming: {},
toastTimer: null,
};
function byId(id) {
return document.getElementById(id);
}
function all(selector, root) {
return Array.prototype.slice.call(
(root || document).querySelectorAll(selector)
);
}
function t(key, fallback, params) {
var translated = fallback || key;
if (window.HyAppI18n && window.HyAppI18n.t) {
translated = window.HyAppI18n.t(key, fallback || key);
}
// common/i18n.js 的 t 只负责取词,活动页在这里统一插值,避免把 params
// 误当 fallback 导致语言包尚未就绪时返回对象并触发运行时异常。
translated = text(translated || fallback || key);
Object.keys(params || {}).forEach(function (name) {
translated = translated
.split('{' + name + '}')
.join(text(params[name]));
});
return translated;
}
function asArray(value) {
return Array.isArray(value) ? value : [];
}
function firstDefined() {
for (var index = 0; index < arguments.length; index += 1) {
var value = arguments[index];
if (value !== undefined && value !== null && value !== '') {
return value;
}
}
return undefined;
}
function toNumber(value, fallback) {
var parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback || 0;
}
function toTime(value) {
if (value === undefined || value === null || value === '') return 0;
if (typeof value === 'number') {
return value > 0 && value < 100000000000 ? value * 1000 : value;
}
var numeric = Number(value);
if (Number.isFinite(numeric)) return toTime(numeric);
var parsed = Date.parse(String(value));
return Number.isFinite(parsed) ? parsed : 0;
}
function text(value) {
return value === undefined || value === null ? '' : String(value);
}
function createElement(tagName, className, content) {
var element = document.createElement(tagName);
if (className) element.className = className;
if (content !== undefined) element.textContent = text(content);
return element;
}
function formatNumber(value) {
var number = toNumber(value, 0);
try {
return new Intl.NumberFormat(
document.documentElement.lang || 'en',
{
notation:
Math.abs(number) >= 1000000 ? 'compact' : 'standard',
maximumFractionDigits: 1,
}
).format(number);
} catch (_) {
return String(number);
}
}
function safeImageURL(value) {
var candidate = text(value).trim();
if (!candidate) return '';
try {
var parsed = new URL(candidate, window.location.href);
var loopback = ['127.0.0.1', 'localhost', '[::1]'].indexOf(
parsed.hostname
);
// Production media must be HTTPS. HTTP is accepted only for loopback
// mock/local development so a remote response cannot inject mixed content.
if (parsed.protocol === 'https:' || loopback >= 0) {
return parsed.toString();
}
} catch (_) {
return '';
}
return '';
}
function query(name) {
return new URLSearchParams(window.location.search || '').get(name);
}
function isMockMode() {
return query('mock') === '1';
}
function makeRequestId(scope) {
var prefix = 'gift-challenge:' + text(scope) + ':';
var random =
window.crypto && window.crypto.randomUUID
? window.crypto.randomUUID()
: Date.now().toString(36) + Math.random().toString(36).slice(2);
return prefix + random;
}
function showToast(message) {
var toast = byId('toast');
if (!toast || !message) return;
window.clearTimeout(state.toastTimer);
toast.textContent = message;
toast.hidden = false;
state.toastTimer = window.setTimeout(function () {
toast.hidden = true;
}, 2600);
}
function getActivity(payload) {
return (payload && payload.activity) || payload || {};
}
function normalizeReward(reward, index) {
reward = reward || {};
return {
id: text(
firstDefined(
reward.id,
reward.rewardId,
reward.resourceId,
'reward-' + index
)
),
name: text(
firstDefined(
reward.name,
reward.rewardName,
reward.resourceName,
t('giftChallenge.reward', 'Reward')
)
),
quantity: Math.max(
1,
toNumber(
firstDefined(
reward.quantity,
reward.count,
reward.amount,
reward.duration,
1
),
1
)
),
imageUrl: safeImageURL(
firstDefined(
reward.imageUrl,
reward.iconUrl,
reward.resourceUrl,
reward.cover,
reward.sourceUrl,
reward.url
)
),
};
}
function normalizeTask(task, index) {
task = task || {};
var target = Math.max(
1,
toNumber(firstDefined(task.target, task.targetValue, 1), 1)
);
var progress = Math.max(
0,
toNumber(
firstDefined(
task.progress,
task.progressValue,
task.currentValue,
0
),
0
)
);
var rawStatus = text(
firstDefined(task.deliveryStatus, task.status, task.taskStatus)
).toUpperCase();
var completed =
task.completed === true ||
Boolean(task.completedTime) ||
rawStatus === 'COMPLETED' ||
progress >= target;
var claimed =
task.claimed === true ||
rawStatus === 'SUCCESS' ||
rawStatus === 'CLAIMED' ||
rawStatus === 'RECEIVED';
var claimable =
!claimed &&
(rawStatus === '' ||
rawStatus === 'NOT_CLAIMED' ||
rawStatus === 'CLAIMABLE' ||
rawStatus === 'COMPLETED') &&
(task.claimable === true || completed);
var type = text(
firstDefined(task.taskType, task.type, task.code)
).toUpperCase();
var code = text(
firstDefined(task.taskCode, task.code, 'TASK_' + (index + 1))
);
return {
code: code,
type: type,
title: text(
firstDefined(
task.taskTitle,
task.title,
task.taskName,
task.name
)
),
sortOrder: Math.max(
1,
toNumber(
firstDefined(task.sortOrder, task.sort, index + 1),
index + 1
)
),
target: target,
progress: progress,
completed: completed,
claimed: claimed,
claimable: claimable,
deliveryStatus: rawStatus,
enabled: task.enabled !== false,
rewards: asArray(
firstDefined(
task.rewards,
task.rewardItems,
task.rewardList,
[]
)
).map(normalizeReward),
};
}
function normalizeTasks(tasks) {
return asArray(tasks)
.map(normalizeTask)
.filter(function (task) {
return task.enabled;
})
.sort(function (left, right) {
return (
left.sortOrder - right.sortOrder ||
left.code.localeCompare(right.code)
);
});
}
function normalizeTier(tier, index) {
tier = tier || {};
var startRank = Math.max(
1,
toNumber(
firstDefined(
tier.startRank,
tier.rankStart,
tier.fromRank,
index + 1
),
index + 1
)
);
var endRank = Math.max(
startRank,
toNumber(
firstDefined(
tier.endRank,
tier.rankEnd,
tier.toRank,
startRank
),
startRank
)
);
return {
id: text(firstDefined(tier.id, tier.ruleId, index)),
period: text(
firstDefined(
tier.periodType,
tier.period,
tier.rankingPeriod,
'OVERALL'
)
).toUpperCase(),
startRank: startRank,
endRank: endRank,
rewards: asArray(
firstDefined(
tier.rewards,
tier.rewardItems,
tier.rewardList,
[]
)
).map(normalizeReward),
};
}
function normalizeRankRewards(payload) {
var raw = firstDefined(
payload && payload.rankRewards,
payload && payload.rankingRewards,
getActivity(payload).rankRewards,
[]
);
var result = { DAILY: [], OVERALL: [] };
if (Array.isArray(raw)) {
raw.map(normalizeTier).forEach(function (tier) {
var period = tier.period === 'DAILY' ? 'DAILY' : 'OVERALL';
result[period].push(tier);
});
} else if (raw && typeof raw === 'object') {
['DAILY', 'OVERALL'].forEach(function (period) {
result[period] = asArray(
raw[period] || raw[period.toLowerCase()]
).map(function (tier, index) {
return normalizeTier(
Object.assign({}, tier, { period: period }),
index
);
});
});
}
Object.keys(result).forEach(function (period) {
result[period].sort(function (left, right) {
return left.startRank - right.startRank;
});
});
return result;
}
function normalizeDetail(payload) {
payload = payload || {};
var activity = getActivity(payload);
var tasks = firstDefined(
payload.tasks,
payload.taskList,
payload.me && payload.me.tasks,
activity.tasks,
[]
);
var serverTime = toTime(
firstDefined(payload.serverTime, activity.serverTime, Date.now())
);
return {
raw: payload,
activityId: text(
firstDefined(
activity.activityId,
activity.id,
payload.activityId,
payload.id
)
),
name: text(
firstDefined(
activity.activityName,
activity.name,
t('giftChallenge.title', 'Gift Challenge')
)
),
status: text(
firstDefined(payload.activityStatus, activity.status, 'ACTIVE')
).toUpperCase(),
timeZone: text(
firstDefined(
activity.timeZone,
payload.timeZone,
DEFAULT_TIME_ZONE
)
),
startTime: toTime(
firstDefined(activity.startTime, payload.startTime)
),
endTime: toTime(firstDefined(activity.endTime, payload.endTime)),
serverTime: serverTime || Date.now(),
statDate: text(
firstDefined(
payload.statDate,
payload.currentDate,
activity.statDate
)
),
tasks: normalizeTasks(tasks),
rankRewards: normalizeRankRewards(payload),
};
}
function mockDetail() {
var now = Date.now();
var rewardSet = [
{ id: 'gold', name: 'Gold', quantity: 99999 },
{ id: 'frame', name: 'Avatar Frame', quantity: 7 },
{ id: 'badge', name: 'Badge', quantity: 1 },
];
return {
activity: {
id: 'mock-gift-challenge',
activityName: 'Gift Challenge',
timeZone: DEFAULT_TIME_ZONE,
startTime: now - 86400000,
endTime: now + 6 * 86400000,
status: 'ACTIVE',
},
serverTime: now,
activityStatus: 'ACTIVE',
tasks: [
{
id: 'mock-task-1',
taskCode: 'DAILY_ENTRY',
taskType: 'ENTER_PAGE',
taskTitle: 'Visit the event page daily.',
targetValue: 1,
enabled: true,
sortOrder: 1,
rewardItems: rewardSet,
},
{
id: 'mock-task-2',
taskCode: 'SEND_GIFT_GOLD_1',
taskType: 'SEND_GIFT_GOLD',
taskTitle: 'Send 5,000 Gold worth of eligible gifts daily.',
targetValue: 5000,
enabled: true,
sortOrder: 2,
rewardItems: rewardSet,
},
{
id: 'mock-task-3',
taskCode: 'SEND_GIFT_GOLD_2',
taskType: 'SEND_GIFT_GOLD',
taskTitle:
'Send 10,000 Gold worth of eligible gifts daily.',
targetValue: 10000,
enabled: true,
sortOrder: 3,
rewardItems: rewardSet,
},
],
rankRewards: [
{
periodType: 'DAILY',
startRank: 1,
endRank: 1,
rewardItems: rewardSet,
},
{
periodType: 'DAILY',
startRank: 2,
endRank: 3,
rewardItems: rewardSet,
},
{
periodType: 'OVERALL',
startRank: 1,
endRank: 1,
rewardItems: rewardSet,
},
{
periodType: 'OVERALL',
startRank: 2,
endRank: 2,
rewardItems: rewardSet,
},
{
periodType: 'OVERALL',
startRank: 3,
endRank: 3,
rewardItems: rewardSet,
},
{
periodType: 'OVERALL',
startRank: 4,
endRank: 6,
rewardItems: rewardSet,
},
{
periodType: 'OVERALL',
startRank: 7,
endRank: 10,
rewardItems: rewardSet,
},
],
};
}
function mockUserState() {
var rewardSet = mockDetail().rankRewards[0].rewardItems;
return {
activityId: 'mock-gift-challenge',
statDate: new Date().toISOString().slice(0, 10),
totalScore: 123456,
dailyScore: 123456,
overallRank: 4,
dailyRank: 4,
tasks: [
{
taskCode: 'DAILY_ENTRY',
taskType: 'ENTER_PAGE',
taskTitle: 'Visit the event page daily.',
sortOrder: 1,
targetValue: 1,
progressValue: 1,
completed: true,
deliveryStatus: 'NOT_CLAIMED',
rewardItems: rewardSet,
},
{
taskCode: 'SEND_GIFT_GOLD_1',
taskType: 'SEND_GIFT_GOLD',
taskTitle: 'Send 5,000 Gold worth of eligible gifts daily.',
sortOrder: 2,
targetValue: 5000,
progressValue: 2400,
completed: false,
deliveryStatus: 'NOT_CLAIMED',
rewardItems: rewardSet,
},
{
taskCode: 'SEND_GIFT_GOLD_2',
taskType: 'SEND_GIFT_GOLD',
taskTitle:
'Send 10,000 Gold worth of eligible gifts daily.',
sortOrder: 3,
targetValue: 10000,
progressValue: 10000,
completed: true,
deliveryStatus: 'SUCCESS',
rewardItems: rewardSet,
},
],
};
}
function mockRanking(period) {
var entries = Array.from({ length: 10 }, function (_, index) {
return {
rank: index + 1,
userId: 'mock-' + (index + 1),
account: String(1635468 + index),
nickname: ['Keven', 'Ayla', 'Leo', 'Nora'][index % 4],
score: (123456 - index * 8173) * (period === 'OVERALL' ? 6 : 1),
me: index === 3,
};
});
return {
entries: entries,
my: Object.assign({}, entries[3], { me: true }),
periodType: period,
settled: period === 'DAILY',
};
}
function mockApi() {
return {
current: function () {
return Promise.resolve(mockDetail());
},
enter: function () {
return Promise.resolve(mockUserState());
},
ranking: function (_, period) {
return Promise.resolve(mockRanking(period));
},
claimTask: function (_, taskCode) {
var userState = mockUserState();
var task = userState.tasks.find(function (item) {
return item.taskCode === taskCode;
});
if (task) task.deliveryStatus = 'SUCCESS';
return Promise.resolve(userState);
},
};
}
function api() {
if (isMockMode()) return mockApi();
if (window.AslanCommon && window.AslanCommon.giftChallenge) {
return window.AslanCommon.giftChallenge;
}
throw new Error('gift_challenge_api_unavailable');
}
function taskTitle(task) {
if (task.type === 'ENTER_PAGE' || task.code === 'ENTER_PAGE') {
return t('giftChallenge.taskEnter', 'Visit the event page daily.');
}
if (task.type === 'SEND_GIFT_GOLD') {
return t(
'giftChallenge.taskSendGold',
'Send {amount} Gold worth of eligible gifts daily.',
{ amount: formatNumber(task.target) }
).replace('{amount}', formatNumber(task.target));
}
// 固定任务类型使用 common 四语文案;后台 taskTitle 只作为未知扩展类型兜底,
// 否则单一配置字符串会覆盖 ar/tr/es 并破坏页面多语言能力。
if (task.title) return task.title;
return t('giftChallenge.untitledTask', 'Daily task');
}
function rewardSlot(reward) {
reward = normalizeReward(reward, 0);
var slot = createElement('div', 'reward-slot');
slot.title = reward.name;
if (reward.imageUrl) {
var image = document.createElement('img');
image.src = reward.imageUrl;
image.alt = reward.name;
image.loading = 'lazy';
image.referrerPolicy = 'no-referrer';
slot.appendChild(image);
} else {
var fallback = createElement('span', 'reward-placeholder', '✦');
fallback.setAttribute('aria-label', reward.name);
slot.appendChild(fallback);
}
slot.appendChild(
createElement('span', 'reward-count', formatNumber(reward.quantity))
);
return slot;
}
function renderTasks() {
var list = byId('taskList');
var empty = byId('taskEmpty');
if (!list || !state.detail) return;
list.replaceChildren();
var tasks = state.detail.tasks;
empty.hidden = tasks.length > 0;
tasks.forEach(function (task) {
var card = createElement('article', 'task-card');
card.dataset.taskCode = task.code;
var heading = createElement('div', 'task-heading');
var titleWrap = createElement('div', 'task-title-wrap');
titleWrap.appendChild(createElement('h3', '', taskTitle(task)));
titleWrap.appendChild(
createElement(
'span',
'task-progress',
formatNumber(Math.min(task.progress, task.target)) +
' / ' +
formatNumber(task.target)
)
);
heading.appendChild(titleWrap);
var action = createElement('button', 'task-action');
action.type = 'button';
action.dataset.taskCode = task.code;
if (state.claiming[task.code]) {
action.classList.add('is-loading');
action.disabled = true;
action.textContent = t('giftChallenge.claiming', 'Claiming');
} else if (task.claimed) {
action.classList.add('is-claimed');
action.disabled = true;
action.textContent = t('giftChallenge.claimed', 'Claimed');
} else if (task.claimable) {
action.dataset.action = 'claim';
action.textContent = t('giftChallenge.receive', 'Receive');
} else if (task.completed) {
// PENDING/PROCESSING 仍可能在下游发放FAILED/UNKNOWN 则必须由运营
// 核账;两类状态都禁用按钮,避免 H5 盲目重发已经到账的奖励。
action.classList.add('is-claimed');
action.disabled = true;
action.textContent =
task.deliveryStatus === 'PENDING' ||
task.deliveryStatus === 'PROCESSING'
? t('giftChallenge.processing', 'Processing')
: t('giftChallenge.underReview', 'Under review');
} else {
action.dataset.action = 'go';
action.textContent = t('giftChallenge.go', 'Go');
}
heading.appendChild(action);
card.appendChild(heading);
var strip = createElement('div', 'reward-strip');
task.rewards.slice(0, 3).forEach(function (reward) {
strip.appendChild(rewardSlot(reward));
});
if (!task.rewards.length) {
strip.appendChild(
rewardSlot({
name: t('giftChallenge.reward', 'Reward'),
quantity: 1,
})
);
}
card.appendChild(strip);
list.appendChild(card);
});
}
function rankingEntries(payload) {
return asArray(
firstDefined(
payload && payload.entries,
payload && payload.rankings,
payload && payload.list,
payload && payload.records,
[]
)
);
}
function normalizeRankEntry(entry, index) {
entry = entry || {};
var privacyHidden =
entry.hidden === true ||
entry.anonymous === true ||
text(entry.privacyMode).toUpperCase() === 'INVISIBLE';
return {
rank: Math.max(
1,
toNumber(
firstDefined(entry.rank, entry.ranking, index + 1),
index + 1
)
),
userId: text(firstDefined(entry.userId, entry.uid, entry.id)),
displayId: text(
firstDefined(
entry.displayId,
entry.userDisplayId,
entry.shortId,
entry.account,
entry.uid
)
),
nickname: privacyHidden
? t('giftChallenge.anonymous', 'Anonymous')
: text(
firstDefined(
entry.nickname,
entry.userName,
entry.name,
t('giftChallenge.anonymous', 'Anonymous')
)
),
avatarUrl: privacyHidden
? ''
: safeImageURL(
firstDefined(
entry.avatarUrl,
entry.avatar,
entry.headImage
)
),
score: Math.max(
0,
toNumber(
firstDefined(entry.score, entry.points, entry.goldAmount),
0
)
),
isSelf: entry.isSelf === true || entry.me === true,
};
}
function avatar(entry, className) {
var wrap = createElement('span', className || 'rank-avatar-wrap');
if (entry.avatarUrl) {
var image = document.createElement('img');
image.className =
className === 'podium-avatar-wrap'
? 'podium-avatar'
: 'rank-avatar';
image.src = entry.avatarUrl;
image.alt = entry.nickname;
image.loading = 'lazy';
image.referrerPolicy = 'no-referrer';
wrap.appendChild(image);
} else {
wrap.appendChild(
createElement(
'span',
'avatar-fallback',
(entry.nickname || '?').slice(0, 1).toUpperCase()
)
);
}
return wrap;
}
function scoreNode(score, className) {
var wrapper = createElement('span', className || 'rank-score');
var coin = document.createElement('img');
coin.src = './aslan/assets/layers/coin.png';
coin.alt = '';
wrapper.appendChild(coin);
wrapper.appendChild(createElement('span', '', formatNumber(score)));
return wrapper;
}
function rankRow(entry) {
var item = createElement('li', 'rank-entry');
item.appendChild(createElement('span', 'rank-number', entry.rank));
item.appendChild(avatar(entry, 'rank-avatar-wrap'));
var user = createElement('span', 'rank-user');
user.appendChild(createElement('strong', '', entry.nickname));
user.appendChild(
createElement(
'small',
'',
t('giftChallenge.idPrefix', 'ID:') +
' ' +
(entry.displayId || '--')
)
);
item.appendChild(user);
item.appendChild(scoreNode(entry.score, 'rank-score'));
return item;
}
function podiumEntry(entry) {
var item = createElement('li', 'podium-entry');
var avatarWrap = avatar(entry, 'podium-avatar-wrap');
avatarWrap.dataset.rankLabel = 'TOP' + entry.rank;
item.appendChild(avatarWrap);
item.appendChild(createElement('span', 'podium-name', entry.nickname));
item.appendChild(scoreNode(entry.score, 'podium-score'));
return item;
}
function renderRanking(payload) {
var entries = rankingEntries(payload).map(normalizeRankEntry);
entries.sort(function (left, right) {
return left.rank - right.rank;
});
var podium = byId('podium');
var list = byId('rankList');
var empty = byId('rankEmpty');
podium.replaceChildren();
list.replaceChildren();
entries.slice(0, 3).forEach(function (entry) {
podium.appendChild(podiumEntry(entry));
});
entries.slice(3).forEach(function (entry) {
list.appendChild(rankRow(entry));
});
empty.hidden = entries.length > 0;
var self = firstDefined(
payload && payload.my,
payload && payload.myRanking,
payload && payload.myRank,
payload && payload.me
);
var selfRank = byId('selfRank');
selfRank.replaceChildren();
if (self && typeof self === 'object') {
var normalizedSelf = normalizeRankEntry(self, entries.length);
normalizedSelf.isSelf = true;
var row = rankRow(normalizedSelf);
while (row.firstChild) selfRank.appendChild(row.firstChild);
selfRank.hidden = false;
} else {
selfRank.hidden = true;
}
}
function rankLabel(tier) {
if (tier.startRank === tier.endRank) {
return t('giftChallenge.topSingle', 'Top {rank}', {
rank: tier.startRank,
}).replace('{rank}', tier.startRank);
}
return t('giftChallenge.topRange', 'Top {start}-{end}', {
start: tier.startRank,
end: tier.endRank,
})
.replace('{start}', tier.startRank)
.replace('{end}', tier.endRank);
}
function renderRewards() {
var list = byId('rewardTierList');
var empty = byId('rewardEmpty');
if (!list || !state.detail) return;
list.replaceChildren();
var tiers = state.detail.rankRewards.OVERALL.length
? state.detail.rankRewards.OVERALL
: state.detail.rankRewards.DAILY;
empty.hidden = tiers.length > 0;
tiers.forEach(function (tier) {
var card = createElement('article', 'reward-tier');
card.appendChild(createElement('h3', '', rankLabel(tier)));
var strip = createElement('div', 'reward-strip');
tier.rewards.slice(0, 3).forEach(function (reward) {
strip.appendChild(rewardSlot(reward));
});
card.appendChild(strip);
list.appendChild(card);
});
}
function setRankingState(mode) {
byId('rankingLoading').hidden = mode !== 'loading';
byId('rankingError').hidden = mode !== 'error';
byId('rankingContent').hidden = mode !== 'ready';
if (mode !== 'ready') byId('selfRank').hidden = true;
}
function loadRanking(force) {
var period = state.currentPeriod;
if (state.rankingLoading[period]) return state.rankingLoading[period];
if (!force && state.rankings[period]) {
renderRanking(state.rankings[period]);
setRankingState('ready');
return Promise.resolve(state.rankings[period]);
}
setRankingState('loading');
var request = Promise.resolve()
.then(function () {
return api().ranking(
state.activityId,
period,
period === 'DAILY' ? state.detail.statDate : '',
RANKING_LIMIT
);
})
.then(function (payload) {
state.rankings[period] = payload || {};
if (state.currentPeriod === period) {
renderRanking(payload || {});
setRankingState('ready');
}
return payload;
})
.catch(function (error) {
if (state.currentPeriod === period) setRankingState('error');
throw error;
})
.finally(function () {
delete state.rankingLoading[period];
});
state.rankingLoading[period] = request;
return request;
}
function updatePrimaryTabs() {
all('[data-view]', byId('primaryTabs')).forEach(function (button) {
var active = button.dataset.view === state.currentView;
button.classList.toggle('is-active', active);
button.setAttribute('aria-selected', active ? 'true' : 'false');
});
all('[data-view-panel]').forEach(function (panel) {
var active = panel.dataset.viewPanel === state.currentView;
panel.classList.toggle('is-active', active);
panel.hidden = !active;
});
}
function switchView(view) {
if (['tasks', 'ranking', 'rewards'].indexOf(view) < 0) return;
state.currentView = view;
updatePrimaryTabs();
if (view === 'ranking') {
loadRanking(false).catch(function () {});
}
if (view === 'rewards') renderRewards();
}
function switchPeriod(period) {
period = period === 'OVERALL' ? 'OVERALL' : 'DAILY';
state.currentPeriod = period;
all('[data-period]', byId('rankPeriodTabs')).forEach(function (button) {
var active = button.dataset.period === period;
button.classList.toggle('is-active', active);
button.setAttribute('aria-selected', active ? 'true' : 'false');
});
loadRanking(false).catch(function () {});
}
function updateCountdown() {
if (!state.detail) return;
var now = Date.now() + state.serverOffsetMs;
var target =
state.detail.startTime > now
? state.detail.startTime
: state.detail.endTime;
var remaining = Math.max(0, target - now);
var seconds = Math.floor(remaining / 1000);
var values = {
days: Math.floor(seconds / 86400),
hours: Math.floor((seconds % 86400) / 3600),
minutes: Math.floor((seconds % 3600) / 60),
seconds: seconds % 60,
};
Object.keys(values).forEach(function (key) {
var element = document.querySelector(
'[data-countdown="' + key + '"]'
);
if (!element) return;
var value = values[key];
// 天数单位由同格的 data-i18n small 节点负责strong 只写数值,避免
// 英文 D 重复且污染阿语、土语文案。
element.textContent =
key === 'days' ? String(value) : String(value).padStart(2, '0');
});
}
function startCountdown() {
window.clearInterval(state.countdownTimer);
updateCountdown();
state.countdownTimer = window.setInterval(updateCountdown, 1000);
}
function renderResultRewards(rewards) {
var target = byId('resultRewards');
target.replaceChildren();
asArray(rewards)
.slice(0, 3)
.forEach(function (reward) {
target.appendChild(rewardSlot(reward));
});
}
function openModal(id) {
var modal = byId(id);
if (!modal) return;
modal.hidden = false;
document.body.style.overflow = 'hidden';
var focusTarget = modal.querySelector('button');
if (focusTarget) focusTarget.focus();
}
function closeModal(id) {
var modal = byId(id);
if (!modal) return;
modal.hidden = true;
if (!document.querySelector('.modal:not([hidden])')) {
document.body.style.overflow = '';
}
}
function refreshDetail() {
return Promise.resolve(api().current()).then(function (payload) {
var refreshed = normalizeDetail(payload);
// 排行请求与领奖幂等键都以活动 ID 为边界。刷新若意外返回另一活动,
// 直接替换完整状态并清空缓存,避免把旧活动的名次显示在新周期。
if (state.activityId && refreshed.activityId !== state.activityId) {
state.rankings = {};
}
state.detail = refreshed;
state.activityId = refreshed.activityId;
state.serverOffsetMs = refreshed.serverTime - Date.now();
renderTasks();
renderRewards();
startCountdown();
return refreshed;
});
}
function applyUserState(result) {
if (!state.detail || !result || typeof result !== 'object') return [];
var tasks = firstDefined(result.tasks, result.taskList);
if (Array.isArray(tasks)) {
state.detail.tasks = normalizeTasks(tasks);
}
if (result.statDate) state.detail.statDate = text(result.statDate);
renderTasks();
return state.detail.tasks;
}
function claimTask(taskCode) {
if (!state.detail || state.claiming[taskCode]) return;
var task = state.detail.tasks.find(function (item) {
return item.code === taskCode;
});
if (!task || !task.claimable || task.claimed) return;
var requestId =
state.claimRequestIds[taskCode] ||
makeRequestId('claim:' + taskCode);
// 同一次点击遇到超时后必须复用 requestId只有服务端确认成功才删除
// 从而把“用户意图重试”和“再次领取”区分开。
state.claimRequestIds[taskCode] = requestId;
state.claiming[taskCode] = true;
renderTasks();
Promise.resolve(api().claimTask(state.activityId, taskCode, requestId))
.then(function (result) {
delete state.claimRequestIds[taskCode];
// claim 返回完整 UserState直接应用其中的用户任务快照不能再用
// /detail 的静态任务配置覆盖刚刚更新的进度和发奖状态。
var updatedTasks = applyUserState(result);
var updatedTask = updatedTasks.find(function (item) {
return item.code === taskCode;
});
if (updatedTask && updatedTask.claimed) {
renderResultRewards(updatedTask.rewards || task.rewards);
openModal('resultModal');
} else {
showToast(
t(
'giftChallenge.rewardPending',
'Reward delivery is being verified.'
)
);
}
})
.catch(function () {
showToast(
t(
'giftChallenge.claimFailed',
'Unable to claim the reward. Please try again.'
)
);
})
.finally(function () {
delete state.claiming[taskCode];
renderTasks();
});
}
function goToTask() {
showToast(
t('giftChallenge.completeInApp', 'Complete this task in the app.')
);
window.setTimeout(function () {
if (window.AslanCommon && window.AslanCommon.back) {
window.AslanCommon.back();
}
}, 350);
}
function recordDailyEntry() {
if (!state.activityId) return Promise.resolve();
return Promise.resolve(api().enter(state.activityId))
.then(function (result) {
var tasks = firstDefined(
result && result.tasks,
result && result.taskList
);
if (Array.isArray(tasks)) {
applyUserState(result);
} else {
// enter 可能只返回成功标记;后台仍是任务状态唯一真相,轻量
// 再读 detail 以免页面继续显示进入任务未完成。
return refreshDetail();
}
})
.catch(function () {
// 进入上报失败不应让排行榜和奖励整页不可用;下次打开由服务端
// 活动日任务唯一键继续幂等补记。
});
}
function setPageState(mode) {
byId('pageLoading').hidden = mode !== 'loading';
byId('pageError').hidden = mode !== 'error';
byId('tasksContent').hidden = mode !== 'ready';
}
function loadPage() {
setPageState('loading');
return refreshDetail()
.then(function () {
if (!state.activityId) throw new Error('activity_not_found');
setPageState('ready');
return recordDailyEntry();
})
.catch(function () {
setPageState('error');
});
}
function bindEvents() {
byId('primaryTabs').addEventListener('click', function (event) {
var button = event.target.closest('[data-view]');
if (button) switchView(button.dataset.view);
});
byId('rankPeriodTabs').addEventListener('click', function (event) {
var button = event.target.closest('[data-period]');
if (button) switchPeriod(button.dataset.period);
});
byId('taskList').addEventListener('click', function (event) {
var button = event.target.closest('[data-action]');
if (!button) return;
if (button.dataset.action === 'claim') {
claimTask(button.dataset.taskCode);
} else if (button.dataset.action === 'go') {
goToTask();
}
});
byId('ruleButton').addEventListener('click', function () {
openModal('ruleModal');
});
byId('rewardButton').addEventListener('click', function () {
switchView('rewards');
});
byId('rewardBack').addEventListener('click', function () {
switchView('ranking');
});
byId('retryButton').addEventListener('click', loadPage);
byId('rankingRetry').addEventListener('click', function () {
loadRanking(true).catch(function () {});
});
document.addEventListener('click', function (event) {
var closer = event.target.closest('[data-close-modal]');
if (closer) closeModal(closer.dataset.closeModal);
});
document.addEventListener('keydown', function (event) {
if (event.key !== 'Escape') return;
all('.modal:not([hidden])').forEach(function (modal) {
closeModal(modal.id);
});
});
window.addEventListener('hyapp:i18n-ready', function () {
renderTasks();
renderRewards();
Object.keys(state.rankings).forEach(function (period) {
if (period === state.currentPeriod) {
renderRanking(state.rankings[period]);
}
});
});
}
function init() {
bindEvents();
updatePrimaryTabs();
loadPage();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
})();