873 lines
30 KiB
JavaScript
873 lines
30 KiB
JavaScript
(function () {
|
||
'use strict';
|
||
|
||
var DESIGN_WIDTH = 1080;
|
||
var DESIGN_HEIGHTS = {
|
||
history: 2651,
|
||
weekly: 2460,
|
||
rewards: 3532,
|
||
};
|
||
// Weekly Star 的公开 DTO 只提供 UTC epoch,不携带展示时区;Fami 设计与产品文案明确固定 UTC+3,
|
||
// 因此集中维护 +180 分钟展示规则,不能根据浏览器本地时区推断,否则同一周期会在不同国家显示不同日期。
|
||
var DISPLAY_OFFSET_MINUTES = 180;
|
||
var MAX_VISIBLE_RANKS = 5;
|
||
var ASSET_BASE = './fami/assets/';
|
||
var LAYER_BASE = ASSET_BASE + 'layers/';
|
||
var FIXTURE_BASE = ASSET_BASE + 'fixtures/';
|
||
|
||
var root = document.getElementById('famiWeekStar');
|
||
var stageWrap = document.getElementById('famiStageWrap');
|
||
var cycleRangeNode = document.getElementById('famiCycleRange');
|
||
var historyRangeNode = document.getElementById('famiHistoryRange');
|
||
var giftListNode = document.getElementById('famiGiftList');
|
||
var weeklyListNode = document.getElementById('famiWeeklyList');
|
||
var historyListNode = document.getElementById('famiHistoryList');
|
||
var rewardGroupsNode = document.getElementById('famiRewardGroups');
|
||
var tabs = array(document.querySelectorAll('[data-tab-target]'));
|
||
var panels = array(document.querySelectorAll('[role="tabpanel"]'));
|
||
var giftTemplate = document.getElementById('famiGiftTemplate');
|
||
var rankTemplate = document.getElementById('famiRankTemplate');
|
||
var rewardGroupTemplate = document.getElementById(
|
||
'famiRewardGroupTemplate'
|
||
);
|
||
var rewardItemTemplate = document.getElementById('famiRewardItemTemplate');
|
||
var params = new URLSearchParams(window.location.search || '');
|
||
// 设计示例头像/礼物只在显式 mock=1 的验收入口使用;生产接口失败必须展示错误态,不能拿示例业务媒体伪装真实数据。
|
||
var useMock = params.get('mock') === '1';
|
||
|
||
var state = {
|
||
tab: normalizeTab(params.get('tab') || window.location.hash.slice(1)),
|
||
currentLoading: true,
|
||
weeklyLoading: true,
|
||
historyLoading: true,
|
||
current: null,
|
||
weeklyEntries: [],
|
||
historyCycles: [],
|
||
currentError: '',
|
||
historyError: '',
|
||
};
|
||
|
||
function array(value) {
|
||
return Array.prototype.slice.call(value || []);
|
||
}
|
||
|
||
function text(value) {
|
||
return String(
|
||
value === undefined || value === null ? '' : value
|
||
).trim();
|
||
}
|
||
|
||
function number(value, fallback) {
|
||
var result = Number(value);
|
||
return Number.isFinite(result) ? result : Number(fallback || 0);
|
||
}
|
||
|
||
function first(source, keys, fallback) {
|
||
source = source || {};
|
||
for (var index = 0; index < keys.length; index += 1) {
|
||
var value = source[keys[index]];
|
||
if (value !== undefined && value !== null && value !== '') {
|
||
return value;
|
||
}
|
||
}
|
||
return fallback;
|
||
}
|
||
|
||
function firstObject(source, keys) {
|
||
var value = first(source, keys, null);
|
||
return value && typeof value === 'object' ? value : {};
|
||
}
|
||
|
||
function firstArray(source, keys) {
|
||
var value = first(source, keys, null);
|
||
return Array.isArray(value) ? value : [];
|
||
}
|
||
|
||
function t(key, fallback) {
|
||
if (window.HyAppI18n && window.HyAppI18n.t) {
|
||
return window.HyAppI18n.t(key, fallback);
|
||
}
|
||
return fallback || key;
|
||
}
|
||
|
||
function formatMessage(key, fallback, values) {
|
||
var result = t(key, fallback);
|
||
Object.keys(values || {}).forEach(function (name) {
|
||
result = result.replace(
|
||
new RegExp('\\{' + name + '\\}', 'g'),
|
||
String(values[name])
|
||
);
|
||
});
|
||
return result;
|
||
}
|
||
|
||
function currentLocale() {
|
||
return window.HyAppI18n && window.HyAppI18n.lang
|
||
? window.HyAppI18n.lang()
|
||
: 'en';
|
||
}
|
||
|
||
function formatNumber(value) {
|
||
try {
|
||
return new Intl.NumberFormat(currentLocale()).format(
|
||
number(value, 0)
|
||
);
|
||
} catch (error) {
|
||
return String(number(value, 0));
|
||
}
|
||
}
|
||
|
||
function formatScore(value) {
|
||
// 设计稿的榜单分数是不带千分位的原始整数;不能复用奖励数量的本地化格式,否则会变成 337,700。
|
||
return String(Math.trunc(number(value, 0)));
|
||
}
|
||
|
||
function pad(value) {
|
||
return String(value).padStart(2, '0');
|
||
}
|
||
|
||
function shiftedDate(value) {
|
||
var timestamp = number(value, 0);
|
||
if (!timestamp) return null;
|
||
return new Date(timestamp + DISPLAY_OFFSET_MINUTES * 60 * 1000);
|
||
}
|
||
|
||
function formatCycleRange(start, end) {
|
||
var from = shiftedDate(start);
|
||
var to = shiftedDate(end);
|
||
if (!from || !to) return '--.-- --:-----.-- --:--(utc+3)';
|
||
return (
|
||
pad(from.getUTCMonth() + 1) +
|
||
'.' +
|
||
pad(from.getUTCDate()) +
|
||
' ' +
|
||
pad(from.getUTCHours()) +
|
||
':' +
|
||
pad(from.getUTCMinutes()) +
|
||
'-' +
|
||
pad(to.getUTCMonth() + 1) +
|
||
'.' +
|
||
pad(to.getUTCDate()) +
|
||
' ' +
|
||
pad(to.getUTCHours()) +
|
||
':' +
|
||
pad(to.getUTCMinutes()) +
|
||
'(utc+3)'
|
||
);
|
||
}
|
||
|
||
function formatHistoryRange(start, end) {
|
||
var from = shiftedDate(start);
|
||
var to = shiftedDate(end);
|
||
if (!from || !to) return '----/--/-------/--/--';
|
||
return (
|
||
from.getUTCFullYear() +
|
||
'/' +
|
||
pad(from.getUTCMonth() + 1) +
|
||
'/' +
|
||
pad(from.getUTCDate()) +
|
||
'-' +
|
||
to.getUTCFullYear() +
|
||
'/' +
|
||
pad(to.getUTCMonth() + 1) +
|
||
'/' +
|
||
pad(to.getUTCDate())
|
||
);
|
||
}
|
||
|
||
function normalizeTab(value) {
|
||
if (value === 'weekly') return 'weekly';
|
||
if (value === 'reward' || value === 'rewards') return 'rewards';
|
||
return 'history';
|
||
}
|
||
|
||
function updateStageSize() {
|
||
var scale = stageWrap.clientWidth / DESIGN_WIDTH;
|
||
var height = DESIGN_HEIGHTS[state.tab] || DESIGN_HEIGHTS.history;
|
||
stageWrap.style.setProperty('--design-scale', String(scale));
|
||
stageWrap.style.height = Math.ceil(height * scale) + 'px';
|
||
document.body.style.minHeight = stageWrap.offsetHeight + 'px';
|
||
}
|
||
|
||
function updateURL(tab) {
|
||
if (!window.history || !window.history.replaceState) return;
|
||
try {
|
||
var url = new URL(window.location.href);
|
||
url.searchParams.set('tab', tab);
|
||
url.hash = '';
|
||
window.history.replaceState(null, '', url.toString());
|
||
} catch (error) {
|
||
// 受限 WebView 可能禁用 History API;Tab 本身仍可正常切换,不让 URL 同步失败阻断页面。
|
||
}
|
||
}
|
||
|
||
function setTab(nextTab, syncURL) {
|
||
var next = normalizeTab(nextTab);
|
||
state.tab = next;
|
||
root.setAttribute('data-tab', next);
|
||
tabs.forEach(function (tab) {
|
||
var active = tab.getAttribute('data-tab-target') === next;
|
||
tab.classList.toggle('is-active', active);
|
||
tab.setAttribute('aria-selected', active ? 'true' : 'false');
|
||
tab.setAttribute('tabindex', active ? '0' : '-1');
|
||
});
|
||
panels.forEach(function (panel) {
|
||
panel.hidden =
|
||
panel.id !==
|
||
'famiPanel' + next.charAt(0).toUpperCase() + next.slice(1);
|
||
});
|
||
updateStageSize();
|
||
if (syncURL) updateURL(next);
|
||
}
|
||
|
||
function imageURL(source) {
|
||
var resource = firstObject(source, ['resource']);
|
||
return text(
|
||
first(
|
||
source,
|
||
[
|
||
'image_url',
|
||
'imageUrl',
|
||
'cover_url',
|
||
'coverUrl',
|
||
'preview_url',
|
||
'previewUrl',
|
||
'asset_url',
|
||
'assetUrl',
|
||
'animation_url',
|
||
'animationUrl',
|
||
'icon_url',
|
||
'iconUrl',
|
||
'image',
|
||
],
|
||
first(
|
||
resource,
|
||
[
|
||
'preview_url',
|
||
'previewUrl',
|
||
'asset_url',
|
||
'assetUrl',
|
||
'animation_url',
|
||
'animationUrl',
|
||
'image_url',
|
||
'imageUrl',
|
||
],
|
||
''
|
||
)
|
||
)
|
||
);
|
||
}
|
||
|
||
function setImage(node, url, alt) {
|
||
node.alt = text(alt);
|
||
if (!text(url)) {
|
||
node.removeAttribute('src');
|
||
return;
|
||
}
|
||
node.src = text(url);
|
||
node.onerror = function () {
|
||
// 资源 CDN 失败时移除坏图标,由同一独立卡位内的 CSS 占位层接管,不能回退到设计示例业务媒体。
|
||
node.removeAttribute('src');
|
||
};
|
||
}
|
||
|
||
function initial(value) {
|
||
var normalized = text(value);
|
||
return normalized ? normalized.charAt(0) : '?';
|
||
}
|
||
|
||
function normalizeEntry(entry, index) {
|
||
entry = entry || {};
|
||
var user = firstObject(entry, ['user']);
|
||
return {
|
||
rank: number(
|
||
first(entry, ['rank_no', 'rankNo', 'rank'], index + 1),
|
||
index + 1
|
||
),
|
||
name: text(
|
||
first(
|
||
user,
|
||
['username', 'display_user_id', 'displayUserId'],
|
||
first(entry, ['user_id', 'userId'], '')
|
||
)
|
||
),
|
||
score: number(first(entry, ['score'], 0), 0),
|
||
avatar: text(
|
||
first(user, ['avatar', 'avatar_url', 'avatarUrl'], '')
|
||
),
|
||
};
|
||
}
|
||
|
||
function normalizeEntries(entries) {
|
||
return (Array.isArray(entries) ? entries : []).map(normalizeEntry);
|
||
}
|
||
|
||
function rankMedal(rank) {
|
||
if (rank === 1) return LAYER_BASE + 'medal-1.webp';
|
||
if (rank === 2) return LAYER_BASE + 'medal-2.webp';
|
||
if (rank === 3) return LAYER_BASE + 'medal-3.webp';
|
||
return '';
|
||
}
|
||
|
||
function renderListState(target, message) {
|
||
target.innerHTML = '';
|
||
var stateNode = document.createElement('div');
|
||
stateNode.className = 'fami-list-state';
|
||
stateNode.textContent = message;
|
||
target.appendChild(stateNode);
|
||
}
|
||
|
||
function renderRankList(
|
||
target,
|
||
entries,
|
||
emptyMessage,
|
||
errorMessage,
|
||
loading
|
||
) {
|
||
if (loading) {
|
||
renderListState(target, t('fami_week_star.loading', 'Loading...'));
|
||
return;
|
||
}
|
||
if (errorMessage) {
|
||
renderListState(
|
||
target,
|
||
t('fami_week_star.load_failed', 'Unable to load rankings.')
|
||
);
|
||
return;
|
||
}
|
||
|
||
var rows = normalizeEntries(entries).slice(0, MAX_VISIBLE_RANKS);
|
||
if (!rows.length) {
|
||
renderListState(target, emptyMessage);
|
||
return;
|
||
}
|
||
|
||
target.innerHTML = '';
|
||
rows.forEach(function (entry) {
|
||
var fragment = rankTemplate.content.cloneNode(true);
|
||
var row = fragment.querySelector('.fami-rank-row');
|
||
var medal = fragment.querySelector('.fami-rank-medal');
|
||
var rankNumber = fragment.querySelector('.fami-rank-number');
|
||
var avatar = fragment.querySelector('.fami-rank-avatar img');
|
||
var avatarPlaceholder = fragment.querySelector(
|
||
'.fami-avatar-placeholder'
|
||
);
|
||
var name = fragment.querySelector('.fami-rank-name');
|
||
var score = fragment.querySelector('.fami-rank-score');
|
||
var displayName =
|
||
entry.name || t('fami_week_star.unknown', 'Unknown');
|
||
|
||
setImage(
|
||
medal,
|
||
rankMedal(entry.rank),
|
||
formatMessage('fami_week_star.rank_aria', 'Rank {rank}', {
|
||
rank: entry.rank,
|
||
})
|
||
);
|
||
rankNumber.textContent = String(entry.rank);
|
||
setImage(avatar, entry.avatar, displayName);
|
||
avatarPlaceholder.textContent = initial(displayName);
|
||
name.textContent = displayName;
|
||
score.textContent = formatScore(entry.score);
|
||
row.setAttribute(
|
||
'aria-label',
|
||
formatMessage(
|
||
'fami_week_star.entry_aria',
|
||
'Rank {rank}, {name}, {score}',
|
||
{
|
||
rank: entry.rank,
|
||
name: displayName,
|
||
score: formatScore(entry.score),
|
||
}
|
||
)
|
||
);
|
||
target.appendChild(fragment);
|
||
});
|
||
}
|
||
|
||
function normalizeGift(gift) {
|
||
gift = gift || {};
|
||
var resource = firstObject(gift, ['resource']);
|
||
return {
|
||
name: text(
|
||
first(
|
||
gift,
|
||
['name'],
|
||
first(resource, ['name'], t('fami_week_star.gift', 'Gift'))
|
||
)
|
||
),
|
||
image: imageURL(gift),
|
||
};
|
||
}
|
||
|
||
function renderGifts(gifts, stateMessage) {
|
||
var normalized = (Array.isArray(gifts) ? gifts : [])
|
||
.map(normalizeGift)
|
||
.slice(0, 3);
|
||
giftListNode.innerHTML = '';
|
||
giftListNode.classList.toggle('has-state', Boolean(stateMessage));
|
||
|
||
if (stateMessage) {
|
||
var stateNode = document.createElement('div');
|
||
stateNode.className = 'fami-current-state';
|
||
stateNode.setAttribute('role', 'status');
|
||
stateNode.textContent = stateMessage;
|
||
giftListNode.appendChild(stateNode);
|
||
return;
|
||
}
|
||
|
||
for (var index = 0; index < 3; index += 1) {
|
||
var gift = normalized[index] || {
|
||
name: t('fami_week_star.gift', 'Gift'),
|
||
image: '',
|
||
};
|
||
var fragment = giftTemplate.content.cloneNode(true);
|
||
var card = fragment.querySelector('.fami-gift-card');
|
||
var image = fragment.querySelector('.fami-gift-media img');
|
||
fragment.querySelector('[data-gift-name]').textContent = gift.name;
|
||
setImage(image, gift.image, gift.name);
|
||
card.setAttribute('aria-label', gift.name);
|
||
giftListNode.appendChild(fragment);
|
||
}
|
||
}
|
||
|
||
function rewardImage(item) {
|
||
return imageURL(item);
|
||
}
|
||
|
||
function rewardLabel(item, group) {
|
||
item = item || {};
|
||
group = group || {};
|
||
var resource = firstObject(item, ['resource']);
|
||
var walletType = text(
|
||
first(item, ['wallet_asset_type', 'walletAssetType'], '')
|
||
).toUpperCase();
|
||
var label = text(
|
||
first(
|
||
item,
|
||
['label', 'name'],
|
||
first(resource, ['name'], first(group, ['name'], ''))
|
||
)
|
||
);
|
||
if (!label && walletType === 'COIN') {
|
||
label = t('fami_week_star.coins', 'Coins');
|
||
}
|
||
if (!label) label = t('fami_week_star.reward', 'Reward');
|
||
|
||
var durationMS = number(
|
||
first(item, ['duration_ms', 'durationMs'], 0),
|
||
0
|
||
);
|
||
var days = Math.round(durationMS / 86400000);
|
||
if (durationMS > 0 && days > 0) {
|
||
label = formatMessage(
|
||
'fami_week_star.reward_days',
|
||
'{name} ({days} days)',
|
||
{ name: label, days: days }
|
||
);
|
||
}
|
||
|
||
var quantity = number(first(item, ['quantity'], 0), 0);
|
||
var walletAmount = number(
|
||
first(item, ['wallet_asset_amount', 'walletAssetAmount'], 0),
|
||
0
|
||
);
|
||
if (quantity > 1) label += ' ×' + formatNumber(quantity);
|
||
else if (walletAmount > 0) label += ' ×' + formatNumber(walletAmount);
|
||
return label;
|
||
}
|
||
|
||
function normalizeRewardGroups(rewards) {
|
||
var source = Array.isArray(rewards) ? rewards : [];
|
||
return [1, 2, 3].map(function (rank) {
|
||
var reward = source.find(function (candidate) {
|
||
return (
|
||
number(
|
||
first(candidate, ['rank_no', 'rankNo', 'rank'], 0),
|
||
0
|
||
) === rank
|
||
);
|
||
});
|
||
reward = reward || {};
|
||
var group = firstObject(reward, [
|
||
'resource_group',
|
||
'resourceGroup',
|
||
'reward_resource_group',
|
||
'rewardResourceGroup',
|
||
]);
|
||
var items = firstArray(reward, ['items', 'resources']);
|
||
if (!items.length)
|
||
items = firstArray(group, ['items', 'resources']);
|
||
return {
|
||
rank: rank,
|
||
group: group,
|
||
items: items.slice(0, 3),
|
||
};
|
||
});
|
||
}
|
||
|
||
function renderRewards(rewards, stateMessage) {
|
||
rewardGroupsNode.innerHTML = '';
|
||
var groups = normalizeRewardGroups(rewards);
|
||
if (
|
||
!stateMessage &&
|
||
!groups.some(function (groupData) {
|
||
return groupData.items.length > 0;
|
||
})
|
||
) {
|
||
stateMessage = t(
|
||
'fami_week_star.empty_rewards',
|
||
'No reward data yet.'
|
||
);
|
||
}
|
||
if (stateMessage) {
|
||
var stateNode = document.createElement('div');
|
||
stateNode.className = 'fami-reward-state';
|
||
stateNode.setAttribute('role', 'status');
|
||
stateNode.textContent = stateMessage;
|
||
rewardGroupsNode.appendChild(stateNode);
|
||
return;
|
||
}
|
||
groups.forEach(function (groupData) {
|
||
var groupFragment = rewardGroupTemplate.content.cloneNode(true);
|
||
var groupNode = groupFragment.querySelector('.fami-reward-group');
|
||
var heading = groupFragment.querySelector(
|
||
'.fami-reward-heading h2'
|
||
);
|
||
var itemsNode = groupFragment.querySelector('.fami-reward-items');
|
||
heading.textContent = 'TOP' + groupData.rank;
|
||
itemsNode.setAttribute(
|
||
'data-count',
|
||
String(groupData.items.length)
|
||
);
|
||
|
||
groupData.items.forEach(function (item) {
|
||
var itemFragment = rewardItemTemplate.content.cloneNode(true);
|
||
var itemNode = itemFragment.querySelector('.fami-reward-item');
|
||
var image = itemFragment.querySelector(
|
||
'.fami-reward-media img'
|
||
);
|
||
var label = rewardLabel(item, groupData.group);
|
||
setImage(image, rewardImage(item), label);
|
||
itemFragment.querySelector('.fami-reward-label').textContent =
|
||
label;
|
||
itemNode.setAttribute('aria-label', label);
|
||
itemsNode.appendChild(itemFragment);
|
||
});
|
||
groupNode.setAttribute(
|
||
'aria-label',
|
||
formatMessage(
|
||
'fami_week_star.top_rewards_aria',
|
||
'Top {rank} rewards',
|
||
{ rank: groupData.rank }
|
||
)
|
||
);
|
||
rewardGroupsNode.appendChild(groupFragment);
|
||
});
|
||
}
|
||
|
||
function cycleOf(value) {
|
||
return firstObject(value, ['cycle']);
|
||
}
|
||
|
||
function render() {
|
||
var current = state.current || {};
|
||
var cycle = cycleOf(current);
|
||
var history = state.historyCycles[0] || {};
|
||
var historyCycle = cycleOf(history);
|
||
var currentGifts = firstArray(cycle, ['gifts']);
|
||
var currentRewards = firstArray(cycle, ['rewards']);
|
||
var historyEntries = firstArray(history, [
|
||
'top_entries',
|
||
'topEntries',
|
||
'entries',
|
||
]);
|
||
var hasCycle = Boolean(
|
||
text(first(cycle, ['cycle_id', 'cycleId'], '')) ||
|
||
number(first(cycle, ['start_ms', 'startMs'], 0), 0)
|
||
);
|
||
var currentStateMessage = '';
|
||
if (state.currentLoading) {
|
||
currentStateMessage = t('fami_week_star.loading', 'Loading...');
|
||
} else if (state.currentError) {
|
||
currentStateMessage = t(
|
||
'fami_week_star.activity_load_failed',
|
||
'Unable to load Weekly Star.'
|
||
);
|
||
} else if (!hasCycle) {
|
||
currentStateMessage = t(
|
||
'fami_week_star.no_active_cycle',
|
||
'No active cycle.'
|
||
);
|
||
}
|
||
var rewardStateMessage = currentStateMessage;
|
||
if (!rewardStateMessage && !currentRewards.length) {
|
||
rewardStateMessage = t(
|
||
'fami_week_star.empty_rewards',
|
||
'No reward data yet.'
|
||
);
|
||
}
|
||
|
||
cycleRangeNode.textContent = formatCycleRange(
|
||
first(cycle, ['start_ms', 'startMs'], 0),
|
||
first(cycle, ['end_ms', 'endMs'], 0)
|
||
);
|
||
historyRangeNode.textContent = formatHistoryRange(
|
||
first(historyCycle, ['start_ms', 'startMs'], 0),
|
||
first(historyCycle, ['end_ms', 'endMs'], 0)
|
||
);
|
||
renderGifts(currentGifts, currentStateMessage);
|
||
renderRankList(
|
||
weeklyListNode,
|
||
state.weeklyEntries,
|
||
t('fami_week_star.empty_weekly', 'No ranking data yet.'),
|
||
state.currentError,
|
||
state.weeklyLoading
|
||
);
|
||
renderRankList(
|
||
historyListNode,
|
||
historyEntries,
|
||
t('fami_week_star.empty_history', 'No previous ranking records.'),
|
||
state.historyError,
|
||
state.historyLoading
|
||
);
|
||
renderRewards(currentRewards, rewardStateMessage);
|
||
}
|
||
|
||
function mockData() {
|
||
var start = Date.UTC(2026, 4, 27, 21, 0, 0);
|
||
var end = Date.UTC(2026, 5, 5, 20, 59, 0);
|
||
var weekly = [
|
||
{
|
||
rank_no: 1,
|
||
score: 337700,
|
||
user: {
|
||
username: 'hanneem',
|
||
avatar: FIXTURE_BASE + 'mock-avatar-top.webp',
|
||
},
|
||
},
|
||
{
|
||
rank_no: 2,
|
||
score: 337700,
|
||
user: {
|
||
username: 'hanneem',
|
||
avatar: FIXTURE_BASE + 'mock-avatar-user.webp',
|
||
},
|
||
},
|
||
{
|
||
rank_no: 3,
|
||
score: 337700,
|
||
user: {
|
||
username: 'hanneem',
|
||
avatar: FIXTURE_BASE + 'mock-avatar-user.webp',
|
||
},
|
||
},
|
||
{
|
||
rank_no: 4,
|
||
score: 337700,
|
||
user: {
|
||
username: 'hanneem',
|
||
avatar: FIXTURE_BASE + 'mock-avatar-user.webp',
|
||
},
|
||
},
|
||
{
|
||
rank_no: 5,
|
||
score: 337700,
|
||
user: {
|
||
username: 'hanneem',
|
||
avatar: FIXTURE_BASE + 'mock-avatar-user.webp',
|
||
},
|
||
},
|
||
];
|
||
var rewardItems = [1, 2, 3].map(function () {
|
||
return {
|
||
name: t('fami_week_star.mock_reward_name', 'Frame'),
|
||
duration_ms: 7 * 86400000,
|
||
image: FIXTURE_BASE + 'mock-gift.webp',
|
||
};
|
||
});
|
||
return {
|
||
current: {
|
||
cycle: {
|
||
cycle_id: 'mock-fami-week-star',
|
||
start_ms: start,
|
||
end_ms: end,
|
||
gifts: [1, 2, 3].map(function () {
|
||
return {
|
||
name: t('fami_week_star.gift', 'Gift'),
|
||
image: FIXTURE_BASE + 'mock-gift.webp',
|
||
};
|
||
}),
|
||
rewards: [1, 2, 3].map(function (rank) {
|
||
return {
|
||
rank_no: rank,
|
||
resource_group: { items: rewardItems },
|
||
};
|
||
}),
|
||
},
|
||
top_entries: weekly,
|
||
server_time_ms: Date.now(),
|
||
},
|
||
weekly: weekly,
|
||
history: [
|
||
{
|
||
cycle: {
|
||
start_ms: Date.UTC(2025, 11, 6, 21, 0, 0),
|
||
end_ms: Date.UTC(2025, 11, 14, 20, 59, 0),
|
||
},
|
||
top_entries: weekly,
|
||
},
|
||
],
|
||
};
|
||
}
|
||
|
||
function errorText(error) {
|
||
return text(error && error.message) || 'request_failed';
|
||
}
|
||
|
||
function loadMock() {
|
||
var mock = mockData();
|
||
state.currentLoading = false;
|
||
state.weeklyLoading = false;
|
||
state.historyLoading = false;
|
||
state.current = mock.current;
|
||
state.weeklyEntries = mock.weekly;
|
||
state.historyCycles = mock.history;
|
||
state.currentError = '';
|
||
state.historyError = '';
|
||
render();
|
||
}
|
||
|
||
function loadReal() {
|
||
var api = window.HyAppAPI && window.HyAppAPI.weeklyStar;
|
||
state.currentLoading = true;
|
||
state.weeklyLoading = true;
|
||
state.historyLoading = true;
|
||
state.currentError = '';
|
||
state.historyError = '';
|
||
render();
|
||
|
||
if (!api) {
|
||
state.currentLoading = false;
|
||
state.weeklyLoading = false;
|
||
state.historyLoading = false;
|
||
state.currentError = 'api_unavailable';
|
||
state.historyError = 'api_unavailable';
|
||
render();
|
||
return;
|
||
}
|
||
|
||
// history 与 current/leaderboard 使用独立 loading/error 状态;慢榜单不能阻塞已经成功的历史记录,任一接口失败也不清空另一块数据。
|
||
api.history(10).then(
|
||
function (data) {
|
||
state.historyCycles = firstArray(data || {}, ['cycles']);
|
||
state.historyError = '';
|
||
state.historyLoading = false;
|
||
render();
|
||
},
|
||
function (error) {
|
||
state.historyCycles = [];
|
||
state.historyError = errorText(error);
|
||
state.historyLoading = false;
|
||
render();
|
||
}
|
||
);
|
||
|
||
api.current()
|
||
.then(function (data) {
|
||
state.current = data || {};
|
||
state.currentError = '';
|
||
state.currentLoading = false;
|
||
// current 一到就先展示周期、礼物和奖励,不等待只影响本周榜的 leaderboard。
|
||
render();
|
||
|
||
var embedded = firstArray(state.current, [
|
||
'top_entries',
|
||
'topEntries',
|
||
]);
|
||
var cycle = cycleOf(state.current);
|
||
var cycleID = text(first(cycle, ['cycle_id', 'cycleId'], ''));
|
||
if (!cycleID) return embedded;
|
||
|
||
// current.top_entries 可直接兜底;leaderboard 额外请求只取设计允许展示的前 5 名。
|
||
// 排行接口独立失败时保留 current 数据,而不是把整个活动判为不可用。
|
||
return api
|
||
.leaderboard({
|
||
cycle_id: cycleID,
|
||
page_size: MAX_VISIBLE_RANKS,
|
||
})
|
||
.then(function (data) {
|
||
var entries = firstArray(data || {}, ['entries']);
|
||
return entries.length ? entries : embedded;
|
||
})
|
||
.catch(function () {
|
||
return embedded;
|
||
});
|
||
})
|
||
.then(function (entries) {
|
||
state.weeklyEntries = Array.isArray(entries) ? entries : [];
|
||
state.weeklyLoading = false;
|
||
render();
|
||
})
|
||
.catch(function (error) {
|
||
// current 失败时周期、礼物、本周榜和奖励统一进入明确错误态;历史请求仍独立完成并保留可用结果。
|
||
state.current = null;
|
||
state.currentLoading = false;
|
||
state.weeklyLoading = false;
|
||
state.currentError = errorText(error);
|
||
state.weeklyEntries = [];
|
||
render();
|
||
});
|
||
}
|
||
|
||
function bindTabs() {
|
||
tabs.forEach(function (tab, index) {
|
||
tab.addEventListener('click', function () {
|
||
setTab(tab.getAttribute('data-tab-target'), true);
|
||
});
|
||
tab.addEventListener('keydown', function (event) {
|
||
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') {
|
||
return;
|
||
}
|
||
event.preventDefault();
|
||
var direction = event.key === 'ArrowRight' ? 1 : -1;
|
||
if (document.documentElement.dir === 'rtl') direction *= -1;
|
||
var nextIndex = (index + direction + tabs.length) % tabs.length;
|
||
tabs[nextIndex].focus();
|
||
setTab(tabs[nextIndex].getAttribute('data-tab-target'), true);
|
||
});
|
||
});
|
||
}
|
||
|
||
function bindResize() {
|
||
var scheduled = false;
|
||
function schedule() {
|
||
if (scheduled) return;
|
||
scheduled = true;
|
||
window.requestAnimationFrame(function () {
|
||
scheduled = false;
|
||
updateStageSize();
|
||
});
|
||
}
|
||
window.addEventListener('resize', schedule);
|
||
window.addEventListener('orientationchange', schedule);
|
||
if (window.ResizeObserver) {
|
||
new ResizeObserver(schedule).observe(stageWrap);
|
||
}
|
||
}
|
||
|
||
function init() {
|
||
bindTabs();
|
||
bindResize();
|
||
setTab(state.tab, false);
|
||
window.addEventListener('hyapp:i18n-ready', function () {
|
||
// common/i18n 已更新静态 data-i18n 节点;这里只重算动态 API 文案与数字,避免重复 apply 触发事件循环。
|
||
render();
|
||
});
|
||
if (useMock) loadMock();
|
||
else loadReal();
|
||
}
|
||
|
||
init();
|
||
})();
|