550 lines
18 KiB
JavaScript
550 lines
18 KiB
JavaScript
(function () {
|
||
var DESIGN_WIDTH = 1080;
|
||
var MAX_PAGE_WIDTH = 768;
|
||
var STORAGE_KEY = 'hy_cp_activity_end_at';
|
||
var stageHeights = {
|
||
plaza: 4283,
|
||
rank: 3670,
|
||
rewards: 4797,
|
||
};
|
||
var activeTab = 'plaza';
|
||
var countdownEndAt = 0;
|
||
var countdownTimer = 0;
|
||
var activityData = {
|
||
rank: null,
|
||
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 = {
|
||
plaza: {
|
||
exchangeRows: buildRows('gift', 4),
|
||
previousRows: buildRows('score', 4),
|
||
},
|
||
rank: [
|
||
{
|
||
place: 1,
|
||
score: '1,234,567',
|
||
leftName: 'Namename',
|
||
rightName: 'Namename',
|
||
},
|
||
{
|
||
place: 2,
|
||
score: '987,654',
|
||
leftName: 'Namename',
|
||
rightName: 'Namename',
|
||
},
|
||
{
|
||
place: 3,
|
||
score: '876,543',
|
||
leftName: 'Namename',
|
||
rightName: 'Namename',
|
||
},
|
||
],
|
||
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 buildRows(type, count) {
|
||
var rows = [];
|
||
for (var index = 0; index < count; index += 1) {
|
||
rows.push({
|
||
type: type,
|
||
leftName: 'Namename',
|
||
rightName: 'Namename',
|
||
score: '1234567',
|
||
});
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
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, '"')
|
||
.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 [
|
||
'<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 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">',
|
||
'<div class="avatar avatar-left"><img src="',
|
||
escapeHTML(row.leftAvatar || './assets/avatar-sample.png'),
|
||
'" alt="" /></div>',
|
||
'<div class="avatar avatar-right"><img src="',
|
||
escapeHTML(row.rightAvatar || './assets/avatar-sample.png'),
|
||
'" alt="" /></div>',
|
||
'<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 firstBody = [
|
||
'<h2 class="panel-title">',
|
||
t(
|
||
'cp.plazaGiftTitle',
|
||
'Gifts exchanged between CPs ≥ 5000 gold will be displayed here'
|
||
),
|
||
'</h2>',
|
||
'<p class="panel-subtitle">',
|
||
t(
|
||
'cp.plazaGiftSubtitle',
|
||
'(Display the last 10 records, sorted in ascending order.)'
|
||
),
|
||
'</p>',
|
||
'<div class="cp-list">',
|
||
mockData.plaza.exchangeRows.map(renderCPRow).join(''),
|
||
'</div>',
|
||
].join('');
|
||
var secondBody = [
|
||
'<h2 class="panel-title">',
|
||
t('cp.previousNo1Title', 'The previous number one position'),
|
||
'</h2>',
|
||
'<p class="panel-subtitle">',
|
||
t(
|
||
'cp.plazaGiftSubtitle',
|
||
'(Display the last 10 records, sorted in ascending order.)'
|
||
),
|
||
'</p>',
|
||
'<div class="cp-list">',
|
||
mockData.plaza.previousRows.map(renderCPRow).join(''),
|
||
'</div>',
|
||
].join('');
|
||
|
||
return [
|
||
renderPanel('plaza-panel-a', 1816, 1152, firstBody),
|
||
renderPanel('plaza-panel-b', 3024, 1126, secondBody),
|
||
].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" />',
|
||
'<div class="avatar podium-avatar left"><img src="',
|
||
escapeHTML(item.leftAvatar || './assets/avatar-sample.png'),
|
||
'" alt="" /></div>',
|
||
'<div class="avatar podium-avatar right"><img src="',
|
||
escapeHTML(item.rightAvatar || './assets/avatar-sample.png'),
|
||
'" alt="" /></div>',
|
||
'<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,
|
||
score: item.score,
|
||
});
|
||
}
|
||
|
||
function renderRank() {
|
||
var rankRows = activityData.rank || mockData.rank;
|
||
var podium = [
|
||
'<section class="rank-podium">',
|
||
rankRows.slice(0, 3).map(renderPodiumCard).join(''),
|
||
'</section>',
|
||
].join('');
|
||
var panelRows = rankRows.length ? rankRows : mockData.rank;
|
||
var panelBody = [
|
||
renderInlineCountdown(),
|
||
'<div class="cp-list">',
|
||
panelRows.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';
|
||
return [
|
||
'<article class="reward-card">',
|
||
'<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 = [1816, 2784, 3752][index];
|
||
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, 912, body);
|
||
}
|
||
|
||
function renderRewards() {
|
||
return (activityData.rewards || mockData.rewards)
|
||
.map(renderRewardPanel)
|
||
.join('');
|
||
}
|
||
|
||
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 normalizeRankEntry(entry) {
|
||
var userA = normalizeUser(entry.user_a);
|
||
var userB = normalizeUser(entry.user_b);
|
||
return {
|
||
place: Number(entry.rank || 0),
|
||
score: formatScore(entry.score),
|
||
leftName: userA.username || userA.display_user_id || 'Namename',
|
||
rightName: userB.username || userB.display_user_id || 'Namename',
|
||
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 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) {
|
||
return {
|
||
name: rewardName(item),
|
||
note:
|
||
item.duration_days > 0
|
||
? item.duration_days + ' days'
|
||
: item.quantity > 0
|
||
? 'x' + item.quantity
|
||
: '',
|
||
coverUrl: item.cover_url || '',
|
||
};
|
||
}),
|
||
};
|
||
}
|
||
|
||
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) && status.entries.length) {
|
||
activityData.rank = status.entries.map(normalizeRankEntry);
|
||
}
|
||
if (Array.isArray(status.rewards) && status.rewards.length) {
|
||
activityData.rewards = status.rewards.map(normalizeRewardSection);
|
||
}
|
||
}
|
||
|
||
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 render() {
|
||
document.title = t('cp.pageTitle', 'Last Weekly CP');
|
||
tabContent.innerHTML =
|
||
activeTab === 'rank'
|
||
? renderRank()
|
||
: activeTab === 'rewards'
|
||
? renderRewards()
|
||
: renderPlaza();
|
||
applyTabState();
|
||
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 = 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();
|
||
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();
|
||
}
|
||
})();
|