hyapp-h5/activity/cp/script.js
2026-07-16 18:52:12 +08:00

737 lines
25 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 PLAZA_VISIBLE_ROWS = 4;
var PLAZA_ROW_HEIGHT = 199;
var PLAZA_ROW_GAP = 24;
var PLAZA_SECONDS_PER_ROW = 3.2;
var PLAZA_FEED_LIMIT = 20;
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.webp" alt="" aria-hidden="true" />',
'<div class="panel-mid" aria-hidden="true"></div>',
'<img class="panel-bottom" src="./assets/panel-bottom.webp" 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">',
'<bdi>',
escapeHTML(row.leftName),
'</bdi>',
'</span>',
'<span class="name name-right">',
'<bdi>',
escapeHTML(row.rightName),
'</bdi>',
'</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 renderPlazaCarousel(rows) {
var sourceRows = Array.isArray(rows) ? rows : [];
var shouldLoop = sourceRows.length > PLAZA_VISIBLE_ROWS;
var rowHTML = sourceRows.map(renderCPRow).join('');
var loopDistance =
sourceRows.length * (PLAZA_ROW_HEIGHT + PLAZA_ROW_GAP);
var loopDuration = Math.max(
sourceRows.length * PLAZA_SECONDS_PER_ROW,
PLAZA_VISIBLE_ROWS * PLAZA_SECONDS_PER_ROW
);
// 一组数据末尾保留一个 row-gap再紧接完全相同的第二组轨道移动一整组高度后第二组首行与初始首行位置一致循环重置时不会闪白或跳帧。
return [
'<div class="plaza-carousel-viewport">',
'<div class="plaza-carousel-track',
shouldLoop ? ' is-looping' : '',
'" style="--plaza-loop-distance:',
loopDistance,
'px;--plaza-loop-duration:',
loopDuration,
's">',
'<div class="plaza-carousel-set">',
rowHTML,
'</div>',
shouldLoop
? '<div class="plaza-carousel-set" aria-hidden="true">' +
rowHTML +
'</div>'
: '',
'</div>',
'</div>',
].join('');
}
function renderPlaza() {
var plazaRows = activityData.plazaRows || [];
var previousRows = activityData.previousRows || [];
var firstBody = [
'<h2 class="panel-title">',
t(
'cp.plazaGiftTitle',
'CP formation gifts worth more than 5,000 coins are shown here'
),
'</h2>',
renderPlazaCarousel(plazaRows),
].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 normalizeFormationGiftEntry(entry) {
var giftCoinValue = Number(
(entry &&
(entry.gift_coin_value ||
entry.giftCoinValue ||
entry.coin_spent ||
entry.coinSpent)) ||
0
);
var requester = normalizePairUser(
entry && (entry.requester || entry.user_a || entry.userA)
);
var target = normalizePairUser(
entry && (entry.target || entry.user_b || entry.userB)
);
// 业务定义是“超过 5000 金币”,不是大于等于;即使旧网关误返回 5000 或更低的记录H5 也不能把它展示出来。
if (
!Number.isFinite(giftCoinValue) ||
giftCoinValue <= 5000 ||
!requester ||
!target
)
return null;
return {
type: 'score',
rawGiftCoinValue: giftCoinValue,
score: formatScore(giftCoinValue),
leftName: requester.name,
rightName: target.name,
leftAvatar: requester.avatar,
rightAvatar: target.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 applyPlazaFormationGiftFeed(data) {
var items = data && Array.isArray(data.items) ? data.items : [];
// feed 由后端按 CP 成立时间倒序返回H5 只做口径校验并保留返回顺序,不再按金币额排成另一个榜单。
activityData.plazaRows = items
.map(normalizeFormationGiftEntry)
.filter(function (item) {
return Boolean(item);
});
}
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 loadPlazaFormationGiftFeed() {
if (
!window.HyAppAPI ||
!window.HyAppAPI.cpSpace ||
!window.HyAppAPI.cpSpace.formationGiftFeed
) {
return;
}
// 20 条可让四行视窗持续轮播,同时限制头像 DOM 和图片解码数量gateway/user-service 还会再次夹紧上限,避免参数被篡改后无界拉取。
window.HyAppAPI.cpSpace
.formationGiftFeed({ limit: PLAZA_FEED_LIMIT })
.then(function (data) {
applyPlazaFormationGiftFeed(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();
loadPlazaFormationGiftFeed();
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();
}
})();