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