1300 lines
51 KiB
HTML
Raw Permalink 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.

<!doctype html>
<html lang="en" translate="no">
<head>
<meta charset="utf-8" />
<meta name="google" content="notranslate" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
/>
<title>Achievement</title>
<link rel="stylesheet" href="../common/theme.css" />
<style>
* {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
:root {
--app-scale: 1;
--achievement-bg: #130a2b;
--achievement-card-border: #fff09b;
--achievement-card-purple: #5f1e68;
--achievement-card-muted: #130828;
}
html,
body {
margin: 0;
width: 100%;
min-height: 100%;
overflow-x: hidden;
background: var(--achievement-bg);
color: #fff;
font-family: Arial, Helvetica, sans-serif;
}
button {
border: 0;
padding: 0;
background: transparent;
color: inherit;
font: inherit;
cursor: pointer;
outline: none;
}
.app-viewport {
position: relative;
width: 375px;
min-height: 812px;
margin: 0 auto;
transform: scale(var(--app-scale));
transform-origin: top center;
}
.page {
position: relative;
width: 375px;
min-height: 812px;
overflow: hidden;
background: var(--achievement-bg);
}
.stage-bg {
position: absolute;
left: 0;
top: 0;
width: 375px;
height: 667px;
object-fit: cover;
pointer-events: none;
}
.stage-shade {
position: absolute;
left: 0;
right: 0;
top: 212px;
height: 455px;
background: linear-gradient(
180deg,
rgba(19, 10, 43, 0) 0%,
var(--achievement-bg) 20%,
var(--achievement-bg) 100%
);
pointer-events: none;
}
.nav {
position: absolute;
left: 0;
top: 0;
z-index: 6;
width: 375px;
height: 44px;
}
.back {
position: absolute;
left: 16px;
top: 10px;
width: 24px;
height: 24px;
}
.back::before {
content: '';
position: absolute;
left: 8px;
top: 5px;
width: 12px;
height: 12px;
border-left: 2px solid #fff;
border-bottom: 2px solid #fff;
transform: rotate(45deg);
}
.profile {
position: absolute;
left: 107px;
top: 40px;
z-index: 2;
width: 162px;
height: 162px;
}
.profile-avatar {
position: absolute;
left: 36px;
top: 36px;
z-index: 1;
width: 90px;
height: 90px;
border-radius: 50%;
object-fit: cover;
}
.profile-frame {
position: absolute;
inset: 0;
z-index: 2;
width: 162px;
height: 162px;
pointer-events: none;
}
.profile-frame img,
.profile-frame video,
.profile-frame canvas {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
.user-name {
position: absolute;
left: 0;
top: 188px;
z-index: 3;
width: 375px;
overflow: hidden;
padding: 0 32px;
font-size: 18px;
line-height: 18px;
text-align: center;
white-space: nowrap;
text-overflow: ellipsis;
}
.earned {
position: absolute;
left: 0;
top: 218px;
z-index: 3;
width: 375px;
color: rgba(255, 255, 255, 0.6);
font-size: 14px;
line-height: 14px;
text-align: center;
}
.earned strong {
color: #fff;
font-weight: 400;
}
.tabs {
position: absolute;
left: 0;
top: 252px;
z-index: 5;
display: grid;
grid-template-columns: 1fr 1fr;
width: 375px;
height: 34px;
padding: 0 40px;
}
.tab {
position: relative;
height: 34px;
color: rgba(255, 255, 255, 0.5);
font-size: 18px;
line-height: 18px;
text-align: center;
}
.tab.active {
color: #fff;
}
.tab.active::after {
content: '';
position: absolute;
left: 50%;
top: 28px;
width: 16px;
height: 3px;
margin-left: -8px;
border-radius: 32px;
background: #fff;
}
.content {
position: relative;
z-index: 4;
min-height: 812px;
padding-top: 299px;
padding-bottom: 92px;
}
.badge-grid {
display: grid;
grid-template-columns: repeat(3, 104px);
gap: 12px;
width: 336px;
margin: 0 auto;
}
.badge-card {
position: relative;
width: 104px;
height: 116px;
overflow: hidden;
border: 1px solid var(--achievement-card-border);
background: var(--achievement-card-purple);
}
.badge-card:focus,
.badge-card:focus-visible,
.badge-card:active {
outline: none;
box-shadow: none;
transform: none;
}
.badge-card::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
166deg,
rgba(165, 76, 178, 0.92) 36%,
rgba(165, 76, 178, 0) 63%
);
mix-blend-mode: plus-lighter;
pointer-events: none;
}
.badge-card.locked {
background: var(--achievement-card-muted);
}
.badge-card.locked::before {
background: linear-gradient(
166deg,
rgba(70, 42, 120, 0.92) 36%,
rgba(59, 36, 100, 0) 63%
);
}
.badge-card.locked img {
filter: grayscale(1) brightness(0.58);
opacity: 0.72;
}
.badge-card.locked .badge-title {
color: #8f879a;
}
.badge-card img {
position: absolute;
left: 12px;
top: 10px;
width: 80px;
height: 80px;
object-fit: contain;
}
.badge-title {
position: absolute;
left: 4px;
right: 4px;
top: 94px;
z-index: 2;
overflow: hidden;
color: #fff;
font-size: 12px;
line-height: 12px;
text-align: center;
text-transform: capitalize;
white-space: nowrap;
text-overflow: ellipsis;
}
.achievement-loading {
color: rgba(255, 255, 255, 0.72);
}
.achievement-loading .hy-loading__mascot {
width: 76px;
height: 76px;
}
.achievement-loading .hy-loading__spinner {
border-color: rgba(255, 255, 255, 0.22);
border-top-color: #ffffff;
}
.toast {
position: fixed;
left: 50%;
bottom: calc(34px * var(--app-scale));
z-index: 80;
max-width: 300px;
transform: translateX(-50%);
padding: 8px 12px;
border-radius: 16px;
background: rgba(0, 0, 0, 0.62);
color: #fff;
font-size: 12px;
line-height: 16px;
text-align: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.18s ease;
}
.toast.show {
opacity: 1;
}
.achievement-modal {
position: fixed;
left: 50%;
top: 0;
z-index: 50;
display: none;
width: 375px;
height: 812px;
overflow: hidden;
background: rgba(0, 0, 0, 0.7);
transform: translateX(-50%) scale(var(--app-scale));
transform-origin: top center;
}
.achievement-modal.active {
display: block;
}
.modal-badge {
position: absolute;
left: 86px;
top: 180px;
width: 203px;
height: 203px;
object-fit: contain;
}
.modal-badge.locked {
filter: grayscale(1) brightness(0.56);
opacity: 0.72;
}
.modal-title {
position: absolute;
left: 0;
top: 407px;
width: 375px;
padding: 0 24px;
overflow: hidden;
font-size: 24px;
line-height: 24px;
text-align: center;
text-transform: capitalize;
white-space: nowrap;
text-overflow: ellipsis;
}
.modal-status {
position: absolute;
left: 0;
top: 447px;
display: flex;
align-items: center;
justify-content: center;
width: 375px;
height: 16px;
color: #d3d3d3;
font-size: 12px;
line-height: 12px;
}
.modal-status.locked {
color: #8f879a;
}
.modal-status.unlocked {
color: #d3d3d3;
}
.modal-status img {
width: 16px;
height: 16px;
margin-right: 4px;
}
.modal-desc {
position: absolute;
left: 20px;
top: 475px;
width: 335px;
max-height: 72px;
overflow: hidden;
color: #fff;
font-size: 12px;
line-height: 16px;
text-align: center;
}
.collection-panel {
position: absolute;
left: 0;
top: 607px;
width: 375px;
height: 205px;
border: 2px solid var(--achievement-card-border);
background: #2a1351;
background-image: linear-gradient(
173deg,
rgba(70, 42, 120, 0.9) 36%,
rgba(59, 36, 100, 0) 63%
);
}
.panel-border {
position: absolute;
left: 0;
top: 595px;
width: 375px;
height: 31px;
object-fit: cover;
pointer-events: none;
}
.collection-title {
position: absolute;
left: 0;
top: 635px;
width: 375px;
font-size: 16px;
font-weight: 600;
line-height: 16px;
text-align: center;
}
.collector-list {
position: absolute;
left: 65px;
top: 675px;
display: flex;
align-items: center;
height: 45px;
}
.collector-list img {
width: 45px;
height: 45px;
margin-left: -5px;
border-radius: 50%;
object-fit: cover;
}
.collector-list img:first-child {
margin-left: 0;
}
.collector-more {
display: flex;
align-items: center;
justify-content: center;
width: 45px;
height: 45px;
margin-left: -5px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.24);
color: #fff;
font-size: 16px;
line-height: 16px;
}
.owner-text {
position: absolute;
left: 0;
top: 744px;
width: 375px;
color: rgba(255, 255, 255, 0.6);
font-size: 12px;
line-height: 12px;
text-align: center;
}
</style>
</head>
<body>
<div id="appViewport" class="app-viewport">
<main id="page" class="page" aria-label="Achievement">
<img class="stage-bg" src="assets/figma/bg-stage.png" alt="" />
<div class="stage-shade"></div>
<nav class="nav" aria-label="Achievement navigation">
<button
id="backButton"
class="back"
aria-label="Back"
data-i18n-aria="achievement.back"
></button>
</nav>
<section class="profile" aria-label="User">
<img id="profileAvatar" class="profile-avatar" alt="" />
<div
id="profileFrame"
class="profile-frame"
aria-hidden="true"
></div>
</section>
<div id="userName" class="user-name"></div>
<div id="earnedText" class="earned">
Earn <strong id="earnedCount">0</strong> achievements
</div>
<div class="tabs" role="tablist" aria-label="Achievement tabs">
<button
class="tab active"
role="tab"
data-tab="honors"
data-i18n="achievement.honors"
>
Honors
</button>
<button
class="tab"
role="tab"
data-tab="event"
data-i18n="achievement.event"
>
Event
</button>
</div>
<section class="content" aria-live="polite">
<div id="badgeGrid" class="badge-grid"></div>
</section>
</main>
</div>
<div
id="loading"
class="hy-loading achievement-loading"
aria-hidden="true"
>
<div class="hy-loading__mascot" aria-hidden="true"></div>
<div class="hy-loading__spinner" aria-hidden="true"></div>
</div>
<div id="toast" class="toast"></div>
<div id="achievementModal" class="achievement-modal" aria-hidden="true">
<img id="modalBadge" class="modal-badge" alt="" />
<div id="modalTitle" class="modal-title"></div>
<div id="modalStatus" class="modal-status"></div>
<div id="modalDesc" class="modal-desc"></div>
<div class="collection-panel"></div>
<img
class="panel-border"
src="assets/figma/modal-border.png"
alt=""
/>
<div class="collection-title" data-i18n="achievement.collection">
Achievement Collection
</div>
<div id="collectorList" class="collector-list"></div>
<div id="ownerText" class="owner-text"></div>
</div>
<script src="../common/api.js"></script>
<script src="../common/toast.js"></script>
<script src="../common/params.js"></script>
<script src="../common/jsbridge.js"></script>
<script src="../common/i18n.js"></script>
<script src="../common/effect-player.js"></script>
<script>
(function () {
var ASSET = 'assets/figma/';
var tabs = {
honors: { collection_type: 'ordinary' },
event: { collection_type: 'event' },
};
var state = {
tab:
new URLSearchParams(window.location.search).get(
'tab'
) || 'honors',
targetUserID: normalizedQueryUserID(),
loading: false,
toastTimer: 0,
profile: null,
appearance: {},
profileFrameURL: '',
profileFramePlayer: null,
achievements: { honors: [], event: [] },
loaded: { honors: false, event: false },
selected: null,
};
function byId(id) {
return document.getElementById(id);
}
function t(key, fallback) {
return window.HyAppI18n && window.HyAppI18n.t
? window.HyAppI18n.t(key, fallback)
: fallback || key;
}
function replaceToken(text, key, value) {
return String(text || '').replace(
new RegExp('\\{' + key + '\\}', 'g'),
String(value)
);
}
function escapeHTML(value) {
return String(value || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function validTab(value) {
return tabs[value] ? value : 'honors';
}
function normalizedQueryUserID() {
var value = String(
new URLSearchParams(window.location.search).get(
'user_id'
) || ''
).trim();
return /^\d+$/.test(value) && Number(value) > 0
? value
: '';
}
function hasAccessToken() {
return !!(
window.HyAppAPI &&
window.HyAppAPI.getAccessToken &&
window.HyAppAPI.getAccessToken()
);
}
function canLoadAchievements() {
return !!(
window.HyAppAPI &&
window.HyAppAPI.achievement &&
(state.targetUserID || hasAccessToken())
);
}
function showToast(message) {
var toast = byId('toast');
toast.textContent = message;
toast.classList.add('show');
window.clearTimeout(state.toastTimer);
state.toastTimer = window.setTimeout(function () {
toast.classList.remove('show');
}, 1800);
}
function resizeApp() {
var page = byId('page');
var contentHeight = Math.max(
812,
page ? page.scrollHeight : 812
);
var baseWidth = 375;
var maxRenderedWidth = 768;
var scale = Math.min(
window.innerWidth / baseWidth,
maxRenderedWidth / baseWidth
);
document.documentElement.style.setProperty(
'--app-scale',
String(scale)
);
byId('appViewport').style.height = contentHeight + 'px';
document.body.style.minHeight =
Math.ceil(contentHeight * scale) + 'px';
}
function profileSource(current) {
var user = current && current.user;
var data =
(user && (user.profile || user.user || user)) || {};
return data && data.profile && !data.nickname
? data.profile
: data;
}
function profileName(profile) {
return (
profile.nickname ||
profile.nick_name ||
profile.nickName ||
profile.username ||
profile.user_name ||
profile.name ||
profile.display_name ||
profile.displayName ||
profile.display_user_id ||
profile.displayUserId ||
profile.user_id ||
profile.userId ||
''
);
}
function profileAvatar(profile) {
return (
profile.avatar ||
profile.user_avatar ||
profile.userAvatar ||
profile.avatar_url ||
profile.avatarUrl ||
profile.profile_avatar ||
profile.profileAvatar ||
''
);
}
function normalizeAppearance(data) {
return (
(data &&
(data.appearance ||
data.user_appearance ||
data.userAppearance ||
data.profile_appearance ||
data.profileAppearance ||
data)) ||
{}
);
}
function frameURL(frameData) {
frameData = frameData || {};
if (typeof frameData === 'string') return frameData;
return (
frameData.asset_url ||
frameData.preview_url ||
frameData.animation_url ||
frameData.url ||
frameData.resource_url ||
frameData.resourceUrl ||
''
);
}
function profileFrameData(profile) {
profile = profile || {};
var appearance = state.appearance || {};
return (
appearance.avatar_frame ||
appearance.avatarFrame ||
appearance.profile_frame ||
appearance.profileFrame ||
profile.avatar_frame ||
profile.avatarFrame ||
profile.profile_frame ||
profile.profileFrame ||
profile.frame ||
{}
);
}
function setImage(node, src, fallback) {
var next = src || fallback || '';
node.onerror = function () {
if (fallback && node.getAttribute('src') !== fallback) {
node.src = fallback;
}
};
if (!next) {
node.removeAttribute('src');
return;
}
if (node.getAttribute('src') !== next) node.src = next;
}
function renderStaticFrame(node, src) {
if (!src) {
node.innerHTML = '';
return;
}
node.innerHTML =
'<img src="' + escapeHTML(src) + '" alt="">';
var img = node.querySelector('img');
img.onerror = function () {
img.onerror = null;
state.profileFrameURL = '';
renderStaticFrame(node, '');
};
}
function isDynamicEffect(url) {
if (
!window.HyAppEffectPlayer ||
!window.HyAppEffectPlayer.inferType
) {
return false;
}
return Boolean(window.HyAppEffectPlayer.inferType(url));
}
function renderProfileFrame(profile) {
var node = byId('profileFrame');
if (!node) return;
var url = frameURL(profileFrameData(profile));
if (state.profileFrameURL === url) return;
state.profileFrameURL = url;
if (state.profileFramePlayer) {
state.profileFramePlayer.destroy();
state.profileFramePlayer = null;
}
if (isDynamicEffect(url)) {
state.profileFramePlayer =
window.HyAppEffectPlayer.create(node, {
fit: 'contain',
waitForWindowLoad: false,
});
state.profileFramePlayer.play(url).catch(function () {
state.profileFrameURL = '';
renderStaticFrame(node, '');
});
return;
}
renderStaticFrame(node, url);
}
function parseJSON(text) {
try {
return text ? JSON.parse(text) : {};
} catch (_) {
return {};
}
}
function definitionOf(item) {
return (item && item.definition) || {};
}
function configOf(item) {
return parseJSON(definitionOf(item).display_config_json);
}
function resourceOf(item) {
var def = definitionOf(item);
return def.primary_badge_resource || {};
}
function resourceImage(resource, unlocked) {
return (
resource.preview_url ||
resource.asset_url ||
resource.animation_url ||
resource.url ||
''
);
}
function isUnlocked(item) {
var status = String(item && item.user_status).toLowerCase();
return (
status === 'unlocked' ||
status === 'completed' ||
Number((item && item.unlocked_at_ms) || 0) > 0
);
}
function sortOrderOf(item) {
var def = definitionOf(item);
var value =
def.sort_order !== undefined
? def.sort_order
: def.sortOrder;
var numeric = Number(value || 0);
return Number.isFinite(numeric) ? numeric : 0;
}
function sortAchievementsForBadgePage(items) {
return (items || [])
.map(function (item, index) {
return { index: index, item: item };
})
.sort(function (left, right) {
var leftUnlocked = isUnlocked(left.item) ? 1 : 0;
var rightUnlocked = isUnlocked(right.item) ? 1 : 0;
if (leftUnlocked !== rightUnlocked) {
return rightUnlocked - leftUnlocked;
}
var sortDelta =
sortOrderOf(right.item) -
sortOrderOf(left.item);
if (sortDelta !== 0) return sortDelta;
return left.index - right.index;
})
.map(function (entry) {
return entry.item;
});
}
function titleOf(item) {
var resource = resourceOf(item);
var def = definitionOf(item);
return def.title || resource.name || '';
}
function descriptionOf(item) {
var config = configOf(item);
var def = definitionOf(item);
return (
config.unlock_text ||
config.unlock_method ||
config.how_to_get ||
def.description ||
''
);
}
function ownerCountOf(item) {
var config = configOf(item);
var candidates = [
item && item.owner_count,
item && item.collector_count,
definitionOf(item).owner_count,
config.owner_count,
config.collector_count,
config.collected_count,
];
for (var i = 0; i < candidates.length; i += 1) {
var value = Number(candidates[i] || 0);
if (value > 0) return value;
}
return 0;
}
function collectorAvatarsOf(item) {
var config = configOf(item);
var list = Array.isArray(config.collectors)
? config.collectors
: Array.isArray(config.owner_avatars)
? config.owner_avatars
: [];
return list
.map(function (owner) {
return typeof owner === 'string'
? owner
: owner &&
(owner.avatar ||
owner.avatar_url ||
owner.avatarUrl);
})
.filter(Boolean)
.slice(0, 5);
}
function formatDate(ms) {
var value = Number(ms || 0);
if (!value) return '';
try {
var date = new Date(value);
var year = date.getFullYear();
var month = String(date.getMonth() + 1).padStart(
2,
'0'
);
var day = String(date.getDate()).padStart(2, '0');
return year + '/' + month + '/' + day;
} catch (_) {
return new Date(value)
.toISOString()
.slice(0, 10)
.replace(/-/g, '/');
}
}
function unlockedCount() {
return []
.concat(state.achievements.honors)
.concat(state.achievements.event)
.filter(isUnlocked).length;
}
function earnedText(count) {
var text = t(
'achievement.earned',
'Earn {count} achievements'
);
return replaceToken(
text,
'count',
'<strong>' + count + '</strong>'
);
}
function renderHeader() {
var profile = state.profile || {};
byId('userName').textContent = profileName(profile);
setImage(byId('profileAvatar'), profileAvatar(profile), '');
renderProfileFrame(profile);
byId('earnedText').innerHTML =
state.loaded.honors || state.loaded.event
? earnedText(unlockedCount())
: '';
}
function cardHTML(item, index) {
var unlocked = isUnlocked(item);
var resource = resourceOf(item);
var img = resourceImage(resource, unlocked);
return (
'<button class="badge-card ' +
(unlocked ? 'unlocked' : 'locked') +
'" data-index="' +
index +
'" aria-label="' +
escapeHTML(titleOf(item)) +
'">' +
(img
? '<img src="' + escapeHTML(img) + '" alt="">'
: '') +
'<div class="badge-title">' +
escapeHTML(titleOf(item)) +
'</div>' +
'</button>'
);
}
function renderContent() {
state.tab = validTab(state.tab);
var list = state.achievements[state.tab] || [];
document.querySelectorAll('.tab').forEach(function (node) {
var active = node.dataset.tab === state.tab;
node.classList.toggle('active', active);
node.setAttribute(
'aria-selected',
active ? 'true' : 'false'
);
});
byId('badgeGrid').innerHTML = list.map(cardHTML).join('');
byId('loading').classList.toggle('active', state.loading);
byId('loading').setAttribute(
'aria-hidden',
state.loading ? 'false' : 'true'
);
resizeApp();
}
function render() {
renderHeader();
renderContent();
}
function normalizeAchievements(data) {
return ((data && data.achievements) || []).filter(
function (item) {
return item && item.definition;
}
);
}
function achievementQuery(name, page) {
var cfg = tabs[name] || tabs.honors;
var query = {
collection_type: cfg.collection_type,
collection_id: cfg.collection_id || '',
page: page,
page_size: 60,
};
if (state.targetUserID) query.user_id = state.targetUserID;
return query;
}
function fetchAchievementList(name, page, acc) {
var nextPage = page || 1;
var items = acc || [];
return window.HyAppAPI.achievement
.list(achievementQuery(name, nextPage), {
skipAuth: !!state.targetUserID,
})
.then(function (data) {
var current = normalizeAchievements(data);
var merged = items.concat(current);
var total = Number(
data && data.total ? data.total : 0
);
// 后端成就列表是分页接口H5 需要完整 tab 数据来同时渲染列表和顶部已获得数量,所以按 total 继续补页。
if (current.length > 0 && total > merged.length) {
return fetchAchievementList(
name,
nextPage + 1,
merged
);
}
// 成就接口按配置排序分页返回H5 徽章墙需要先把所有页收齐,再按用户已解锁优先重排,避免未解锁项插在已解锁徽章之间。
return sortAchievementsForBadgePage(merged);
});
}
function loadTab(name) {
state.tab = validTab(name);
if (window.history && window.history.replaceState) {
var url = new URL(window.location.href);
url.searchParams.set('tab', state.tab);
window.history.replaceState(null, '', url.toString());
}
render();
if (state.loaded[state.tab]) return Promise.resolve();
if (!canLoadAchievements()) {
state.loaded[state.tab] = true;
render();
return Promise.resolve();
}
state.loading = true;
render();
// Gateway 只开放 collection_type / collection_id 过滤tab 不拼 achievement_type避免把活动类型误当集合类型传给后端。
return fetchAchievementList(state.tab)
.then(function (data) {
state.achievements[state.tab] = data;
state.loaded[state.tab] = true;
})
.catch(function (error) {
state.loaded[state.tab] = true;
showToast(
(error && error.message) ||
t('achievement.loadFailed', 'Load failed')
);
})
.finally(function () {
state.loading = false;
render();
});
}
function loadUserProfile() {
var tasks = [];
var hasToken = hasAccessToken();
if (
state.targetUserID &&
window.HyAppAPI &&
window.HyAppAPI.user
) {
if (window.HyAppAPI.user.profile) {
tasks.push(
window.HyAppAPI.user
.profile(state.targetUserID, {
skipAuth: true,
})
.then(function (data) {
state.profile = profileSource(data);
renderHeader();
return data;
})
.catch(function () {
return null;
})
);
}
if (window.HyAppAPI.user.appearance) {
tasks.push(
window.HyAppAPI.user
.appearance(state.targetUserID, {
skipAuth: true,
})
.then(function (data) {
state.appearance =
normalizeAppearance(data);
renderHeader();
return data;
})
.catch(function () {
state.appearance = {};
renderHeader();
return null;
})
);
}
} else if (
window.HyAppParams &&
window.HyAppParams.loadUser
) {
tasks.push(
window.HyAppParams.loadUser()
.then(function (current) {
state.profile = profileSource(current);
renderHeader();
return current;
})
.catch(function () {
return null;
})
);
}
if (
!state.targetUserID &&
hasToken &&
window.HyAppAPI.user &&
window.HyAppAPI.user.appearance
) {
tasks.push(
window.HyAppAPI.user
.appearance()
.then(function (data) {
state.appearance =
normalizeAppearance(data);
renderHeader();
return data;
})
.catch(function () {
state.appearance = {};
renderHeader();
return null;
})
);
}
if (!tasks.length) {
return Promise.resolve(null);
}
// 用户资料和装扮是两个独立接口:任一失败都不能阻断成就页,成功的那部分立即刷新头部。
return Promise.all(tasks);
}
function loadCounts() {
var active = state.tab;
var other = active === 'honors' ? 'event' : 'honors';
if (state.loaded[other]) return Promise.resolve();
if (!canLoadAchievements()) {
state.loaded[other] = true;
renderHeader();
return Promise.resolve();
}
// Earned count spans both tabs, but preloading the hidden tab must not mutate the visible tab or browser URL.
return fetchAchievementList(other)
.then(function (data) {
state.achievements[other] = data;
state.loaded[other] = true;
renderHeader();
})
.catch(function () {
state.loaded[other] = true;
renderHeader();
});
}
function renderCollectors(item) {
var custom = collectorAvatarsOf(item);
byId('collectorList').innerHTML =
custom
.slice(0, 5)
.map(function (src) {
return (
'<img src="' + escapeHTML(src) + '" alt="">'
);
})
.join('') +
(custom.length
? '<div class="collector-more">...</div>'
: '');
}
function openModal(item) {
state.selected = item;
var unlocked = isUnlocked(item);
var resource = resourceOf(item);
setImage(
byId('modalBadge'),
resourceImage(resource, unlocked),
''
);
byId('modalBadge').classList.toggle('locked', !unlocked);
byId('modalTitle').textContent = titleOf(item);
byId('modalDesc').textContent = descriptionOf(item);
byId('modalStatus').classList.toggle('unlocked', unlocked);
byId('modalStatus').classList.toggle('locked', !unlocked);
if (unlocked) {
var date = formatDate(item.unlocked_at_ms);
byId('modalStatus').textContent = date
? replaceToken(
t(
'achievement.collectedWithDate',
'Collected on {date}'
),
'date',
date
)
: t('achievement.collected', 'Collected');
} else {
byId('modalStatus').textContent = t(
'achievement.notCollected',
'Not Collected'
);
}
var owners = ownerCountOf(item);
byId('ownerText').textContent = owners
? replaceToken(
t(
'achievement.ownerCount',
'Only {count} people own it'
),
'count',
owners
)
: '';
renderCollectors(item);
byId('achievementModal').classList.add('active');
byId('achievementModal').setAttribute(
'aria-hidden',
'false'
);
}
function closeModal() {
state.selected = null;
byId('achievementModal').classList.remove('active');
byId('achievementModal').setAttribute(
'aria-hidden',
'true'
);
}
byId('backButton').addEventListener('click', function () {
if (state.selected) {
closeModal();
return;
}
window.HyAppBridge.back();
});
document
.querySelector('.tabs')
.addEventListener('click', function (event) {
var target = event.target.closest('[data-tab]');
if (!target) return;
loadTab(target.dataset.tab);
});
byId('badgeGrid').addEventListener('click', function (event) {
var target = event.target.closest('[data-index]');
if (!target) return;
target.blur();
var list = state.achievements[state.tab] || [];
var item = list[Number(target.dataset.index)];
if (item) openModal(item);
});
byId('achievementModal').addEventListener(
'click',
function (event) {
if (event.target === byId('achievementModal'))
closeModal();
}
);
window.addEventListener('resize', resizeApp);
window.addEventListener('orientationchange', function () {
window.requestAnimationFrame(resizeApp);
});
window.addEventListener('hyapp:i18n-ready', render);
render();
window.HyAppBridge.ready({ page: 'achievement' });
loadTab(state.tab).finally(function () {
loadUserProfile();
loadCounts();
});
})();
</script>
</body>
</html>