hyapp-h5/rank/huwaa.html
2026-07-15 21:10:02 +08:00

1639 lines
58 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
/>
<meta name="theme-color" content="#3e1b00" />
<title data-i18n="huwaa_rank.page_title">Huwaa Ranking</title>
<link rel="stylesheet" href="../common/theme.css" />
<style>
:root {
--rank-zoom: 1;
--rank-bg: url('assets/huwaa/bg-wealth.webp');
--rank-base: #3e1b00;
--rank-fade: rgba(62, 27, 0, 0.9);
--rank-shade: rgba(58, 34, 16, 0.58);
--row-frame: url('assets/huwaa/row-wealth.svg');
--row-photo-opacity: 0.14;
--mine-start: #ffe27d;
--mine-end: #d78d0d;
--mine-glow: rgba(255, 210, 106, 0.68);
--mine-text: #6a4300;
--gold: #ffc928;
}
* {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
html,
body {
min-height: 100%;
margin: 0;
background: var(--rank-base);
color: #fff;
font-family: Arial, 'Helvetica Neue', sans-serif;
overscroll-behavior-y: none;
}
body {
overflow-x: hidden;
}
button {
margin: 0;
padding: 0;
border: 0;
outline: 0;
color: inherit;
font: inherit;
background: transparent;
cursor: pointer;
}
img {
display: block;
user-select: none;
-webkit-user-drag: none;
}
.rank-app {
position: relative;
width: 375px;
min-height: 100vh;
margin: 0 auto;
overflow: visible;
isolation: isolate;
background: var(--rank-base);
zoom: var(--rank-zoom);
}
.rank-background {
position: absolute;
z-index: -2;
inset: 0 0 auto;
width: 375px;
min-height: 100%;
overflow: hidden;
background: var(--rank-base);
pointer-events: none;
}
.rank-background::before {
position: absolute;
top: -115px;
left: 0;
width: 375px;
height: 666px;
background: var(--rank-bg) center / 375px 666px no-repeat;
content: '';
}
/* Figma 的背景蒙版在原稿 y=595 结束;移除 44px 状态栏后,渐隐必须在 y=551 前收进主题底色,避免列表中段出现断层。 */
.rank-background::after {
position: absolute;
top: 425px;
left: 0;
width: 375px;
height: 140px;
background: linear-gradient(
180deg,
transparent,
var(--rank-fade) 72%,
var(--rank-base) 100%
);
content: '';
}
.sticky-header {
position: sticky;
z-index: 50;
top: 0;
width: 375px;
height: 88px;
background: linear-gradient(
180deg,
var(--rank-shade) 0%,
rgba(9, 5, 35, 0.16) 72%,
transparent 100%
);
}
.top-nav {
position: relative;
width: 375px;
height: 44px;
}
.back-button {
position: absolute;
z-index: 2;
top: 0;
left: 8px;
display: grid;
width: 40px;
height: 44px;
place-items: center;
}
.back-button::before {
width: 10px;
height: 10px;
border-bottom: 2px solid #fff;
border-left: 2px solid #fff;
content: '';
transform: rotate(45deg);
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.7));
}
.help-button {
position: absolute;
z-index: 2;
top: 11px;
right: 18px;
width: 14px;
height: 14px;
border: 1px solid rgba(255, 255, 255, 0.8);
border-radius: 50%;
color: #fff;
font-size: 10px;
line-height: 12px;
text-align: center;
}
.board-tabs {
position: absolute;
top: 0;
left: 57px;
display: grid;
width: 263px;
height: 44px;
grid-template-columns: 70px 60px 72px 61px;
align-items: center;
}
.board-tab {
position: relative;
height: 44px;
color: #e2e2e2;
font-size: 16px;
line-height: 44px;
text-align: center;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.76);
white-space: nowrap;
}
.board-tab.is-active {
color: var(--gold);
text-shadow:
0 0 7px rgba(255, 202, 45, 0.68),
0 1px 2px rgba(0, 0, 0, 0.76);
}
.board-tab.is-active::after {
position: absolute;
bottom: 3px;
left: 50%;
width: 16px;
height: 3px;
margin-left: -8px;
border-radius: 3px;
background: var(--gold);
box-shadow: 0 0 7px rgba(255, 201, 40, 0.85);
content: '';
}
.period-tabs {
position: absolute;
top: 53px;
left: 65px;
display: grid;
width: 258px;
height: 29px;
border: 1px solid #ffc376;
border-radius: 15px;
background: #23180a;
grid-template-columns: repeat(3, 86px);
}
.period-tab {
position: relative;
z-index: 1;
height: 29px;
color: #fff;
font-size: 12px;
line-height: 28px;
text-align: center;
white-space: nowrap;
}
.period-tab::before {
position: absolute;
z-index: -1;
top: -6px;
left: -6px;
display: none;
width: 92px;
height: 40px;
background: url('assets/huwaa/period-active.webp') center / 92px
40px no-repeat;
content: '';
}
.period-tab.is-active {
color: #492307;
}
.period-tab.is-active::before {
display: block;
}
.podium {
position: relative;
z-index: 1;
width: 375px;
height: 278px;
}
.podium-card {
position: absolute;
overflow: visible;
text-align: center;
}
.podium-card.top-one {
top: 6px;
left: 114px;
width: 147px;
height: 225px;
}
.podium-card.side {
top: 89px;
width: 119px;
height: 202px;
}
.podium-card.top-two {
left: 4px;
}
.podium-card.top-three {
right: 4px;
}
.podium-avatar,
.podium-frame,
.podium-banner,
.game-frame {
position: absolute;
pointer-events: none;
}
.podium-avatar {
z-index: 1;
border-radius: 50%;
object-fit: cover;
background: #241504;
}
.podium-banner {
z-index: 0;
}
.podium-frame,
.game-frame {
z-index: 2;
}
.top-one .podium-avatar {
top: 29px;
left: 37px;
width: 73px;
height: 73px;
}
.top-one .podium-frame {
inset: 0 auto auto 0;
width: 147px;
height: 138px;
}
.top-one .podium-banner {
top: 83px;
left: 12px;
width: 122px;
height: 142px;
}
.side .podium-avatar {
top: 18px;
left: 25px;
width: 70px;
height: 70px;
}
.side .podium-frame {
inset: 0 auto auto 0;
width: 119px;
height: 119px;
}
.side .podium-banner {
top: 68px;
left: 11px;
width: 98px;
height: 134px;
}
.top-three .podium-banner {
width: 99px;
}
.game-frame {
inset: 0;
width: 100%;
height: 100%;
}
.rank-app.is-game .top-one {
top: 2px;
height: 221px;
}
.rank-app.is-game .side {
height: 197px;
}
.podium-copy {
position: absolute;
z-index: 3;
color: #6a4300;
text-align: center;
}
.top-one .podium-copy {
top: 140px;
left: 23px;
width: 100px;
}
.side .podium-copy {
top: 122px;
left: 20px;
width: 79px;
}
.podium-name {
display: block;
overflow: hidden;
font-size: 14px;
line-height: 14px;
text-overflow: ellipsis;
white-space: nowrap;
}
.side .podium-name {
font-size: 12px;
line-height: 14px;
}
.badge-strip {
display: flex;
height: 20px;
align-items: center;
justify-content: center;
gap: 1px;
overflow: hidden;
}
.rank-badge {
width: auto;
max-width: 19px;
height: 16px;
object-fit: contain;
}
.side .rank-badge {
max-width: 17px;
height: 14px;
}
.podium-score,
.row-score,
.mine-score {
display: flex;
align-items: center;
gap: 3px;
white-space: nowrap;
}
.podium-score {
justify-content: center;
height: 14px;
font-size: 10px;
line-height: 14px;
}
.score-coin {
width: 12px;
height: 12px;
object-fit: contain;
}
.side .podium-score,
.side .score-coin {
font-size: 9px;
}
.side .score-coin {
width: 10px;
height: 10px;
}
.rank-list {
position: relative;
z-index: 2;
width: 375px;
min-height: 375px;
padding-bottom: 104px;
}
.rank-item {
position: relative;
display: block;
width: 375px;
height: 75px;
text-align: left;
}
.row-frame,
.row-photo {
position: absolute;
top: 8px;
left: 10px;
width: 355px;
height: 63px;
}
.row-frame {
z-index: 0;
content: var(--row-frame);
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.28));
}
.row-photo {
z-index: 1;
background: var(--rank-bg) center -204px / 375px 666px no-repeat;
clip-path: polygon(
2.8% 0,
97.3% 0,
100% 15%,
100% 86%,
97.3% 100%,
2.8% 100%,
0 86%,
0 15%
);
opacity: var(--row-photo-opacity);
}
.row-ornament {
position: absolute;
z-index: 3;
top: 0;
left: 133px;
width: 110px;
height: 14px;
}
.rank-number {
position: absolute;
z-index: 4;
top: 31px;
left: 21px;
width: 20px;
font-size: 16px;
line-height: 18px;
text-align: center;
}
.row-avatar {
position: absolute;
z-index: 4;
top: 19px;
left: 48px;
width: 42px;
height: 42px;
border-radius: 50%;
object-fit: cover;
background: #1c120b;
}
.row-main {
position: absolute;
z-index: 4;
top: 19px;
left: 98px;
width: 148px;
height: 45px;
}
.row-name {
display: block;
overflow: hidden;
font-size: 14px;
line-height: 17px;
text-overflow: ellipsis;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
white-space: nowrap;
}
.row-badges {
display: flex;
height: 20px;
align-items: center;
gap: 3px;
overflow: hidden;
}
.row-badges .rank-badge {
max-width: 17px;
height: 14px;
}
.row-score {
position: absolute;
z-index: 4;
top: 29px;
right: 29px;
justify-content: flex-end;
width: 86px;
font-size: 16px;
line-height: 20px;
text-align: right;
}
.row-score .score-coin {
width: 14px;
height: 14px;
}
.empty-state,
.error-state {
display: none;
min-height: 150px;
align-items: center;
justify-content: center;
padding: 24px;
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
text-align: center;
text-shadow: 0 1px 3px #000;
}
.empty-state.is-visible,
.error-state.is-visible {
display: flex;
}
.my-rank {
position: fixed;
z-index: 80;
bottom: 0;
left: 50%;
display: block;
width: 375px;
height: 82px;
margin-left: -187.5px;
overflow: hidden;
border-top: 1px solid #ffe76d;
border-radius: 8px 8px 0 0;
color: var(--mine-text);
text-align: left;
background:
linear-gradient(
180deg,
var(--mine-glow) 0%,
transparent 100%
),
linear-gradient(90deg, var(--mine-start), var(--mine-end));
box-shadow: 0 -5px 16px rgba(26, 12, 0, 0.22);
zoom: var(--rank-zoom);
}
.my-rank::before {
position: absolute;
top: -1px;
left: 40px;
width: 88px;
height: 1px;
background: linear-gradient(
90deg,
transparent,
#fff,
transparent
);
content: '';
opacity: 0.7;
}
.mine-number {
position: absolute;
top: 22px;
left: 18px;
width: 12px;
font-size: 20px;
line-height: 22px;
text-align: center;
}
.mine-avatar {
position: absolute;
top: 11px;
left: 46px;
width: 44px;
height: 44px;
border-radius: 50%;
object-fit: cover;
}
.mine-main {
position: absolute;
top: 12px;
left: 100px;
width: 150px;
}
.mine-name {
display: block;
overflow: hidden;
font-size: 16px;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
.mine-badges {
height: 23px;
justify-content: flex-start;
gap: 5px;
}
.mine-badges .rank-badge {
max-width: 18px;
height: 16px;
}
.mine-score {
position: absolute;
top: 24px;
right: 22px;
justify-content: flex-end;
width: 90px;
font-size: 16px;
line-height: 20px;
}
.mine-score .score-coin {
width: 14px;
height: 14px;
}
.home-indicator {
position: absolute;
right: 121px;
bottom: 9px;
left: 121px;
height: 5px;
border-radius: 3px;
background: #fff;
}
.loading-overlay {
position: fixed;
z-index: 120;
inset: 0;
display: none;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.16);
}
.loading-overlay.is-visible {
display: flex;
}
.loading-dot {
width: 30px;
height: 30px;
border: 3px solid rgba(255, 255, 255, 0.28);
border-top-color: #ffd349;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
[dir='rtl'] .rank-item,
[dir='rtl'] .my-rank {
direction: ltr;
}
@media (prefers-reduced-motion: reduce) {
.loading-dot {
animation-duration: 1.8s;
}
}
</style>
<script>
window.HyAppI18nSupported = ['en', 'ar', 'tr', 'es'];
// Huwaa 独立入口必须在公共 API/参数脚本执行前固定 app_code否则 localStorage 中其他品牌值会污染请求头与用户概览。
window.HyAppDefaultAppCode = 'huwaa';
(function enforceHuwaaAppCode() {
try {
var entryURL = new URL(window.location.href);
if (entryURL.searchParams.get('app_code') !== 'huwaa') {
entryURL.searchParams.set('app_code', 'huwaa');
window.history.replaceState(
null,
'',
entryURL.toString()
);
}
} catch (_) {
// 旧 WebView 可能禁止 file:// 改写 history默认值仍保证请求层发送 X-App-Code: huwaa。
}
})();
</script>
<script src="../common/api.js"></script>
<script src="../common/params.js"></script>
<script src="../common/jsbridge.js"></script>
<script src="../common/i18n.js"></script>
</head>
<body>
<main class="rank-app" id="rankApp">
<div class="rank-background" aria-hidden="true"></div>
<header class="sticky-header">
<nav
class="top-nav"
data-i18n-aria="fami_rank.navigation"
aria-label="Ranking navigation"
>
<button
class="back-button"
id="backButton"
type="button"
data-i18n-aria="fami_rank.back"
aria-label="Back"
></button>
<div class="board-tabs" id="boardTabs">
<button
class="board-tab"
type="button"
data-board="wealth"
data-i18n="fami_rank.wealth"
>
Wealth
</button>
<button
class="board-tab"
type="button"
data-board="room"
data-i18n="fami_rank.room"
>
Room
</button>
<button
class="board-tab"
type="button"
data-board="charm"
data-i18n="fami_rank.charm"
>
Charm
</button>
<button
class="board-tab"
type="button"
data-board="game"
data-i18n="huwaa_rank.game"
>
Game
</button>
</div>
<button
class="help-button"
id="helpButton"
type="button"
data-i18n-aria="fami_rank.rules"
aria-label="Ranking rules"
>
?
</button>
</nav>
<div class="period-tabs" id="periodTabs">
<button
class="period-tab"
type="button"
data-period="today"
data-i18n="fami_rank.daily"
>
Daily
</button>
<button
class="period-tab"
type="button"
data-period="week"
data-i18n="fami_rank.weekly"
>
Weekly
</button>
<button
class="period-tab"
type="button"
data-period="month"
data-i18n="fami_rank.monthly"
>
Monthly
</button>
</div>
</header>
<section class="podium" id="podium" aria-live="polite"></section>
<section class="rank-list" id="rankList"></section>
<div
class="empty-state"
id="emptyState"
data-i18n="fami_rank.empty"
>
No ranking data yet
</div>
<div
class="error-state"
id="errorState"
data-i18n="fami_rank.network_error"
>
Unable to load the ranking. Please try again.
</div>
</main>
<button class="my-rank" id="myRank" type="button" hidden></button>
<div
class="loading-overlay"
id="loadingOverlay"
aria-hidden="true"
data-i18n-aria="fami_rank.loading"
aria-label="Loading"
>
<span class="loading-dot"></span>
</div>
<script>
(function () {
'use strict';
var ASSET_ROOT = 'assets/huwaa/';
var BOARD_CONFIG = {
// UI 名称与后端枚举解耦;切换主题时只从这里读取协议值,避免翻译文案或 URL 别名直接进入接口。
wealth: {
apiType: 'sent',
background: 'bg-wealth.webp',
rowFrame: 'row-wealth.svg',
base: '#3e1b00',
fade: 'rgba(62, 27, 0, .9)',
shade: 'rgba(58, 34, 16, .58)',
photoOpacity: '.14',
mineStart: '#ffe27d',
mineEnd: '#d78d0d',
mineGlow: 'rgba(255, 210, 106, .68)',
mineText: '#6a4300',
},
room: {
apiType: 'room',
background: 'bg-room.webp',
rowFrame: 'row-room.svg',
base: '#003958',
fade: 'rgba(0, 57, 88, .92)',
shade: 'rgba(9, 5, 35, .38)',
photoOpacity: '.14',
mineStart: '#5577f4',
mineEnd: '#122b81',
mineGlow: 'rgba(101, 136, 255, .72)',
mineText: '#fff',
},
charm: {
apiType: 'received',
background: 'bg-charm.webp',
rowFrame: 'row-charm.svg',
base: '#2f0044',
fade: 'rgba(47, 0, 68, .92)',
shade: 'rgba(9, 5, 35, .36)',
photoOpacity: '.14',
mineStart: '#c230df',
mineEnd: '#6c128a',
mineGlow: 'rgba(213, 80, 255, .7)',
mineText: '#fff',
},
game: {
// 游戏榜设计已预留独立枚举;当前端先于服务端发布时,接口会落入标准错误态而不会误用财富榜数据。
apiType: 'game',
background: 'bg-game.webp',
rowFrame: 'row-game.svg',
base: '#190c03',
fade: 'rgba(25, 12, 3, .94)',
shade: 'rgba(9, 5, 35, .36)',
photoOpacity: '.2',
mineStart: '#ffe27d',
mineEnd: '#d78d0d',
mineGlow: 'rgba(255, 210, 106, .68)',
mineText: '#6a4300',
},
};
var PERIODS = ['today', 'week', 'month'];
var query = new URLSearchParams(window.location.search);
var state = {
board: normalizeBoard(
query.get('board') || query.get('board_type')
),
period:
PERIODS.indexOf(query.get('period')) >= 0
? query.get('period')
: 'today',
page: 1,
pageSize: 20,
total: 0,
items: [],
myRank: null,
currentUser: null,
loading: false,
loaded: false,
error: null,
requestSerial: 0,
};
function byId(id) {
return document.getElementById(id);
}
function normalizeBoard(value) {
var aliases = {
sent: 'wealth',
wealth: 'wealth',
received: 'charm',
charm: 'charm',
room: 'room',
rooms: 'room',
game: 'game',
gaming: 'game',
};
return (
aliases[String(value || '').toLowerCase()] || 'wealth'
);
}
function escapeHTML(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function t(key, fallback) {
return window.HyAppI18n && window.HyAppI18n.t
? window.HyAppI18n.t(key, fallback)
: fallback;
}
function mockEnabled() {
var hasToken = Boolean(
window.HyAppAPI &&
window.HyAppAPI.getAccessToken &&
window.HyAppAPI.getAccessToken()
);
// APP 可能以 file:// 承载 H5只有文件预览且无 token 时启用样例,携带 token 必须走 Huwaa 真实请求。
return (
query.get('mock') === '1' ||
(window.location.protocol === 'file:' && !hasToken)
);
}
function compactNumber(value) {
var number = Number(value || 0);
if (!Number.isFinite(number)) number = 0;
if (number >= 1000000000)
return trimDecimal(number / 1000000000) + 'b';
if (number >= 1000000)
return trimDecimal(number / 1000000) + 'm';
if (number >= 1000) return trimDecimal(number / 1000) + 'k';
return String(Math.round(number));
}
function trimDecimal(value) {
return value
.toFixed(value >= 100 ? 0 : 1)
.replace(/\.0$/, '');
}
function normalizeItem(raw) {
var item = raw || {};
var user = item.user || {};
var room = item.room || {};
var badges =
user.short_badge_cover_urls ||
user.shortBadgeCoverUrls ||
[];
if (!Array.isArray(badges)) badges = [];
return {
rank: Number(item.rank || 0),
giftValue: Number(
item.gift_value || item.giftValue || item.value || 0
),
userId: String(
item.user_id || user.user_id || user.userId || ''
),
roomId: String(
item.room_id || room.room_id || room.roomId || ''
),
name:
user.username ||
user.nickname ||
user.display_user_id ||
room.title ||
room.name ||
room.room_short_id ||
t('fami_rank.unknown', 'Unknown'),
avatar:
user.avatar ||
user.avatar_url ||
user.avatarUrl ||
room.cover_url ||
room.coverUrl ||
room.room_avatar ||
room.avatar ||
'',
badges: badges.filter(function (url) {
return typeof url === 'string' && url.trim();
}),
};
}
function normalizeCurrentUser(current) {
var overview = current && current.user;
var profile =
(overview &&
(overview.profile || overview.user || overview)) ||
{};
var badges =
profile.short_badge_cover_urls ||
profile.shortBadgeCoverUrls ||
[];
if (!Array.isArray(badges)) badges = [];
return {
rank: 0,
giftValue: 0,
userId: String(
profile.user_id ||
profile.userId ||
profile.id ||
''
),
roomId: '',
name:
profile.nickname ||
profile.username ||
profile.display_user_id ||
profile.user_id ||
t('fami_rank.unknown', 'Unknown'),
avatar:
profile.avatar ||
profile.avatar_url ||
profile.avatarUrl ||
'',
badges: badges.filter(function (url) {
return typeof url === 'string' && url.trim();
}),
};
}
function normalizePayload(data, reset) {
var incoming = (
data && Array.isArray(data.items) ? data.items : []
)
.map(normalizeItem)
.filter(function (item) {
return item.rank > 0;
});
state.items = reset
? incoming
: state.items.concat(incoming);
state.total = Number(
(data && data.total) || state.items.length || 0
);
state.page = Number((data && data.page) || state.page);
state.pageSize = Number(
(data && (data.page_size || data.pageSize)) ||
state.pageSize
);
state.myRank =
data && data.my_rank
? normalizeItem(data.my_rank)
: null;
state.loaded = true;
state.error = null;
}
function mockItems() {
return Array.from({ length: 14 }, function (_, index) {
return {
rank: index + 1,
giftValue:
index < 3 ? 1232345 : 102400 - index * 700,
userId: 'huwaa_user_' + (index + 1),
roomId: 'huwaa_room_' + (index + 1),
name: 'Namenamename',
avatar:
ASSET_ROOT +
(index === 0
? 'default-top.webp'
: index < 3
? 'default-side.webp'
: 'default-row.webp'),
// 样例徽章只用于 Figma 核对;真实接口为空时必须不渲染,不能伪造用户权益。
badges: Array.from(
{ length: index === 0 ? 5 : 4 },
function () {
return ASSET_ROOT + 'mock-badge.webp';
}
),
};
});
}
function visibleItems() {
return mockEnabled() ? mockItems() : state.items;
}
function itemAvatar(item, rank) {
if (item && item.avatar) return item.avatar;
if (rank === 1) return ASSET_ROOT + 'default-top.webp';
if (rank === 2 || rank === 3)
return ASSET_ROOT + 'default-side.webp';
return ASSET_ROOT + 'default-row.webp';
}
function badgeMarkup(item, className, limit) {
var badges =
item && Array.isArray(item.badges) ? item.badges : [];
return (
'<span class="' +
className +
'">' +
badges
.slice(0, limit)
.map(function (url) {
return (
'<img class="rank-badge" src="' +
escapeHTML(url) +
'" alt="" />'
);
})
.join('') +
'</span>'
);
}
function scoreMarkup(item, className) {
return (
'<span class="' +
className +
'"><img class="score-coin" src="' +
ASSET_ROOT +
'coin.webp" alt="" /><span>' +
compactNumber(item ? item.giftValue : 0) +
'</span></span>'
);
}
function podiumMarkup(item, rank) {
var actual = item || {
rank: rank,
name: t('fami_rank.unknown', 'Unknown'),
giftValue: 0,
badges: [],
};
var classes =
rank === 1
? 'podium-card top-one'
: rank === 2
? 'podium-card side top-two'
: 'podium-card side top-three';
var normalFrame =
rank === 1
? 'top-frame.webp'
: rank === 2
? 'side-frame-silver.webp'
: 'side-frame-bronze.webp';
var normalBanner =
rank === 1
? 'top-banner.webp'
: rank === 2
? 'side-banner-silver.webp'
: 'side-banner-bronze.webp';
var gameFrame =
rank === 1
? 'game-top.webp'
: rank === 2
? 'game-silver.webp'
: 'game-bronze.webp';
var art =
state.board === 'game'
? '<img class="game-frame" src="' +
ASSET_ROOT +
gameFrame +
'" alt="" />'
: '<img class="podium-banner" src="' +
ASSET_ROOT +
normalBanner +
'" alt="" /><img class="podium-avatar" src="' +
escapeHTML(itemAvatar(actual, rank)) +
'" alt="" /><img class="podium-frame" src="' +
ASSET_ROOT +
normalFrame +
'" alt="" />';
return (
'<button class="' +
classes +
'" type="button" data-rank="' +
rank +
'"' +
(item ? '' : ' disabled') +
'>' +
art +
'<span class="podium-copy"><span class="podium-name">' +
escapeHTML(actual.name) +
'</span>' +
badgeMarkup(actual, 'badge-strip', rank === 1 ? 5 : 4) +
scoreMarkup(actual, 'podium-score') +
'</span></button>'
);
}
function rowMarkup(item) {
return (
'<button class="rank-item" type="button" data-rank="' +
item.rank +
'"><img class="row-frame" src="' +
ASSET_ROOT +
BOARD_CONFIG[state.board].rowFrame +
'" alt="" /><span class="row-photo"></span><img class="row-ornament" src="' +
ASSET_ROOT +
'row-ornament.webp" alt="" /><span class="rank-number">' +
item.rank +
'</span><img class="row-avatar" src="' +
escapeHTML(itemAvatar(item, 0)) +
'" alt="" /><span class="row-main"><span class="row-name">' +
escapeHTML(item.name) +
'</span>' +
badgeMarkup(item, 'row-badges', 4) +
'</span>' +
scoreMarkup(item, 'row-score') +
'</button>'
);
}
function currentMineItem() {
if (mockEnabled()) {
var sample = mockItems()[3];
sample.badges.push(ASSET_ROOT + 'mock-badge.webp');
return sample;
}
// 未上榜仍保留真实用户资料,排名显示 --、值显示 0否则底栏会在弱活跃用户上整条消失。
return state.myRank || state.currentUser;
}
function renderMine() {
var item = currentMineItem();
var mine = byId('myRank');
mine.hidden = !item;
if (!item) {
mine.innerHTML = '';
return;
}
mine.innerHTML =
'<span class="mine-number">' +
(item.rank || '--') +
'</span><img class="mine-avatar" src="' +
escapeHTML(itemAvatar(item, 0)) +
'" alt="" /><span class="mine-main"><span class="mine-name">' +
escapeHTML(item.name) +
'</span>' +
badgeMarkup(item, 'badge-strip mine-badges', 5) +
'</span>' +
scoreMarkup(item, 'mine-score') +
'<span class="home-indicator"></span>';
}
function render() {
var config = BOARD_CONFIG[state.board];
var root = document.documentElement;
root.style.setProperty(
'--rank-bg',
'url("' + ASSET_ROOT + config.background + '")'
);
root.style.setProperty('--rank-base', config.base);
root.style.setProperty('--rank-fade', config.fade);
root.style.setProperty('--rank-shade', config.shade);
root.style.setProperty(
'--row-photo-opacity',
config.photoOpacity
);
root.style.setProperty('--mine-start', config.mineStart);
root.style.setProperty('--mine-end', config.mineEnd);
root.style.setProperty('--mine-glow', config.mineGlow);
root.style.setProperty('--mine-text', config.mineText);
document
.querySelector('meta[name="theme-color"]')
.setAttribute('content', config.base);
byId('rankApp').classList.toggle(
'is-game',
state.board === 'game'
);
document
.querySelectorAll('[data-board]')
.forEach(function (node) {
node.classList.toggle(
'is-active',
node.getAttribute('data-board') === state.board
);
});
document
.querySelectorAll('[data-period]')
.forEach(function (node) {
node.classList.toggle(
'is-active',
node.getAttribute('data-period') ===
state.period
);
});
var items = visibleItems();
byId('podium').innerHTML =
podiumMarkup(items[0], 1) +
podiumMarkup(items[1], 2) +
podiumMarkup(items[2], 3);
byId('rankList').innerHTML = items
.slice(3)
.map(rowMarkup)
.join('');
byId('emptyState').classList.toggle(
'is-visible',
state.loaded && !state.error && items.length === 0
);
byId('errorState').classList.toggle(
'is-visible',
state.loaded &&
Boolean(state.error) &&
items.length === 0
);
byId('loadingOverlay').classList.toggle(
'is-visible',
state.loading
);
byId('loadingOverlay').setAttribute(
'aria-hidden',
state.loading ? 'false' : 'true'
);
renderMine();
}
function updateURLState() {
if (!window.history || !window.history.replaceState) return;
var url = new URL(window.location.href);
url.searchParams.set('app_code', 'huwaa');
url.searchParams.set('board', state.board);
url.searchParams.set('period', state.period);
window.history.replaceState(null, '', url.toString());
}
function load(reset) {
if (reset) {
state.page = 1;
state.items = [];
state.total = 0;
state.myRank = null;
state.loaded = false;
state.error = null;
}
if (mockEnabled()) {
state.loaded = true;
state.loading = false;
state.error = null;
render();
return Promise.resolve();
}
if (!window.HyAppAPI || !window.HyAppAPI.getAccessToken()) {
state.loaded = true;
state.loading = false;
state.error = new Error('missing_token');
render();
return Promise.resolve();
}
var requestedPage = state.page;
var serial = ++state.requestSerial;
state.loading = true;
state.error = null;
render();
return window.HyAppAPI.get(
'/api/v1/activities/user-leaderboards',
{
board_type: BOARD_CONFIG[state.board].apiType,
period: state.period,
page: requestedPage,
page_size: state.pageSize,
}
)
.then(function (payload) {
// 切榜、切周期可能并发;只接收最后一次请求,防止慢响应把当前主题覆盖回旧榜。
if (serial !== state.requestSerial) return;
normalizePayload(payload, reset);
})
.catch(function (error) {
if (serial !== state.requestSerial) return;
state.loaded = true;
state.error = error;
// 翻页失败回滚页码,下一次触底才会重试失败页而不是永久跳页。
if (!reset)
state.page = Math.max(1, requestedPage - 1);
})
.finally(function () {
if (serial !== state.requestSerial) return;
state.loading = false;
render();
});
}
function switchBoard(board) {
if (!BOARD_CONFIG[board] || state.board === board) return;
state.board = board;
state.requestSerial += 1;
updateURLState();
window.scrollTo(0, 0);
load(true);
}
function switchPeriod(period) {
if (PERIODS.indexOf(period) < 0 || state.period === period)
return;
state.period = period;
state.requestSerial += 1;
updateURLState();
window.scrollTo(0, 0);
load(true);
}
function itemByRank(rank) {
return visibleItems().find(function (item) {
return String(item.rank) === String(rank);
});
}
function openItem(item) {
if (!item) return;
if (state.board === 'room') {
if (item.roomId)
window.HyAppBridge.openRoom(item.roomId);
return;
}
if (item.userId) window.HyAppBridge.openUser(item.userId);
}
function resizeCanvas() {
// 375px 是 Figma 基准;窄屏整体缩放,避免皇冠、榜框的绝对坐标被流式布局挤散。
var zoom = Math.min(1, window.innerWidth / 375);
document.documentElement.style.setProperty(
'--rank-zoom',
String(zoom)
);
}
byId('boardTabs').addEventListener('click', function (event) {
var target = event.target.closest('[data-board]');
if (target) switchBoard(target.getAttribute('data-board'));
});
byId('periodTabs').addEventListener('click', function (event) {
var target = event.target.closest('[data-period]');
if (target)
switchPeriod(target.getAttribute('data-period'));
});
byId('podium').addEventListener('click', function (event) {
var target = event.target.closest('[data-rank]');
if (target)
openItem(itemByRank(target.getAttribute('data-rank')));
});
byId('rankList').addEventListener('click', function (event) {
var target = event.target.closest('[data-rank]');
if (target)
openItem(itemByRank(target.getAttribute('data-rank')));
});
byId('myRank').addEventListener('click', function () {
openItem(currentMineItem());
});
byId('backButton').addEventListener('click', function () {
window.HyAppBridge.back();
});
byId('helpButton').addEventListener('click', function () {
if (window.HyAppBridge.openRankNote)
window.HyAppBridge.openRankNote(state.board);
});
document.addEventListener(
'error',
function (event) {
var image = event.target;
if (!image || image.tagName !== 'IMG') return;
if (image.classList.contains('rank-badge')) {
// 失效远端徽章直接移除;本地替代图会错误展示用户并未拥有的权益。
image.remove();
return;
}
if (
image.classList.contains('row-frame') ||
image.classList.contains('row-ornament') ||
image.classList.contains('podium-frame') ||
image.classList.contains('podium-banner') ||
image.classList.contains('game-frame') ||
image.classList.contains('score-coin')
) {
return;
}
// CDN 头像失败只回退头像资源,装饰和徽章保持真实失败语义。
image.src = ASSET_ROOT + 'default-row.webp';
},
true
);
window.addEventListener('scroll', function () {
if (
mockEnabled() ||
state.loading ||
!state.loaded ||
!state.total ||
state.items.length >= state.total
) {
return;
}
if (
window.innerHeight + window.scrollY >=
document.documentElement.scrollHeight - 180
) {
state.page += 1;
load(false);
}
});
window.addEventListener('resize', resizeCanvas);
window.addEventListener('hyapp:i18n-ready', render);
// WebView 刷新会恢复旧滚动位置;排行榜入口固定从吸顶导航开始,避免首屏直接落在三甲中段。
if ('scrollRestoration' in window.history) {
window.history.scrollRestoration = 'manual';
}
window.scrollTo(0, 0);
resizeCanvas();
updateURLState();
render();
window.HyAppBridge.ready({ page: 'rank/huwaa' });
// 用户概览失败不能阻断排行榜;接口未返回 my_rank 时仍可用概览填充个人底栏。
Promise.resolve(
window.HyAppParams && window.HyAppParams.loadUser
? window.HyAppParams.loadUser()
: null
)
.then(function (current) {
state.currentUser = current
? normalizeCurrentUser(current)
: null;
render();
})
.catch(function () {
return null;
})
.finally(function () {
load(true);
});
})();
</script>
</body>
</html>