(function () {
var DESIGN_WIDTH = 1080;
var MAX_PAGE_WIDTH = 768;
var LALU_COIN_IMAGE = '../../common/png/lalu-coin.png';
var REWARD_FIRST_TOP = 1816;
var REWARD_PANEL_GAP = 56;
var REWARD_GRID_TOP = 220;
var REWARD_CARD_MIN_HEIGHT = 264;
var REWARD_GRID_ROW_GAP = 24;
var REWARD_PANEL_BOTTOM_PADDING = 80;
var REWARD_STAGE_BOTTOM_PADDING = 133;
var REWARD_MIN_PANEL_HEIGHT = 320;
var STORAGE_KEY = 'hy_cp_activity_end_at';
var stageHeights = {
plaza: 4283,
rank: 3670,
rewards: 4797,
};
var rewardStageHeight = stageHeights.rewards;
var activeTab = 'rank';
var countdownEndAt = 0;
var countdownTimer = 0;
var activityData = {
plazaRows: [],
previousRows: [],
rank: [],
rewards: null,
periodEndMS: 0,
};
var viewport = document.getElementById('appViewport');
var page = document.getElementById('page');
var stage = document.querySelector('.stage');
var stageWrap = document.getElementById('stageWrap');
var tabContent = document.getElementById('tabContent');
var copyright = document.querySelector('.copyright');
var tabs = Array.prototype.slice.call(document.querySelectorAll('.tab'));
var mockData = {
rewards: [
{
crown: 'crown-gold.png',
items: [
{ key: 'cp.rewardMedal7', noteKey: 'cp.rewardMedalNote' },
{ key: 'cp.rewardHonorBanner7' },
{ key: 'cp.rewardHeadwear7' },
{ key: 'cp.rewardMount7' },
{ key: 'cp.rewardCoins20000' },
{ key: 'cp.rewardDuke7' },
],
},
{
crown: 'crown-silver.png',
items: [
{ key: 'cp.rewardMedal7', noteKey: 'cp.rewardMedalNote' },
{ key: 'cp.rewardHonorBanner7' },
{ key: 'cp.rewardMount7' },
{ key: 'cp.rewardCoins20000' },
{ key: 'cp.rewardMarquis7' },
],
},
{
crown: 'crown-bronze.png',
items: [
{ key: 'cp.rewardMedal7', noteKey: 'cp.rewardMedalNote' },
{ key: 'cp.rewardHonorBanner7' },
{ key: 'cp.rewardCoins10000' },
{ key: 'cp.rewardCount7' },
],
},
],
};
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 twoDigits(value) {
return String(Math.max(0, value)).padStart(2, '0');
}
function resolveFallbackEndAt() {
var params = new URLSearchParams(window.location.search || '');
var explicitEnd = Date.parse(
params.get('end_at') || params.get('endAt') || ''
);
if (Number.isFinite(explicitEnd) && explicitEnd > Date.now())
return explicitEnd;
try {
var cached = Number(window.localStorage.getItem(STORAGE_KEY));
if (Number.isFinite(cached) && cached > Date.now()) return cached;
var fallback =
Date.now() +
2 * 86400 * 1000 +
2 * 3600 * 1000 +
2 * 60 * 1000 +
2 * 1000;
window.localStorage.setItem(STORAGE_KEY, String(fallback));
return fallback;
} catch (error) {
return (
Date.now() +
2 * 86400 * 1000 +
2 * 3600 * 1000 +
2 * 60 * 1000 +
2 * 1000
);
}
}
function getTimeParts() {
var diff = Math.max(0, countdownEndAt - Date.now());
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 renderPanel(className, top, height, bodyHTML) {
return [
'',
'
',
'',
'
',
'',
bodyHTML,
'
',
'',
].join('');
}
function estimateRewardPanelHeight(section) {
var count =
section && Array.isArray(section.items) ? section.items.length : 0;
var rows = count > 0 ? Math.ceil(count / 3) : 0;
var gridHeight =
rows > 0
? rows * REWARD_CARD_MIN_HEIGHT +
Math.max(0, rows - 1) * REWARD_GRID_ROW_GAP
: 0;
return Math.max(
REWARD_MIN_PANEL_HEIGHT,
REWARD_GRID_TOP + gridHeight + REWARD_PANEL_BOTTOM_PADDING
);
}
function renderAvatar(className, avatar) {
if (!avatar) return '';
return [
'
',
'
,
')
',
'
',
].join('');
}
function renderCPRow(row) {
var center =
row.type === 'score'
? [
'
',
'',
escapeHTML(row.score),
'',
].join('')
: [
'',
'

',
'
',
].join('');
return [
'',
renderAvatar('avatar-left', row.leftAvatar),
renderAvatar('avatar-right', row.rightAvatar),
'',
escapeHTML(row.leftName),
'',
'',
escapeHTML(row.rightName),
'',
'
',
'
',
center,
'',
].join('');
}
function renderPlaza() {
var plazaRows = activityData.plazaRows || [];
var previousRows = activityData.previousRows || [];
var firstBody = [
'',
t(
'cp.plazaGiftTitle',
'CP pairs with intimacy ≥ 5000 are shown here'
),
'
',
'',
plazaRows.map(renderCPRow).join(''),
'
',
].join('');
var panels = [renderPanel('plaza-panel-a', 1816, 1152, firstBody)];
if (previousRows.length) {
var secondBody = [
'',
t('cp.previousNo1Title', 'Previous Top 3'),
'
',
'',
previousRows.map(renderCPRow).join(''),
'
',
].join('');
panels.push(renderPanel('plaza-panel-b', 3024, 1126, secondBody));
}
return panels.join('');
}
function renderInlineCountdown() {
var units = [
['days', 'cp.days', 'D'],
['hours', 'cp.hours', 'H'],
['minutes', 'cp.minutes', 'M'],
['seconds', 'cp.seconds', 'S'],
];
return [
'',
units
.map(function (unit) {
return [
'
',
'

',
'
02',
'
',
t(unit[1], unit[2]),
'',
'
',
].join('');
})
.join(''),
'
',
].join('');
}
function renderPodiumCard(item) {
var crown =
item.place === 1
? 'rank-crown-gold.png'
: item.place === 2
? 'rank-crown-silver.png'
: 'crown-bronze.png';
return [
'',
'
',
'
',
renderAvatar('podium-avatar left', item.leftAvatar),
renderAvatar('podium-avatar right', item.rightAvatar),
'',
escapeHTML(item.leftName),
'',
escapeHTML(item.rightName),
'
',
'',
].join('');
}
function renderRankRow(item) {
return renderCPRow({
type: 'score',
leftName: item.leftName,
rightName: item.rightName,
leftAvatar: item.leftAvatar,
rightAvatar: item.rightAvatar,
score: item.score,
});
}
function renderRank() {
var rankRows = activityData.rank || [];
var podium = [
'',
rankRows.slice(0, 3).map(renderPodiumCard).join(''),
'',
].join('');
var panelBody = [
renderInlineCountdown(),
'',
rankRows.map(renderRankRow).join(''),
'
',
].join('');
return podium + renderPanel('rank-panel', 2430, 1107, panelBody);
}
function renderRewardItem(item) {
var name = item.name || t(item.key, item.key);
var note = item.note || (item.noteKey ? t(item.noteKey, '') : '');
var cover = item.coverUrl || './assets/reward-frame.png';
var cardClass =
item.resourceType === 'coin'
? 'reward-card is-coin'
: 'reward-card';
return [
'',
'',
'',
escapeHTML(name),
'
',
note
? '' + escapeHTML(note) + '
'
: '',
'',
].join('');
}
function renderRewardPanel(section, index) {
var className = [
'reward-panel',
'reward-' + ['a', 'b', 'c'][index],
].join(' ');
var top = REWARD_FIRST_TOP;
for (var cursor = 0; cursor < index; cursor += 1) {
top +=
estimateRewardPanelHeight(
(activityData.rewards || mockData.rewards)[cursor]
) + REWARD_PANEL_GAP;
}
var height = estimateRewardPanelHeight(section);
var body = [
'
',
'',
section.items.map(renderRewardItem).join(''),
'
',
].join('');
return renderPanel(className, top, height, body);
}
function renderRewards() {
var sections = activityData.rewards || mockData.rewards;
var bottom = REWARD_FIRST_TOP;
var html = sections
.map(function (section, index) {
var panel = renderRewardPanel(section, index);
bottom += estimateRewardPanelHeight(section) + REWARD_PANEL_GAP;
return panel;
})
.join('');
rewardStageHeight =
bottom - REWARD_PANEL_GAP + REWARD_STAGE_BOTTOM_PADDING;
return html;
}
function syncRewardPanelLayout() {
if (activeTab !== 'rewards') return;
var nextTop = REWARD_FIRST_TOP;
var panels = Array.prototype.slice.call(
tabContent.querySelectorAll('.reward-panel')
);
panels.forEach(function (panel) {
var grid = panel.querySelector('.reward-grid');
var measuredHeight = REWARD_MIN_PANEL_HEIGHT;
// 奖励内容来自接口,不同档位数量和多语言换行都可能变化;渲染后用真实 grid 高度回写面板高度,避免固定 912px 留出大空白。
if (grid) {
measuredHeight = Math.max(
measuredHeight,
grid.offsetTop +
grid.offsetHeight +
REWARD_PANEL_BOTTOM_PADDING
);
}
measuredHeight = Math.ceil(measuredHeight);
panel.style.top = nextTop + 'px';
panel.style.height = measuredHeight + 'px';
nextTop += measuredHeight + REWARD_PANEL_GAP;
});
if (panels.length) {
rewardStageHeight =
nextTop - REWARD_PANEL_GAP + REWARD_STAGE_BOTTOM_PADDING;
}
}
function formatScore(value) {
var number = Number(value || 0);
if (!Number.isFinite(number)) return '0';
return number.toLocaleString('en-US');
}
function normalizeUser(user) {
return user || {};
}
function normalizePairUser(user) {
var source = normalizeUser(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,
avatar: source.avatar || '',
};
}
function normalizeRankEntry(entry) {
var userA = normalizePairUser(entry.user_a);
var userB = normalizePairUser(entry.user_b);
if (!userA || !userB) return null;
return {
type: 'score',
place: Number(entry.rank || 0),
score: formatScore(entry.score),
leftName: userA.name,
rightName: userB.name,
leftAvatar: userA.avatar,
rightAvatar: userB.avatar,
};
}
function normalizePlazaEntry(entry) {
var intimacyValue = Number(
entry.intimacy_value || entry.intimacyValue || 0
);
var userA = normalizePairUser(entry.user_a);
var userB = normalizePairUser(entry.user_b);
if (intimacyValue < 5000 || !userA || !userB) return null;
return {
type: 'score',
rawScore: intimacyValue,
score: formatScore(intimacyValue),
leftName: userA.name,
rightName: userB.name,
leftAvatar: userA.avatar,
rightAvatar: userB.avatar,
};
}
function rewardName(item) {
var type = item.resource_type || 'reward';
if (item.duration_days > 0)
return type + ' ' + item.duration_days + 'D';
if (item.quantity > 0) return type + ' x' + item.quantity;
return type;
}
function rewardResourceType(item) {
return String(
item.resource_type || item.resourceType || ''
).toLowerCase();
}
function rewardCoverUrl(item) {
var resourceType = rewardResourceType(item);
if (resourceType === 'coin') return LALU_COIN_IMAGE;
return item.cover_url || item.coverUrl || '';
}
function normalizeRewardSection(reward, index) {
var crowns = ['crown-gold.png', 'crown-silver.png', 'crown-bronze.png'];
return {
crown: crowns[index] || 'crown-bronze.png',
items: (reward.items || []).map(function (item) {
var resourceType = rewardResourceType(item);
return {
name: rewardName(item),
resourceType: resourceType,
note:
item.duration_days > 0
? item.duration_days + ' days'
: item.quantity > 0
? 'x' + item.quantity
: '',
coverUrl: rewardCoverUrl(item),
};
}),
};
}
function applyActivityStatus(status) {
if (!status) return;
if (status.period && status.period.end_ms > Date.now()) {
activityData.periodEndMS = status.period.end_ms;
countdownEndAt = status.period.end_ms;
}
if (Array.isArray(status.entries)) {
var rankRows = status.entries
.map(normalizeRankEntry)
.filter(function (item) {
return Boolean(item);
});
activityData.rank = rankRows;
}
if (Array.isArray(status.previous_entries)) {
activityData.previousRows = status.previous_entries
.map(normalizeRankEntry)
.filter(function (item) {
return Boolean(item);
})
.slice(0, 3);
}
if (Array.isArray(status.rewards) && status.rewards.length) {
activityData.rewards = status.rewards.map(normalizeRewardSection);
}
}
function applyPlazaLeaderboard(data) {
var items = data && Array.isArray(data.items) ? data.items : [];
// CP 广场只展示真实接口里亲密值达到门槛的 CP;接口为空或低于门槛时保持空列表,不再用 mock 用户兜底。
activityData.plazaRows = items
.map(normalizePlazaEntry)
.filter(function (item) {
return Boolean(item);
})
.sort(function (left, right) {
return right.rawScore - left.rawScore;
})
.slice(0, 4);
}
function loadActivityStatus() {
if (!window.HyAppAPI || !window.HyAppAPI.cpWeeklyRank) return;
// 周榜配置和奖励由后端控制;H5 只在接口成功时替换展示数据,失败保持静态兜底,避免活动页因为鉴权或网络问题白屏。
window.HyAppAPI.cpWeeklyRank
.status({ limit: 10 })
.then(function (status) {
applyActivityStatus(status);
render();
})
.catch(function () {});
}
function loadPlazaLeaderboard() {
if (
!window.HyAppAPI ||
!window.HyAppAPI.cpSpace ||
!window.HyAppAPI.cpSpace.intimacyLeaderboard
) {
return;
}
// 后端亲密榜已按亲密值降序聚合;H5 仍二次过滤 5000 门槛并限制 4 个,防止旧数据或异常分页把低亲密 CP 展示出来。
window.HyAppAPI.cpSpace
.intimacyLeaderboard({
relation_type: 'cp',
page: 1,
page_size: 4,
})
.then(function (data) {
applyPlazaLeaderboard(data);
render();
})
.catch(function () {});
}
function render() {
document.title = t('cp.pageTitle', 'Last Weekly CP');
tabContent.innerHTML =
activeTab === 'rank'
? renderRank()
: activeTab === 'rewards'
? renderRewards()
: renderPlaza();
applyTabState();
syncRewardPanelLayout();
updateStageHeight();
updateCountdown();
}
function applyTabState() {
tabs.forEach(function (tab) {
var isActive = tab.getAttribute('data-tab') === activeTab;
tab.classList.toggle('is-active', isActive);
tab.querySelector('img').src = isActive
? './assets/tab-active.png'
: './assets/tab-inactive.png';
});
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 =
activeTab === 'rewards'
? rewardStageHeight
: stageHeights[activeTab] || stageHeights.plaza;
var pageWidth = getPageWidth();
var designScale = pageWidth / DESIGN_WIDTH;
stage.style.minHeight = designHeight + 'px';
stage.style.height = designHeight + 'px';
// 页面保持 1080 设计稿坐标系,大屏只放大到 768px,避免桌面或平板 WebView 把活动页拉得过宽。
document.documentElement.style.setProperty(
'--page-width',
pageWidth + 'px'
);
document.documentElement.style.setProperty(
'--design-scale',
String(designScale)
);
stageWrap.style.height = Math.ceil(designHeight * designScale) + 'px';
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 bindTabs() {
tabs.forEach(function (tab) {
tab.addEventListener('click', function () {
activeTab = tab.getAttribute('data-tab') || 'plaza';
render();
});
});
}
function init() {
countdownEndAt = resolveFallbackEndAt();
bindTabs();
render();
loadPlazaLeaderboard();
loadActivityStatus();
countdownTimer = window.setInterval(updateCountdown, 1000);
window.addEventListener('resize', updateStageHeight);
window.addEventListener('hyapp:i18n-ready', render);
}
window.addEventListener('beforeunload', function () {
if (countdownTimer) window.clearInterval(countdownTimer);
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();