965 lines
34 KiB
JavaScript
965 lines
34 KiB
JavaScript
(function () {
|
||
'use strict';
|
||
|
||
var DESIGN_WIDTH = 1080;
|
||
var MAX_PAGE_WIDTH = 768;
|
||
var ASSET_BASE = './huwaa/assets/layers/';
|
||
var PODIUM_ASSETS = {
|
||
gold: 'podium-gold.webp',
|
||
silver: 'podium-silver.webp',
|
||
bronze: 'podium-bronze.webp',
|
||
};
|
||
var CROWN_ASSETS = {
|
||
1: 'crown-gold.webp',
|
||
2: 'crown-silver.webp',
|
||
3: 'crown-bronze.webp',
|
||
};
|
||
var STAGE_HEIGHTS = {
|
||
plaza: 4283,
|
||
rank: 3733,
|
||
rewards: 4782,
|
||
};
|
||
var PANEL_LAYOUT = {
|
||
plazaFirstTop: 1677,
|
||
plazaFirstHeight: 1168,
|
||
plazaSecondTop: 2904,
|
||
plazaSecondHeight: 1142,
|
||
rankPanelTop: 2484,
|
||
rankPanelHeight: 1142,
|
||
rewardFirstTop: 1677,
|
||
rewardPanelHeight: 960,
|
||
rewardPanelGap: 56,
|
||
};
|
||
|
||
var viewport = document.getElementById('huwaaCpViewport');
|
||
var page = document.getElementById('huwaaCpPage');
|
||
var stage = document.getElementById('huwaaCpStage');
|
||
var stageWrap = document.getElementById('huwaaCpStageWrap');
|
||
var tabContent = document.getElementById('huwaaCpTabContent');
|
||
var copyright = document.querySelector('.huwaa-cp-copyright');
|
||
var announcement = document.getElementById('huwaaCpAnnouncement');
|
||
var tabs = Array.prototype.slice.call(
|
||
document.querySelectorAll('.huwaa-cp-tab')
|
||
);
|
||
|
||
var requestGeneration = 0;
|
||
var countdownEndAt = 0;
|
||
var countdownTimer = 0;
|
||
var serverTimeOffsetMS = 0;
|
||
var activeTab = resolveInitialTab();
|
||
var activityData = {
|
||
statusState: 'loading',
|
||
plazaState: 'loading',
|
||
rank: [],
|
||
previousRows: [],
|
||
plazaRows: [],
|
||
rewards: [],
|
||
enabled: true,
|
||
};
|
||
|
||
function t(key, fallback) {
|
||
if (window.HyAppI18n && window.HyAppI18n.t) {
|
||
return window.HyAppI18n.t(key, fallback);
|
||
}
|
||
return fallback || key;
|
||
}
|
||
|
||
function escapeHTML(value) {
|
||
return String(value == null ? '' : value)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
function safeImageURL(value) {
|
||
var source = String(value || '').trim();
|
||
if (!source) return '';
|
||
if (/^(https?:|data:image\/|blob:|\.\.?\/)/i.test(source)) {
|
||
return escapeHTML(source);
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function resolveInitialTab() {
|
||
try {
|
||
var requested = new URLSearchParams(window.location.search).get(
|
||
'tab'
|
||
);
|
||
if (['plaza', 'rank', 'rewards'].indexOf(requested) >= 0) {
|
||
return requested;
|
||
}
|
||
} catch (_) {
|
||
// 无 URLSearchParams 的旧 WebView 使用设计稿默认的 CP Rank,不影响核心接口加载。
|
||
}
|
||
return 'rank';
|
||
}
|
||
|
||
function isLocalRuntime() {
|
||
try {
|
||
var params = new URLSearchParams(window.location.search);
|
||
var hostname = String(window.location.hostname || '').toLowerCase();
|
||
return (
|
||
hostname === 'localhost' ||
|
||
hostname === '127.0.0.1' ||
|
||
hostname === '::1' ||
|
||
params.get('env') === 'local'
|
||
);
|
||
} catch (_) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function isExplicitMockMode() {
|
||
try {
|
||
return (
|
||
isLocalRuntime() &&
|
||
new URLSearchParams(window.location.search).get('mock') === '1'
|
||
);
|
||
} catch (_) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function twoDigits(value) {
|
||
return String(Math.max(0, Number(value) || 0)).padStart(2, '0');
|
||
}
|
||
|
||
function resolveExplicitEndAt() {
|
||
if (!isLocalRuntime()) return 0;
|
||
try {
|
||
var params = new URLSearchParams(window.location.search);
|
||
var parsed = Date.parse(
|
||
params.get('end_at') || params.get('endAt') || ''
|
||
);
|
||
return Number.isFinite(parsed) && parsed > Date.now() ? parsed : 0;
|
||
} catch (_) {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
function nowMS() {
|
||
return Date.now() + serverTimeOffsetMS;
|
||
}
|
||
|
||
function getTimeParts() {
|
||
var diff =
|
||
countdownEndAt > 0 ? Math.max(0, countdownEndAt - nowMS()) : 0;
|
||
var totalSeconds = Math.floor(diff / 1000);
|
||
return {
|
||
days: Math.floor(totalSeconds / 86400),
|
||
hours: Math.floor((totalSeconds % 86400) / 3600),
|
||
minutes: Math.floor((totalSeconds % 3600) / 60),
|
||
seconds: totalSeconds % 60,
|
||
};
|
||
}
|
||
|
||
function updateCountdown() {
|
||
var parts = getTimeParts();
|
||
Object.keys(parts).forEach(function (key) {
|
||
document
|
||
.querySelectorAll('[data-count="' + key + '"]')
|
||
.forEach(function (node) {
|
||
node.textContent = twoDigits(parts[key]);
|
||
});
|
||
});
|
||
}
|
||
|
||
function initials(name) {
|
||
var normalized = String(name || '').trim();
|
||
return normalized ? normalized.charAt(0).toUpperCase() : 'CP';
|
||
}
|
||
|
||
function renderAvatar(className, avatar, name) {
|
||
var imageURL = safeImageURL(avatar);
|
||
return [
|
||
'<span class="huwaa-cp-avatar ',
|
||
className,
|
||
'" aria-hidden="true">',
|
||
imageURL
|
||
? '<img src="' + imageURL + '" alt="" loading="lazy" />'
|
||
: '<span>' + escapeHTML(initials(name)) + '</span>',
|
||
'</span>',
|
||
].join('');
|
||
}
|
||
|
||
function renderPodiumAvatar(className, avatar, name) {
|
||
var imageURL = safeImageURL(avatar);
|
||
return [
|
||
'<span class="huwaa-cp-podium-avatar ',
|
||
className,
|
||
'" aria-hidden="true">',
|
||
imageURL
|
||
? '<img src="' + imageURL + '" alt="" loading="lazy" />'
|
||
: '<span>' + escapeHTML(initials(name)) + '</span>',
|
||
'</span>',
|
||
].join('');
|
||
}
|
||
|
||
function renderState(state, retryScope) {
|
||
var key =
|
||
state === 'error'
|
||
? 'cp.error'
|
||
: state === 'loading'
|
||
? 'cp.loading'
|
||
: state === 'disabled'
|
||
? 'cp.disabled'
|
||
: state === 'unavailable'
|
||
? 'cp.unavailable'
|
||
: 'cp.empty';
|
||
var fallback =
|
||
state === 'error'
|
||
? 'Unable to load activity data.'
|
||
: state === 'loading'
|
||
? 'Loading...'
|
||
: state === 'disabled'
|
||
? 'This activity is not available.'
|
||
: state === 'unavailable'
|
||
? 'No data source is available yet.'
|
||
: 'No data yet.';
|
||
return [
|
||
'<div class="huwaa-cp-state ',
|
||
state === 'loading' ? 'is-loading' : '',
|
||
'" role="status" aria-live="polite">',
|
||
'<span>',
|
||
escapeHTML(t(key, fallback)),
|
||
'</span>',
|
||
retryScope
|
||
? '<button class="huwaa-cp-retry" type="button" data-retry="' +
|
||
escapeHTML(retryScope) +
|
||
'">' +
|
||
escapeHTML(t('cp.retry', 'Retry')) +
|
||
'</button>'
|
||
: '',
|
||
'</div>',
|
||
].join('');
|
||
}
|
||
|
||
function renderPanel(options) {
|
||
var classes = ['huwaa-cp-panel'];
|
||
if (options.className) classes.push(options.className);
|
||
return [
|
||
'<section class="',
|
||
classes.join(' '),
|
||
'"',
|
||
options.ariaLabel
|
||
? ' aria-label="' + escapeHTML(options.ariaLabel) + '"'
|
||
: '',
|
||
' style="top:',
|
||
options.top,
|
||
'px;height:',
|
||
options.height,
|
||
'px">',
|
||
'<div class="huwaa-cp-panel-body">',
|
||
options.body,
|
||
'</div>',
|
||
'</section>',
|
||
].join('');
|
||
}
|
||
|
||
function renderPanelHeader(
|
||
titleKey,
|
||
titleFallback,
|
||
subtitleKey,
|
||
subtitleFallback
|
||
) {
|
||
return [
|
||
'<h2 class="huwaa-cp-panel-title">',
|
||
escapeHTML(t(titleKey, titleFallback)),
|
||
'</h2>',
|
||
subtitleKey
|
||
? '<p class="huwaa-cp-panel-subtitle">' +
|
||
escapeHTML(t(subtitleKey, subtitleFallback)) +
|
||
'</p>'
|
||
: '',
|
||
].join('');
|
||
}
|
||
|
||
function renderCPRow(row) {
|
||
var center =
|
||
row.type === 'gift'
|
||
? '<img class="huwaa-cp-gift" src="' +
|
||
ASSET_BASE +
|
||
'gift-plane.webp" alt="" aria-hidden="true" />'
|
||
: [
|
||
'<img class="huwaa-cp-score-heart" src="',
|
||
ASSET_BASE,
|
||
'score-heart.webp" alt="" aria-hidden="true" />',
|
||
'<strong class="huwaa-cp-score">',
|
||
escapeHTML(row.score),
|
||
'</strong>',
|
||
].join('');
|
||
return [
|
||
'<article class="huwaa-cp-row">',
|
||
renderAvatar('is-left', row.leftAvatar, row.leftName),
|
||
renderAvatar('is-right', row.rightAvatar, row.rightName),
|
||
'<span class="huwaa-cp-name is-left">',
|
||
escapeHTML(row.leftName),
|
||
'</span>',
|
||
'<span class="huwaa-cp-name is-right">',
|
||
escapeHTML(row.rightName),
|
||
'</span>',
|
||
'<img class="huwaa-cp-deco-line is-left" src="',
|
||
ASSET_BASE,
|
||
'deco-line-left.webp" alt="" aria-hidden="true" />',
|
||
'<img class="huwaa-cp-deco-line is-right" src="',
|
||
ASSET_BASE,
|
||
'deco-line-right.webp" alt="" aria-hidden="true" />',
|
||
center,
|
||
'</article>',
|
||
].join('');
|
||
}
|
||
|
||
function rowsOrState(rows, state, retryScope) {
|
||
if (rows.length) {
|
||
return (
|
||
'<div class="huwaa-cp-list">' +
|
||
rows.map(renderCPRow).join('') +
|
||
'</div>'
|
||
);
|
||
}
|
||
return renderState(
|
||
state === 'ready' ? 'empty' : state,
|
||
state === 'error' ? retryScope : ''
|
||
);
|
||
}
|
||
|
||
function renderPreviousPanel(top, height) {
|
||
var body =
|
||
renderPanelHeader(
|
||
'cp.previousWeekTopTitle',
|
||
'Previous Top 3',
|
||
'cp.previousWeekTopSubtitle',
|
||
'(Top CPs from the previous completed week.)'
|
||
) +
|
||
rowsOrState(
|
||
activityData.previousRows,
|
||
activityData.statusState,
|
||
'status'
|
||
);
|
||
return renderPanel({ top: top, height: height, body: body });
|
||
}
|
||
|
||
function renderPlaza() {
|
||
var firstBody =
|
||
renderPanelHeader(
|
||
'cp.huwaaPlazaGiftTitle',
|
||
'Gifts exchanged between CPs ≥ 5000 gold will be displayed here',
|
||
'cp.plazaGiftSubtitle',
|
||
'(Display the last 10 records, sorted in ascending order.)'
|
||
) +
|
||
rowsOrState(activityData.plazaRows, activityData.plazaState, '');
|
||
return [
|
||
renderPanel({
|
||
top: PANEL_LAYOUT.plazaFirstTop,
|
||
height: PANEL_LAYOUT.plazaFirstHeight,
|
||
className: 'is-gift-panel',
|
||
body: firstBody,
|
||
}),
|
||
renderPreviousPanel(
|
||
PANEL_LAYOUT.plazaSecondTop,
|
||
PANEL_LAYOUT.plazaSecondHeight
|
||
),
|
||
].join('');
|
||
}
|
||
|
||
function renderPodiumPair(item, place) {
|
||
if (!item) return '';
|
||
var level = place === 1 ? 'gold' : place === 2 ? 'silver' : 'bronze';
|
||
var accessibleLabel = [
|
||
t('cp.rankPlace', 'Rank'),
|
||
String(place),
|
||
item.leftName,
|
||
item.rightName,
|
||
].join(' ');
|
||
return [
|
||
'<article class="huwaa-cp-podium is-',
|
||
level,
|
||
'" aria-label="',
|
||
escapeHTML(accessibleLabel),
|
||
'">',
|
||
renderPodiumAvatar('is-left', item.leftAvatar, item.leftName),
|
||
renderPodiumAvatar('is-right', item.rightAvatar, item.rightName),
|
||
'<img class="huwaa-cp-podium-art" src="',
|
||
ASSET_BASE,
|
||
PODIUM_ASSETS[level],
|
||
'" alt="" aria-hidden="true" />',
|
||
'<span class="huwaa-cp-podium-name is-left">',
|
||
escapeHTML(item.leftName),
|
||
'</span>',
|
||
'<span class="huwaa-cp-podium-name is-right">',
|
||
escapeHTML(item.rightName),
|
||
'</span>',
|
||
'</article>',
|
||
].join('');
|
||
}
|
||
|
||
function renderRank() {
|
||
var podium = '';
|
||
if (activityData.rank.length) {
|
||
var rankAt = function (place) {
|
||
return activityData.rank.find(function (item) {
|
||
return item.place === place;
|
||
});
|
||
};
|
||
podium = [
|
||
'<section class="huwaa-cp-rank-podium" aria-label="',
|
||
escapeHTML(t('cp.rankPodiumLabel', 'Top CP ranking')),
|
||
'">',
|
||
renderPodiumPair(rankAt(1), 1),
|
||
renderPodiumPair(rankAt(2), 2),
|
||
renderPodiumPair(rankAt(3), 3),
|
||
'</section>',
|
||
].join('');
|
||
} else {
|
||
var rankStateKey =
|
||
activityData.statusState === 'error'
|
||
? 'cp.error'
|
||
: activityData.statusState === 'loading'
|
||
? 'cp.loading'
|
||
: activityData.statusState === 'disabled'
|
||
? 'cp.disabled'
|
||
: 'cp.empty';
|
||
var rankStateFallback =
|
||
activityData.statusState === 'error'
|
||
? 'Unable to load activity data.'
|
||
: activityData.statusState === 'loading'
|
||
? 'Loading...'
|
||
: activityData.statusState === 'disabled'
|
||
? 'This activity is not available.'
|
||
: 'No data yet.';
|
||
podium = [
|
||
'<p class="huwaa-cp-rank-empty" role="status" aria-live="polite">',
|
||
escapeHTML(t(rankStateKey, rankStateFallback)),
|
||
'</p>',
|
||
].join('');
|
||
}
|
||
return (
|
||
podium +
|
||
renderPreviousPanel(
|
||
PANEL_LAYOUT.rankPanelTop,
|
||
PANEL_LAYOUT.rankPanelHeight
|
||
)
|
||
);
|
||
}
|
||
|
||
function rewardName(item) {
|
||
var resourceName =
|
||
item.resourceName ||
|
||
item.name ||
|
||
item.resource_name ||
|
||
localizedResourceType(item.resource_type || item.resourceType) ||
|
||
t('cp.reward', 'Reward');
|
||
var duration = Number(item.duration_days || item.durationDays || 0);
|
||
var quantity = Number(item.quantity || 0);
|
||
if (duration > 0) {
|
||
return (
|
||
resourceName + '*' + duration + ' ' + t('cp.daysLong', 'days')
|
||
);
|
||
}
|
||
if (quantity > 0) return resourceName + '*' + quantity;
|
||
return resourceName;
|
||
}
|
||
|
||
function localizedResourceType(value) {
|
||
var type = String(value || '')
|
||
.trim()
|
||
.toLowerCase();
|
||
var keys = {
|
||
avatar_frame: ['cp.rewardAvatarFrame', 'Avatar frame'],
|
||
profile_card: ['cp.rewardProfileCard', 'Profile card'],
|
||
coin: ['cp.rewardCoins', 'Coins'],
|
||
vehicle: ['cp.rewardVehicle', 'Vehicle'],
|
||
chat_bubble: ['cp.rewardChatBubble', 'Chat bubble'],
|
||
badge: ['cp.rewardBadge', 'Badge'],
|
||
floating_screen: ['cp.rewardFloatingScreen', 'Floating screen'],
|
||
gift: ['cp.rewardGift', 'Gift'],
|
||
mic_seat_icon: ['cp.rewardMicSeatIcon', 'Mic seat icon'],
|
||
mic_seat_animation: [
|
||
'cp.rewardMicSeatAnimation',
|
||
'Mic seat animation',
|
||
],
|
||
emoji_pack: ['cp.rewardEmojiPack', 'Emoji pack'],
|
||
vip_trial_card: ['cp.rewardVipTrialCard', 'VIP trial card'],
|
||
};
|
||
return keys[type] ? t(keys[type][0], keys[type][1]) : '';
|
||
}
|
||
|
||
function rewardNote(item) {
|
||
return String(item.description || item.note || '').trim();
|
||
}
|
||
|
||
function renderRewardItem(item) {
|
||
var cover = safeImageURL(item.coverUrl || item.cover_url);
|
||
return [
|
||
'<article class="huwaa-cp-reward-card">',
|
||
'<div class="huwaa-cp-reward-icon">',
|
||
cover
|
||
? '<img class="huwaa-cp-reward-cover" src="' +
|
||
cover +
|
||
'" alt="" loading="lazy" />'
|
||
: '',
|
||
'<img class="huwaa-cp-reward-frame" src="',
|
||
ASSET_BASE,
|
||
'reward-frame.webp" alt="" aria-hidden="true" />',
|
||
'</div>',
|
||
'<div class="huwaa-cp-reward-name">',
|
||
escapeHTML(item.displayName || rewardName(item)),
|
||
'</div>',
|
||
item.note || rewardNote(item)
|
||
? '<div class="huwaa-cp-reward-note">' +
|
||
escapeHTML(item.note || rewardNote(item)) +
|
||
'</div>'
|
||
: '',
|
||
'</article>',
|
||
].join('');
|
||
}
|
||
|
||
function renderRewardPanel(section, index) {
|
||
var tierNo = Number(section.rankNo || index + 1);
|
||
if (tierNo < 1 || tierNo > 3) tierNo = index + 1;
|
||
var top =
|
||
PANEL_LAYOUT.rewardFirstTop +
|
||
index *
|
||
(PANEL_LAYOUT.rewardPanelHeight + PANEL_LAYOUT.rewardPanelGap);
|
||
var items = Array.isArray(section.items) ? section.items : [];
|
||
var tierLabels = {
|
||
1: t('cp.rewardTierGold', 'First-place rewards'),
|
||
2: t('cp.rewardTierSilver', 'Second-place rewards'),
|
||
3: t('cp.rewardTierBronze', 'Third-place rewards'),
|
||
};
|
||
var body = [
|
||
'<img class="huwaa-cp-reward-crown" src="',
|
||
ASSET_BASE,
|
||
CROWN_ASSETS[tierNo] || CROWN_ASSETS[3],
|
||
'" alt="" aria-hidden="true" />',
|
||
items.length
|
||
? '<div class="huwaa-cp-reward-grid">' +
|
||
items.map(renderRewardItem).join('') +
|
||
'</div>'
|
||
: renderState('empty', ''),
|
||
].join('');
|
||
return renderPanel({
|
||
top: top,
|
||
height: PANEL_LAYOUT.rewardPanelHeight,
|
||
className: 'huwaa-cp-reward-panel',
|
||
ariaLabel: tierLabels[tierNo],
|
||
body: body,
|
||
});
|
||
}
|
||
|
||
function renderRewards() {
|
||
if (!activityData.rewards.length) {
|
||
var state =
|
||
activityData.statusState === 'ready'
|
||
? 'empty'
|
||
: activityData.statusState;
|
||
return renderPanel({
|
||
top: PANEL_LAYOUT.rewardFirstTop,
|
||
height: PANEL_LAYOUT.rewardPanelHeight,
|
||
className: 'huwaa-cp-reward-panel',
|
||
body: renderState(state, state === 'error' ? 'status' : ''),
|
||
});
|
||
}
|
||
return [1, 2, 3]
|
||
.map(function (rankNo) {
|
||
return (
|
||
activityData.rewards.find(function (section) {
|
||
return section.rankNo === rankNo;
|
||
}) || { rankNo: rankNo, items: [] }
|
||
);
|
||
})
|
||
.map(renderRewardPanel)
|
||
.join('');
|
||
}
|
||
|
||
function formatScore(value) {
|
||
var number = Number(value || 0);
|
||
if (!Number.isFinite(number)) return '0';
|
||
try {
|
||
return number.toLocaleString(document.documentElement.lang || 'en');
|
||
} catch (_) {
|
||
return String(number);
|
||
}
|
||
}
|
||
|
||
function normalizePairUser(user) {
|
||
var source = user || {};
|
||
var userID = source.user_id || source.userId || '';
|
||
var displayID = source.display_user_id || source.displayUserId || '';
|
||
var name = source.username || source.name || displayID || userID || '';
|
||
if (!name && !source.avatar) return null;
|
||
return {
|
||
name: name || 'CP',
|
||
avatar: source.avatar || source.avatar_url || '',
|
||
};
|
||
}
|
||
|
||
function normalizeRankEntry(entry, fallbackPlace) {
|
||
var source = entry || {};
|
||
var userA = normalizePairUser(source.user_a || source.userA);
|
||
var userB = normalizePairUser(source.user_b || source.userB);
|
||
if (!userA || !userB) return null;
|
||
return {
|
||
type: 'score',
|
||
place: Number(source.rank || fallbackPlace || 0),
|
||
score: formatScore(source.score || source.intimacy_value || 0),
|
||
leftName: userA.name,
|
||
rightName: userB.name,
|
||
leftAvatar: userA.avatar,
|
||
rightAvatar: userB.avatar,
|
||
};
|
||
}
|
||
|
||
function normalizeRewardSection(section, fallbackRankNo) {
|
||
var source = section || {};
|
||
var group = source.resource_group || source.resourceGroup || {};
|
||
var groupItems = Array.isArray(group.items) ? group.items : [];
|
||
var items = Array.isArray(source.items) ? source.items : [];
|
||
return {
|
||
rankNo: Number(
|
||
source.rank_no || source.rankNo || fallbackRankNo || 0
|
||
),
|
||
items: items.map(function (item, index) {
|
||
var groupItem = groupItems[index] || {};
|
||
var resource = groupItem.resource || {};
|
||
return {
|
||
resourceName: String(resource.name || '').trim(),
|
||
resource_type:
|
||
item.resource_type ||
|
||
item.resourceType ||
|
||
resource.resource_type ||
|
||
resource.resourceType ||
|
||
'',
|
||
duration_days: item.duration_days || item.durationDays || 0,
|
||
quantity: item.quantity || groupItem.quantity || 0,
|
||
coverUrl:
|
||
item.cover_url ||
|
||
item.coverUrl ||
|
||
resource.preview_url ||
|
||
resource.previewUrl ||
|
||
resource.asset_url ||
|
||
resource.assetUrl ||
|
||
'',
|
||
};
|
||
}),
|
||
};
|
||
}
|
||
|
||
function applyActivityStatus(status) {
|
||
var source = status || {};
|
||
var serverTimeMS = Number(
|
||
source.server_time_ms || source.serverTimeMS || 0
|
||
);
|
||
if (serverTimeMS > 0) {
|
||
serverTimeOffsetMS = serverTimeMS - Date.now();
|
||
}
|
||
activityData.enabled = source.enabled !== false;
|
||
if (!activityData.enabled) {
|
||
countdownEndAt = 0;
|
||
activityData.rank = [];
|
||
activityData.previousRows = [];
|
||
activityData.rewards = [];
|
||
activityData.statusState = 'disabled';
|
||
return;
|
||
}
|
||
var endMS = Number(
|
||
(source.period && (source.period.end_ms || source.period.endMS)) ||
|
||
source.end_ms ||
|
||
source.endMS ||
|
||
0
|
||
);
|
||
countdownEndAt = endMS > nowMS() ? endMS : 0;
|
||
|
||
activityData.rank = Array.isArray(source.entries)
|
||
? source.entries
|
||
.map(function (entry, index) {
|
||
return normalizeRankEntry(entry, index + 1);
|
||
})
|
||
.filter(Boolean)
|
||
.sort(function (left, right) {
|
||
return left.place - right.place;
|
||
})
|
||
.slice(0, 3)
|
||
: [];
|
||
activityData.previousRows = Array.isArray(source.previous_entries)
|
||
? source.previous_entries
|
||
.map(function (entry, index) {
|
||
return normalizeRankEntry(entry, index + 1);
|
||
})
|
||
.filter(Boolean)
|
||
.slice(0, 3)
|
||
: [];
|
||
activityData.rewards = Array.isArray(source.rewards)
|
||
? source.rewards
|
||
.map(function (section, index) {
|
||
return normalizeRewardSection(section, index + 1);
|
||
})
|
||
.filter(function (section) {
|
||
return section.rankNo >= 1 && section.rankNo <= 3;
|
||
})
|
||
.sort(function (left, right) {
|
||
return left.rankNo - right.rankNo;
|
||
})
|
||
: [];
|
||
activityData.statusState = 'ready';
|
||
}
|
||
|
||
function applyMockData() {
|
||
var pairs = [
|
||
['Avery', 'Noor', 126580],
|
||
['Mina', 'Elif', 118420],
|
||
['Lina', 'Sofia', 109730],
|
||
['Dara', 'Maya', 98760],
|
||
];
|
||
function pair(row, index, type) {
|
||
return {
|
||
type: type || 'score',
|
||
place: index + 1,
|
||
score: formatScore(row[2]),
|
||
rawScore: row[2],
|
||
leftName: row[0],
|
||
rightName: row[1],
|
||
leftAvatar: '',
|
||
rightAvatar: '',
|
||
};
|
||
}
|
||
activityData.rank = pairs.slice(0, 3).map(function (row, index) {
|
||
return pair(row, index, 'score');
|
||
});
|
||
activityData.previousRows = pairs
|
||
.slice(0, 3)
|
||
.map(function (row, index) {
|
||
return pair(row, index, 'score');
|
||
});
|
||
activityData.plazaRows = pairs.map(function (row, index) {
|
||
return pair(row, index, 'gift');
|
||
});
|
||
activityData.rewards = ['gold', 'silver', 'bronze'].map(
|
||
function (_, tier) {
|
||
return {
|
||
rankNo: tier + 1,
|
||
items: [
|
||
{
|
||
displayName: t('cp.rewardMedal7', 'Medal*7 days'),
|
||
note:
|
||
tier === 0
|
||
? t(
|
||
'cp.rewardMedalNote',
|
||
'(Permanent medal after getting 3 times)'
|
||
)
|
||
: '',
|
||
},
|
||
{
|
||
displayName: t(
|
||
'cp.rewardHonorBanner7',
|
||
'Honor banner*7 days'
|
||
),
|
||
},
|
||
{
|
||
displayName: t(
|
||
'cp.rewardHeadwear7',
|
||
'Headwear*7days'
|
||
),
|
||
},
|
||
{ displayName: t('cp.rewardMount7', 'Mount*7days') },
|
||
{
|
||
displayName: t(
|
||
'cp.rewardCoins20000',
|
||
'Coins*20000'
|
||
),
|
||
},
|
||
{ displayName: t('cp.rewardDuke7', 'Duke*7days') },
|
||
],
|
||
};
|
||
}
|
||
);
|
||
activityData.statusState = 'ready';
|
||
activityData.plazaState = 'ready';
|
||
activityData.enabled = true;
|
||
serverTimeOffsetMS = 0;
|
||
countdownEndAt = Date.now() + 2 * 86400000 + 2 * 3600000 + 2 * 60000;
|
||
}
|
||
|
||
function loadData(scope) {
|
||
var generation = ++requestGeneration;
|
||
if (isExplicitMockMode()) {
|
||
applyMockData();
|
||
render();
|
||
return Promise.resolve();
|
||
}
|
||
|
||
if (!scope || scope === 'status') {
|
||
activityData.statusState = 'loading';
|
||
activityData.rank = [];
|
||
activityData.previousRows = [];
|
||
activityData.rewards = [];
|
||
}
|
||
// 设计稿的 Plaza 是“单笔 CP 送礼事件流”;当前公开 intimacy-leaderboard 是累计亲密榜,
|
||
// 两者排序和业务含义不同。在后端事件 feed 上线前保持明确不可用态,绝不把累计榜伪装成送礼记录。
|
||
activityData.plazaState = 'unavailable';
|
||
activityData.plazaRows = [];
|
||
render();
|
||
|
||
var statusRequest =
|
||
window.HyAppAPI && window.HyAppAPI.cpWeeklyRank
|
||
? window.HyAppAPI.cpWeeklyRank.status({ limit: 3 })
|
||
: Promise.reject(new Error('cp_weekly_rank_api_unavailable'));
|
||
|
||
// allSettled 统一承接未来可并行加入的 Plaza 事件 feed;generation 防止重试后的旧周榜响应覆盖新状态。
|
||
return Promise.allSettled([statusRequest]).then(function (results) {
|
||
if (generation !== requestGeneration) return;
|
||
if (results[0].status === 'fulfilled') {
|
||
applyActivityStatus(results[0].value);
|
||
} else {
|
||
activityData.statusState = 'error';
|
||
}
|
||
render();
|
||
});
|
||
}
|
||
|
||
function applyTabState() {
|
||
tabs.forEach(function (tab) {
|
||
var isActive = tab.getAttribute('data-tab') === activeTab;
|
||
tab.classList.toggle('is-active', isActive);
|
||
tab.setAttribute('aria-selected', String(isActive));
|
||
tab.setAttribute('tabindex', isActive ? '0' : '-1');
|
||
var image = tab.querySelector('img');
|
||
if (image) {
|
||
image.src =
|
||
ASSET_BASE +
|
||
(isActive ? 'tab-active.webp' : 'tab-inactive.webp');
|
||
}
|
||
});
|
||
var activeButton = tabs.find(function (tab) {
|
||
return tab.getAttribute('data-tab') === activeTab;
|
||
});
|
||
tabContent.setAttribute(
|
||
'aria-labelledby',
|
||
activeButton ? activeButton.id : 'huwaaCpTabRank'
|
||
);
|
||
page.setAttribute('data-tab', activeTab);
|
||
}
|
||
|
||
function getPageWidth() {
|
||
var viewportWidth =
|
||
window.innerWidth ||
|
||
document.documentElement.clientWidth ||
|
||
DESIGN_WIDTH;
|
||
return Math.min(Math.max(viewportWidth, 1), MAX_PAGE_WIDTH);
|
||
}
|
||
|
||
function updateStageHeight() {
|
||
var designHeight = STAGE_HEIGHTS[activeTab] || STAGE_HEIGHTS.rank;
|
||
// Rewards 的完整 4782px 高度仅对应三档奖励都存在的设计态。加载失败、活动关闭或确实为空时
|
||
// 只有首块状态面板;按面板底部收拢页面,避免把不存在的二、三档内容伪装成长段空白。
|
||
if (activeTab === 'rewards' && !activityData.rewards.length) {
|
||
designHeight =
|
||
PANEL_LAYOUT.rewardFirstTop +
|
||
PANEL_LAYOUT.rewardPanelHeight +
|
||
160;
|
||
}
|
||
var pageWidth = getPageWidth();
|
||
var designScale = pageWidth / DESIGN_WIDTH;
|
||
stage.style.height = designHeight + 'px';
|
||
stage.style.minHeight = designHeight + 'px';
|
||
document.documentElement.style.setProperty(
|
||
'--huwaa-cp-page-width',
|
||
pageWidth + 'px'
|
||
);
|
||
document.documentElement.style.setProperty(
|
||
'--huwaa-cp-design-scale',
|
||
String(designScale)
|
||
);
|
||
stageWrap.style.height = Math.ceil(designHeight * designScale) + 'px';
|
||
stageWrap.style.minHeight = stageWrap.style.height;
|
||
copyright.style.top = designHeight - 77 + 'px';
|
||
document.body.style.minHeight = stageWrap.style.height;
|
||
viewport.style.minHeight = stageWrap.style.height;
|
||
page.style.minHeight = stageWrap.style.height;
|
||
}
|
||
|
||
function render() {
|
||
document.title = t('cp.pageTitle', 'Last Weekly CP');
|
||
tabContent.innerHTML =
|
||
activeTab === 'plaza'
|
||
? renderPlaza()
|
||
: activeTab === 'rewards'
|
||
? renderRewards()
|
||
: renderRank();
|
||
applyTabState();
|
||
updateStageHeight();
|
||
updateCountdown();
|
||
}
|
||
|
||
function updateURLTab() {
|
||
try {
|
||
var nextURL = new URL(window.location.href);
|
||
nextURL.searchParams.set('tab', activeTab);
|
||
window.history.replaceState(null, '', nextURL.toString());
|
||
} catch (_) {
|
||
// tab 切换本身不依赖 URL;history 被宿主禁用时仅放弃可分享状态参数。
|
||
}
|
||
}
|
||
|
||
function selectTab(nextTab, shouldFocus) {
|
||
if (['plaza', 'rank', 'rewards'].indexOf(nextTab) < 0) return;
|
||
activeTab = nextTab;
|
||
updateURLTab();
|
||
render();
|
||
var activeButton = tabs.find(function (tab) {
|
||
return tab.getAttribute('data-tab') === activeTab;
|
||
});
|
||
if (shouldFocus && activeButton) activeButton.focus();
|
||
announcement.textContent =
|
||
activeButton && activeButton.querySelector('span')
|
||
? activeButton.querySelector('span').textContent
|
||
: '';
|
||
}
|
||
|
||
function bindInteractions() {
|
||
tabs.forEach(function (tab, index) {
|
||
tab.addEventListener('click', function () {
|
||
selectTab(tab.getAttribute('data-tab') || 'rank', false);
|
||
});
|
||
tab.addEventListener('keydown', function (event) {
|
||
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')
|
||
return;
|
||
event.preventDefault();
|
||
var delta = event.key === 'ArrowRight' ? 1 : -1;
|
||
var nextIndex = (index + delta + tabs.length) % tabs.length;
|
||
selectTab(tabs[nextIndex].getAttribute('data-tab'), true);
|
||
});
|
||
});
|
||
tabContent.addEventListener('click', function (event) {
|
||
var retry = event.target.closest('[data-retry]');
|
||
if (retry) loadData(retry.getAttribute('data-retry') || 'status');
|
||
});
|
||
}
|
||
|
||
function init() {
|
||
countdownEndAt = resolveExplicitEndAt();
|
||
bindInteractions();
|
||
render();
|
||
loadData();
|
||
countdownTimer = window.setInterval(updateCountdown, 1000);
|
||
window.addEventListener('resize', updateStageHeight);
|
||
window.addEventListener('hyapp:i18n-ready', function () {
|
||
if (isExplicitMockMode()) applyMockData();
|
||
render();
|
||
});
|
||
}
|
||
|
||
window.addEventListener('beforeunload', function () {
|
||
requestGeneration += 1;
|
||
if (countdownTimer) window.clearInterval(countdownTimer);
|
||
});
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
})();
|