(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(); } })();