1266 lines
48 KiB
HTML
1266 lines
48 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;
|
||
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;
|
||
}
|
||
.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;
|
||
}
|
||
.achievement-loading {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 320px;
|
||
z-index: 7;
|
||
width: 375px;
|
||
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>
|
||
<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="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
|
||
class="hy-loading__text"
|
||
data-i18n="achievement.loading"
|
||
>
|
||
Loading...
|
||
</div>
|
||
</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 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',
|
||
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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
function validTab(value) {
|
||
return tabs[value] ? value : 'honors';
|
||
}
|
||
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 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 ||
|
||
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 {
|
||
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 = 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('');
|
||
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() {
|
||
var tasks = [];
|
||
var hasToken =
|
||
window.HyAppAPI &&
|
||
window.HyAppAPI.getAccessToken &&
|
||
window.HyAppAPI.getAccessToken();
|
||
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 (
|
||
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 (
|
||
!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);
|
||
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
|
||
)
|
||
: 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;
|
||
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>
|