hyapp-h5/activity/cp/script.js
2026-06-25 14:59:55 +08:00

688 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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 [
'<section class="ornate-panel ',
className,
'" style="top:',
top,
'px;height:',
height,
'px">',
'<img class="panel-top" src="./assets/panel-top.png" alt="" aria-hidden="true" />',
'<div class="panel-mid" aria-hidden="true"></div>',
'<img class="panel-bottom" src="./assets/panel-bottom.png" alt="" aria-hidden="true" />',
'<div class="panel-body">',
bodyHTML,
'</div>',
'</section>',
].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 [
'<div class="avatar ',
className,
'">',
'<img src="',
escapeHTML(avatar),
'" alt="" />',
'</div>',
].join('');
}
function renderCPRow(row) {
var center =
row.type === 'score'
? [
'<img class="score-heart" src="./assets/score-heart.png" alt="" aria-hidden="true" />',
'<strong class="score-value">',
escapeHTML(row.score),
'</strong>',
].join('')
: [
'<div class="gift-center">',
'<img src="./assets/gift-plane.png" alt="" aria-hidden="true" />',
'</div>',
].join('');
return [
'<article class="cp-row">',
renderAvatar('avatar-left', row.leftAvatar),
renderAvatar('avatar-right', row.rightAvatar),
'<span class="name name-left">',
escapeHTML(row.leftName),
'</span>',
'<span class="name name-right">',
escapeHTML(row.rightName),
'</span>',
'<img class="deco-line deco-left" src="./assets/deco-line.png" alt="" aria-hidden="true" />',
'<img class="deco-line deco-right" src="./assets/deco-line.png" alt="" aria-hidden="true" />',
center,
'</article>',
].join('');
}
function renderPlaza() {
var plazaRows = activityData.plazaRows || [];
var previousRows = activityData.previousRows || [];
var firstBody = [
'<h2 class="panel-title">',
t(
'cp.plazaGiftTitle',
'CP pairs with intimacy ≥ 5000 are shown here'
),
'</h2>',
'<div class="cp-list">',
plazaRows.map(renderCPRow).join(''),
'</div>',
].join('');
var panels = [renderPanel('plaza-panel-a', 1816, 1152, firstBody)];
if (previousRows.length) {
var secondBody = [
'<h2 class="panel-title">',
t('cp.previousNo1Title', 'Previous Top 3'),
'</h2>',
'<div class="cp-list">',
previousRows.map(renderCPRow).join(''),
'</div>',
].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 [
'<div class="countdown-inline">',
units
.map(function (unit) {
return [
'<div class="time-unit">',
'<img src="./assets/countdown-heart.png" alt="" aria-hidden="true" />',
'<strong data-count="',
unit[0],
'">02</strong>',
'<span>',
t(unit[1], unit[2]),
'</span>',
'</div>',
].join('');
})
.join(''),
'</div>',
].join('');
}
function renderPodiumCard(item) {
var crown =
item.place === 1
? 'rank-crown-gold.png'
: item.place === 2
? 'rank-crown-silver.png'
: 'crown-bronze.png';
return [
'<article class="podium-card rank-',
item.place,
'">',
'<img class="podium-bg" src="./assets/rank-wing-heart.png" alt="" aria-hidden="true" />',
'<img class="podium-crown" src="./assets/',
crown,
'" alt="" aria-hidden="true" />',
renderAvatar('podium-avatar left', item.leftAvatar),
renderAvatar('podium-avatar right', item.rightAvatar),
'<div class="podium-names"><span>',
escapeHTML(item.leftName),
'</span><span>',
escapeHTML(item.rightName),
'</span></div>',
'</article>',
].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 = [
'<section class="rank-podium">',
rankRows.slice(0, 3).map(renderPodiumCard).join(''),
'</section>',
].join('');
var panelBody = [
renderInlineCountdown(),
'<div class="cp-list">',
rankRows.map(renderRankRow).join(''),
'</div>',
].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 [
'<article class="',
cardClass,
'">',
'<div class="reward-icon-box"><img src="',
escapeHTML(cover),
'" alt="" aria-hidden="true" /></div>',
'<div class="reward-name">',
escapeHTML(name),
'</div>',
note
? '<div class="reward-note">' + escapeHTML(note) + '</div>'
: '',
'</article>',
].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 = [
'<img class="reward-crown" src="./assets/',
section.crown,
'" alt="" aria-hidden="true" />',
'<div class="reward-grid">',
section.items.map(renderRewardItem).join(''),
'</div>',
].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();
}
})();