(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, ''');
}
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 [
'',
imageURL
? '
'
: '' + escapeHTML(initials(name)) + '',
'',
].join('');
}
function renderPodiumAvatar(className, avatar, name) {
var imageURL = safeImageURL(avatar);
return [
'',
imageURL
? '
'
: '' + escapeHTML(initials(name)) + '',
'',
].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 [
'
',
'',
escapeHTML(t(key, fallback)),
'',
retryScope
? ''
: '',
'
',
].join('');
}
function renderPanel(options) {
var classes = ['huwaa-cp-panel'];
if (options.className) classes.push(options.className);
return [
'',
'',
options.body,
'
',
'',
].join('');
}
function renderPanelHeader(
titleKey,
titleFallback,
subtitleKey,
subtitleFallback
) {
return [
'',
escapeHTML(t(titleKey, titleFallback)),
'
',
subtitleKey
? '' +
escapeHTML(t(subtitleKey, subtitleFallback)) +
'
'
: '',
].join('');
}
function renderCPRow(row) {
var center =
row.type === 'gift'
? '
'
: [
'
',
'',
escapeHTML(row.score),
'',
].join('');
return [
'',
renderAvatar('is-left', row.leftAvatar, row.leftName),
renderAvatar('is-right', row.rightAvatar, row.rightName),
'',
escapeHTML(row.leftName),
'',
'',
escapeHTML(row.rightName),
'',
'
',
'
',
center,
'',
].join('');
}
function rowsOrState(rows, state, retryScope) {
if (rows.length) {
return (
'' +
rows.map(renderCPRow).join('') +
'
'
);
}
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 [
'',
renderPodiumAvatar('is-left', item.leftAvatar, item.leftName),
renderPodiumAvatar('is-right', item.rightAvatar, item.rightName),
'
',
'',
escapeHTML(item.leftName),
'',
'',
escapeHTML(item.rightName),
'',
'',
].join('');
}
function renderRank() {
var podium = '';
if (activityData.rank.length) {
var rankAt = function (place) {
return activityData.rank.find(function (item) {
return item.place === place;
});
};
podium = [
'',
renderPodiumPair(rankAt(1), 1),
renderPodiumPair(rankAt(2), 2),
renderPodiumPair(rankAt(3), 3),
'',
].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 = [
'',
escapeHTML(t(rankStateKey, rankStateFallback)),
'
',
].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 [
'',
'',
cover
? '

'
: '',
'

',
'
',
'',
escapeHTML(item.displayName || rewardName(item)),
'
',
item.note || rewardNote(item)
? '' +
escapeHTML(item.note || rewardNote(item)) +
'
'
: '',
'',
].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 = [
'
',
items.length
? '' +
items.map(renderRewardItem).join('') +
'
'
: 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();
}
})();