(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 [ '', ].join(''); } function renderPodiumAvatar(className, avatar, name) { var imageURL = safeImageURL(avatar); return [ '', ].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(); } })();