1163 lines
44 KiB
HTML
1163 lines
44 KiB
HTML
<!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;
|
||
width: 90px;
|
||
height: 90px;
|
||
border-radius: 50%;
|
||
object-fit: cover;
|
||
}
|
||
.profile-frame {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 162px;
|
||
height: 162px;
|
||
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::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 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;
|
||
}
|
||
.empty-state {
|
||
display: none;
|
||
width: 375px;
|
||
padding-top: 60px;
|
||
text-align: center;
|
||
}
|
||
.empty-state.active {
|
||
display: block;
|
||
}
|
||
.empty-state img {
|
||
display: block;
|
||
width: 146px;
|
||
height: 143px;
|
||
margin: 0 auto 24px;
|
||
object-fit: contain;
|
||
}
|
||
.empty-text,
|
||
.footer-text {
|
||
color: rgba(255, 255, 255, 0.6);
|
||
font-size: 10px;
|
||
line-height: 10px;
|
||
text-align: center;
|
||
}
|
||
.footer-text {
|
||
margin-top: 24px;
|
||
}
|
||
.loading {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 320px;
|
||
z-index: 7;
|
||
display: none;
|
||
width: 375px;
|
||
color: rgba(255, 255, 255, 0.72);
|
||
font-size: 12px;
|
||
line-height: 16px;
|
||
text-align: center;
|
||
}
|
||
.loading.active {
|
||
display: block;
|
||
}
|
||
.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-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 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: justify;
|
||
}
|
||
.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"
|
||
src="assets/figma/profile-avatar.png"
|
||
alt=""
|
||
/>
|
||
<img
|
||
class="profile-frame"
|
||
src="assets/figma/header-frame.png"
|
||
alt=""
|
||
/>
|
||
</section>
|
||
<div id="userName" class="user-name">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>
|
||
<div id="emptyState" class="empty-state">
|
||
<img src="assets/figma/empty-cat.png" alt="" />
|
||
<div
|
||
class="empty-text"
|
||
data-i18n="achievement.stayTuned"
|
||
>
|
||
Stay Tuned For More Achievements!
|
||
</div>
|
||
</div>
|
||
<div
|
||
id="footerText"
|
||
class="footer-text"
|
||
data-i18n="achievement.stayTuned"
|
||
>
|
||
Stay Tuned For More Achievements!
|
||
</div>
|
||
</section>
|
||
<div
|
||
id="loading"
|
||
class="loading"
|
||
data-i18n="achievement.loading"
|
||
>
|
||
Loading...
|
||
</div>
|
||
</main>
|
||
</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>
|
||
(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',
|
||
loading: false,
|
||
toastTimer: 0,
|
||
profile: 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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
function validTab(value) {
|
||
return tabs[value] ? value : 'honors';
|
||
}
|
||
function isLocalOrigin() {
|
||
var host = window.location.hostname;
|
||
return (
|
||
host === '127.0.0.1' ||
|
||
host === 'localhost' ||
|
||
host === '::1'
|
||
);
|
||
}
|
||
function previewMode() {
|
||
if (!isLocalOrigin()) return '';
|
||
return String(
|
||
new URLSearchParams(window.location.search).get(
|
||
'preview'
|
||
) || ''
|
||
).toLowerCase();
|
||
}
|
||
function previewAchievement(index, unlocked) {
|
||
return {
|
||
definition: {
|
||
achievement_id: 'preview_' + index,
|
||
title: 'Pirple Pearl',
|
||
description:
|
||
'Complete the required achievement goal to collect this badge.',
|
||
display_config_json: JSON.stringify({
|
||
owner_count: 10,
|
||
}),
|
||
primary_badge_resource: {
|
||
name: 'Pirple Pearl',
|
||
preview_url:
|
||
ASSET +
|
||
(unlocked
|
||
? 'badge-gold.png'
|
||
: 'badge-silver.png'),
|
||
},
|
||
},
|
||
user_status: unlocked ? 'unlocked' : 'in_progress',
|
||
unlocked_at_ms: unlocked
|
||
? new Date('2026-01-01T00:00:00Z').getTime()
|
||
: 0,
|
||
};
|
||
}
|
||
function applyLocalPreview() {
|
||
var mode = previewMode();
|
||
if (!mode) return false;
|
||
var sample = [0, 1, 2, 3, 4, 5, 6, 7, 8].map(
|
||
function (_, index) {
|
||
return previewAchievement(
|
||
index + 1,
|
||
mode === 'unlocked' ||
|
||
(mode !== 'locked' &&
|
||
(index === 0 || index >= 3))
|
||
);
|
||
}
|
||
);
|
||
state.profile = {
|
||
name: 'Name',
|
||
avatar: ASSET + 'profile-avatar.png',
|
||
};
|
||
state.achievements.honors = mode === 'empty' ? [] : sample;
|
||
state.achievements.event = [];
|
||
state.loaded.honors = true;
|
||
state.loaded.event = true;
|
||
render();
|
||
if (mode === 'locked') openModal(sample[1]);
|
||
if (mode === 'unlocked') openModal(sample[0]);
|
||
return true;
|
||
}
|
||
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;
|
||
return (user && (user.profile || user.user || user)) || {};
|
||
}
|
||
function profileName(profile) {
|
||
return (
|
||
profile.nickname ||
|
||
profile.nick_name ||
|
||
profile.name ||
|
||
profile.display_name ||
|
||
profile.displayName ||
|
||
'Name'
|
||
);
|
||
}
|
||
function profileAvatar(profile) {
|
||
return (
|
||
profile.avatar ||
|
||
profile.user_avatar ||
|
||
profile.userAvatar ||
|
||
profile.avatar_url ||
|
||
profile.avatarUrl ||
|
||
profile.profile_avatar ||
|
||
profile.profileAvatar ||
|
||
''
|
||
);
|
||
}
|
||
function setImage(node, src, fallback) {
|
||
var next = src || fallback || '';
|
||
node.onerror = function () {
|
||
if (fallback && node.getAttribute('src') !== fallback) {
|
||
node.src = fallback;
|
||
}
|
||
};
|
||
if (node.getAttribute('src') !== next) node.src = next;
|
||
}
|
||
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 ||
|
||
ASSET +
|
||
(unlocked ? 'badge-gold.png' : 'badge-silver.png')
|
||
);
|
||
}
|
||
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 titleOf(item) {
|
||
var resource = resourceOf(item);
|
||
var def = definitionOf(item);
|
||
return resource.name || def.title || 'Pirple Pearl';
|
||
}
|
||
function descriptionOf(item) {
|
||
var config = configOf(item);
|
||
var def = definitionOf(item);
|
||
return (
|
||
config.unlock_text ||
|
||
config.unlock_method ||
|
||
config.how_to_get ||
|
||
def.description ||
|
||
t(
|
||
'achievement.defaultHowTo',
|
||
'Complete the required achievement goal to collect this badge.'
|
||
)
|
||
);
|
||
}
|
||
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 {
|
||
return new Date(value).toLocaleDateString(
|
||
window.HyAppI18n && window.HyAppI18n.lang
|
||
? window.HyAppI18n.lang()
|
||
: 'en',
|
||
{
|
||
year: 'numeric',
|
||
month: 'long',
|
||
day: 'numeric',
|
||
}
|
||
);
|
||
} catch (_) {
|
||
return new Date(value).toISOString().slice(0, 10);
|
||
}
|
||
}
|
||
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),
|
||
ASSET + 'profile-avatar.png'
|
||
);
|
||
byId('earnedText').innerHTML = 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 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('');
|
||
var empty = !state.loading && list.length === 0;
|
||
byId('emptyState').classList.toggle('active', empty);
|
||
byId('footerText').style.display = empty ? 'none' : 'block';
|
||
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;
|
||
return {
|
||
collection_type: cfg.collection_type,
|
||
collection_id: cfg.collection_id || '',
|
||
page: page,
|
||
page_size: 60,
|
||
};
|
||
}
|
||
function fetchAchievementList(name, page, acc) {
|
||
var nextPage = page || 1;
|
||
var items = acc || [];
|
||
return window.HyAppAPI.achievement
|
||
.list(achievementQuery(name, nextPage))
|
||
.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
|
||
);
|
||
}
|
||
return 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 (
|
||
!window.HyAppAPI ||
|
||
!window.HyAppAPI.achievement ||
|
||
!window.HyAppAPI.getAccessToken()
|
||
) {
|
||
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() {
|
||
if (!window.HyAppParams || !window.HyAppParams.loadUser) {
|
||
return Promise.resolve(null);
|
||
}
|
||
return window.HyAppParams.loadUser()
|
||
.then(function (current) {
|
||
state.profile = profileSource(current);
|
||
renderHeader();
|
||
return current;
|
||
})
|
||
.catch(function () {
|
||
return null;
|
||
});
|
||
}
|
||
function loadCounts() {
|
||
var active = state.tab;
|
||
var other = active === 'honors' ? 'event' : 'honors';
|
||
if (state.loaded[other]) return Promise.resolve();
|
||
if (
|
||
!window.HyAppAPI ||
|
||
!window.HyAppAPI.achievement ||
|
||
!window.HyAppAPI.getAccessToken()
|
||
) {
|
||
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);
|
||
var avatars = custom.length
|
||
? custom
|
||
: [
|
||
ASSET + 'collector-1.png',
|
||
ASSET + 'collector-2.png',
|
||
ASSET + 'collector-3.png',
|
||
ASSET + 'collector-4.png',
|
||
ASSET + 'collector-5.png',
|
||
];
|
||
byId('collectorList').innerHTML =
|
||
avatars
|
||
.slice(0, 5)
|
||
.map(function (src) {
|
||
return (
|
||
'<img src="' + escapeHTML(src) + '" alt="">'
|
||
);
|
||
})
|
||
.join('') + '<div class="collector-more">...</div>';
|
||
}
|
||
function openModal(item) {
|
||
state.selected = item;
|
||
var unlocked = isUnlocked(item);
|
||
var resource = resourceOf(item);
|
||
byId('modalBadge').src = resourceImage(resource, unlocked);
|
||
byId('modalTitle').textContent = titleOf(item);
|
||
byId('modalDesc').textContent = descriptionOf(item);
|
||
if (unlocked) {
|
||
var date =
|
||
formatDate(item.unlocked_at_ms) ||
|
||
t('achievement.collected', 'Collected');
|
||
byId('modalStatus').innerHTML = escapeHTML(
|
||
replaceToken(
|
||
t(
|
||
'achievement.collectedWithDate',
|
||
'{date} Collected'
|
||
),
|
||
'date',
|
||
date
|
||
)
|
||
);
|
||
} else {
|
||
byId('modalStatus').innerHTML =
|
||
'<img src="' +
|
||
ASSET +
|
||
'lock.png" alt="">' +
|
||
escapeHTML(
|
||
t('achievement.notCollected', 'Not Collected')
|
||
);
|
||
}
|
||
var owners = ownerCountOf(item);
|
||
byId('ownerText').textContent = owners
|
||
? replaceToken(
|
||
t(
|
||
'achievement.ownerCount',
|
||
'Only {count} people own it'
|
||
),
|
||
'count',
|
||
owners
|
||
)
|
||
: t(
|
||
'achievement.ownerUpdating',
|
||
'Collection records are updating'
|
||
);
|
||
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;
|
||
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' });
|
||
if (applyLocalPreview()) return;
|
||
loadTab(state.tab).finally(function () {
|
||
loadUserProfile();
|
||
loadCounts();
|
||
});
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|