hyapp-h5/level/index.html
2026-05-29 20:38:44 +08:00

381 lines
24 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" 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>Level</title>
<link rel="stylesheet" href="../common/theme.css">
<style>
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
:root { --app-scale: 1; }
html, body { margin: 0; width: 100%; min-height: 100%; overflow-x: hidden; background: #130a2b; 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; height: 826px; margin: 0 auto; transform: scale(var(--app-scale)); transform-origin: top center; }
.page { position: relative; width: 375px; height: 826px; overflow: hidden; background: #130a2b; }
.page-bg { position: absolute; left: -28px; top: -297px; width: 599px; height: 1123px; object-fit: cover; opacity: .54; pointer-events: none; }
.home-indicator { position: absolute; left: 121px; bottom: 8px; z-index: 8; width: 134px; height: 5px; border-radius: 100px; background: #fff; }
.nav { position: absolute; left: 0; top: 0; z-index: 5; width: 375px; height: 44px; }
.back { position: absolute; left: 16px; top: 10px; width: 24px; height: 24px; }
.back::before { content: ""; position: absolute; left: 7px; top: 5px; width: 13px; height: 13px; border-left: 2px solid #fff; border-bottom: 2px solid #fff; transform: rotate(45deg); }
.title { position: absolute; left: 0; top: 13px; width: 375px; font-size: 18px; line-height: 18px; font-weight: 500; text-align: center; text-transform: capitalize; }
.tabs { position: absolute; left: 0; top: 58px; z-index: 5; width: 375px; height: 30px; }
.tab { position: absolute; top: 0; width: 92px; height: 30px; color: rgba(255,255,255,.5); font-size: 18px; line-height: 18px; font-weight: 700; text-align: center; text-transform: capitalize; }
.tab[data-track="wealth"] { left: 16px; }
.tab[data-track="charm"] { left: 142px; }
.tab[data-track="game"] { left: 266px; }
.tab.active { color: #fff; opacity: 1; }
.tab.active::after { content: ""; position: absolute; left: 50%; top: 24px; width: 16px; height: 3px; margin-left: -8px; border-radius: 31px; background: #fff; }
.level-card { position: absolute; left: 16px; top: 111px; z-index: 2; width: 343px; height: 111px; overflow: hidden; background: transparent; }
.level-card-bg { display: block; width: 343px; height: 111px; object-fit: cover; }
.card-decor { position: absolute; left: 24px; top: 99px; z-index: 3; width: 164px; height: 27px; object-fit: cover; }
.card-decor.right { left: 188px; transform: rotate(180deg) scaleY(-1); }
.level-name { position: absolute; left: 40px; top: 133px; z-index: 4; font-size: 16px; line-height: 16px; font-weight: 900; text-transform: capitalize; color: var(--title-fill, #fff); text-shadow: 0 2px 0 var(--title-shadow); }
.avatar-wrap { position: absolute; left: 259px; top: 126px; z-index: 4; width: 80px; height: 80px; }
.avatar { position: absolute; left: 11px; top: 11px; width: 58px; height: 58px; border-radius: 50%; object-fit: cover; }
.avatar-frame { position: absolute; inset: 0; width: 80px; height: 80px; object-fit: cover; }
.progress { position: absolute; left: 40px; top: 171px; z-index: 4; width: 182px; height: 38px; color: var(--progress-text); }
.lv-current, .lv-next { position: absolute; top: 7px; font-size: 12px; line-height: 12px; text-transform: capitalize; }
.lv-current { left: 0; }
.lv-next { left: 159px; }
.progress-percent { position: absolute; left: 67px; top: 0; width: 45px; font-size: 8px; line-height: 8px; text-align: center; }
.progress-track { position: absolute; left: 24px; top: 12px; width: 133px; height: 4px; border-radius: 3px; background: var(--progress-track); }
.progress-fill { width: 0; height: 4px; border-radius: 3px; background: var(--progress-fill); }
.need { position: absolute; left: 0; top: 27px; width: 220px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 8px; line-height: 8px; color: var(--need-text); }
.level-table { position: absolute; left: 16px; top: 238px; z-index: 2; width: 343px; height: 318px; border-radius: 12px; background: rgba(67,0,255,.11); }
.table-head { position: absolute; top: 16px; height: 16px; font-size: 16px; line-height: 16px; font-weight: 500; text-align: center; text-transform: capitalize; white-space: nowrap; }
.head-range { left: 19px; width: 49px; }
.head-icons { left: 124px; width: 42px; }
.head-frame { left: 226px; width: 96px; }
.tier-row { position: absolute; left: 0; z-index: 2; width: 343px; height: 46px; }
.tier-row.r1 { top: 44px; }
.tier-row.r2 { top: 98px; }
.tier-row.r3 { top: 152px; }
.tier-row.r4 { top: 206px; }
.tier-row.r5 { top: 260px; }
.range { position: absolute; left: 29px; top: 16px; width: 30px; font-size: 14px; line-height: 14px; text-align: center; }
.badge-icon { position: absolute; left: 113px; top: 11px; width: 62px; height: 24px; object-fit: contain; }
.frame-icon { position: absolute; left: 250px; top: 0; width: 46px; height: 46px; object-fit: contain; }
.upgrade-title { position: absolute; left: 0; top: 574px; z-index: 3; width: 375px; color: var(--upgrade-title); font-size: 20px; line-height: 20px; font-weight: 700; text-align: center; }
.upgrade-list { position: absolute; left: 0; top: 616px; z-index: 3; width: 375px; height: 98px; }
.upgrade-item { position: absolute; top: 0; width: 119px; text-align: center; }
.upgrade-one { left: 128px; width: 120px; }
.upgrade-three:nth-child(1) { left: 1px; }
.upgrade-three:nth-child(2) { left: 128px; }
.upgrade-three:nth-child(3) { left: 255px; }
.upgrade-icon { display: block; width: 54px; height: 54px; margin: 0 auto; object-fit: contain; }
.upgrade-name { margin-top: 10px; font-size: 14px; line-height: 14px; font-weight: 500; white-space: nowrap; }
.upgrade-exp { margin-top: 8px; font-size: 14px; line-height: 14px; white-space: nowrap; }
.consume { position: absolute; left: 43px; top: 736px; z-index: 4; width: 289px; height: 50px; overflow: hidden; border: 1.5px solid #fff09b; background: #2a1351; color: #fff; font-size: 18px; line-height: 49px; font-weight: 700; text-align: center; }
.consume-bg { position: absolute; inset: -26px -38px; width: 363px; height: 104px; object-fit: cover; opacity: .8; mix-blend-mode: plus-lighter; pointer-events: none; }
.consume.gold .consume-bg { inset: -38px -79px; width: 447px; height: 127px; opacity: 1; mix-blend-mode: normal; }
.consume span { position: relative; z-index: 2; }
.corner { position: absolute; z-index: 3; width: 16px; height: 16px; object-fit: contain; }
.corner.c1 { left: 0; top: 0; }
.corner.c2 { right: 0; top: 0; transform: rotate(90deg); }
.corner.c3 { right: 0; bottom: 0; transform: rotate(180deg); }
.corner.c4 { left: 0; bottom: 0; transform: rotate(270deg); }
.hit { position: absolute; z-index: 7; border-radius: 8px; }
.hit-consume { left: 43px; top: 736px; width: 289px; height: 50px; }
.loading-overlay { position: absolute; inset: 0; z-index: 20; display: none; align-items: center; justify-content: center; background: rgba(19,10,43,.32); backdrop-filter: blur(1px); pointer-events: auto; }
.loading-overlay.active { display: flex; }
.cat-loader { width: 72px; height: 72px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background: rgba(0,0,0,.18); box-shadow: 0 8px 28px rgba(0,0,0,.28); }
.cat-loader svg { width: 54px; height: 54px; overflow: visible; animation: cat-float 1.1s ease-in-out infinite; }
.cat-line { fill: none; stroke: #fff; stroke-width: 3.4; stroke-linecap: round; stroke-linejoin: round; }
.cat-eye { transform-origin: center; animation: cat-blink 1.6s ease-in-out infinite; }
.cat-whisker { opacity: .9; animation: cat-whisker 1.1s ease-in-out infinite; }
.toast { position: fixed; left: 50%; bottom: calc(34px * var(--app-scale)); z-index: 60; max-width: 300px; transform: translateX(-50%); padding: 8px 12px; border-radius: 16px; background: rgba(0,0,0,.62); color: #fff; font-size: 12px; line-height: 16px; text-align: center; opacity: 0; pointer-events: none; transition: opacity .18s ease; }
.toast.show { opacity: 1; }
@keyframes cat-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
@keyframes cat-blink { 0%, 72%, 100% { transform: scaleY(1); } 80%, 88% { transform: scaleY(.18); } }
@keyframes cat-whisker { 0%, 100% { opacity: .62; } 50% { opacity: 1; } }
</style>
</head>
<body>
<div id="appViewport" class="app-viewport">
<main id="page" class="page">
<img id="pageBg" class="page-bg" alt="">
<nav class="nav" aria-label="Level navigation">
<button id="backButton" class="back" aria-label="Back"></button>
<div class="title">Level</div>
</nav>
<div id="tabs" class="tabs">
<button class="tab" data-track="wealth">Wealth</button>
<button class="tab" data-track="charm">Charm</button>
<button class="tab" data-track="game">Game</button>
</div>
<section class="level-card" aria-label="Current level"><img id="cardBg" class="level-card-bg" alt=""></section>
<img id="decorLeft" class="card-decor" alt="">
<img id="decorRight" class="card-decor right" alt="">
<div id="levelName" class="level-name">Level 0</div>
<div class="avatar-wrap">
<img id="avatar" class="avatar" alt="">
<img id="avatarFrame" class="avatar-frame" alt="">
</div>
<div class="progress">
<div id="lvCurrent" class="lv-current">Lv.0</div>
<div id="progressPercent" class="progress-percent">0%</div>
<div class="progress-track"><div id="progressFill" class="progress-fill"></div></div>
<div id="lvNext" class="lv-next">Lv.1</div>
<div id="needText" class="need">You need 12,345,600 more experience points</div>
</div>
<section class="level-table" aria-label="Level tiers">
<div class="table-head head-range">Range</div>
<div class="table-head head-icons">Icons</div>
<div class="table-head head-frame">Profile Frame</div>
<div id="tierRows"></div>
</section>
<div id="upgradeTitle" class="upgrade-title">How to upgrade</div>
<div id="upgradeList" class="upgrade-list"></div>
<button id="consumeButton" class="consume" aria-label="To consume"></button>
<button id="consumeHit" class="hit hit-consume" aria-label="To consume"></button>
<div class="home-indicator"></div>
<div id="loadingOverlay" class="loading-overlay" aria-hidden="true">
<div class="cat-loader" aria-label="Loading">
<svg viewBox="0 0 64 64" role="img" aria-hidden="true">
<path class="cat-line" d="M15 31 C15 19 24 14 32 14 C40 14 49 19 49 31 C49 43 42 50 32 50 C22 50 15 43 15 31 Z"/>
<path class="cat-line" d="M20 18 L16 7 L27 14"/>
<path class="cat-line" d="M44 18 L48 7 L37 14"/>
<path class="cat-line cat-eye" d="M25 31 L25 32"/>
<path class="cat-line cat-eye" d="M39 31 L39 32"/>
<path class="cat-line" d="M32 35 L29 38 L35 38 Z"/>
<path class="cat-line cat-whisker" d="M11 32 H23"/>
<path class="cat-line cat-whisker" d="M10 40 L23 37"/>
<path class="cat-line cat-whisker" d="M41 32 H53"/>
<path class="cat-line cat-whisker" d="M41 37 L54 40"/>
</svg>
</div>
</div>
</main>
</div>
<div id="toast" class="toast"></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>
(function () {
var A = "assets/component/";
var tracks = {
charm: {
bg: A + "bg-charm.png", cardBg: A + "card-charm.png", decor: A + "decor-charm.png", avatar: A + "avatar-charm.png", frame: A + "avatar-frame-charm.png",
card: "#130828", texture: "url('" + A + "button-purple.png')", textureOpacity: .26, glow: "linear-gradient(176deg, #462a78 36%, rgba(59,36,100,0) 63%)",
titleFill: "#ffffff", titleShadow: "#be1e21", titleGradient: "linear-gradient(135deg, #fff 27%, #f4cbff 49%, #fff 72%)",
progress: "#fff", progressTrack: "rgba(255,255,255,.2)", progressFill: "#fff", need: "rgba(255,255,255,.6)", upgrade: "#f718e9",
button: "purple", upgrades: [{ icon: A + "upgrade-gift-charm.svg", name: "Receive a gift", exp: "1 Coins = 1 EXP" }]
},
wealth: {
bg: A + "bg-wealth.png", cardBg: A + "card-wealth.png", decor: A + "decor-wealth.png", avatar: A + "avatar-wealth.png", frame: A + "avatar-frame-wealth.png",
card: "#ffa85c", texture: "url('" + A + "button-gold.png')", textureOpacity: .41, glow: "linear-gradient(165deg, rgba(255,242,130,.62) 35%, rgba(255,242,130,0) 48%), linear-gradient(-12deg, rgba(255,242,130,.55) 26%, rgba(255,242,130,0) 28%)",
titleFill: "#fff7e8", titleShadow: "#be561e", titleGradient: "linear-gradient(135deg, #fff 27%, #ffdfcb 49%, #fff 72%)",
progress: "#b7420c", progressTrack: "rgba(183,66,12,.2)", progressFill: "#b7420c", need: "rgba(183,66,12,.6)", upgrade: "#ffbe5f",
button: "gold", upgrades: [
{ icon: A + "upgrade-gift-wealth.svg", name: "Receive a Gift", exp: "1 Coins = 1 EXP" },
{ icon: A + "upgrade-car-wealth.svg", name: "Buy a car", exp: "1 Coins = 1 EXP" },
{ icon: A + "upgrade-avatar-wealth.svg", name: "Buy Avatar Frame", exp: "1 Coins = 1 EXP" }
]
},
game: {
bg: A + "bg-game.png", cardBg: A + "card-game.png", decor: A + "decor-game.png", avatar: A + "avatar-game.png", frame: A + "avatar-frame-game.png",
card: "#118624", texture: "url('" + A + "button-purple.png')", textureOpacity: .38, glow: "linear-gradient(165deg, rgba(255,242,130,.34) 35%, rgba(255,242,130,0) 48%), linear-gradient(-12deg, rgba(255,242,130,.3) 26%, rgba(255,242,130,0) 28%)",
titleFill: "#f2fff2", titleShadow: "#5ba626", titleGradient: "linear-gradient(135deg, #fff 27%, #cbffd7 49%, #fff 72%)",
progress: "#fff", progressTrack: "rgba(255,255,255,.2)", progressFill: "#fff", need: "rgba(255,255,255,.6)", upgrade: "#ffbe5f",
button: "purple", upgrades: [{ icon: A + "upgrade-game.svg", name: "Earned through gameplay", exp: "1 Coins = 1 EXP" }]
}
};
var state = { track: new URLSearchParams(window.location.search).get("track") || "charm", loading: false, details: {}, userAvatar: "", toastTimer: 0 };
function byId(id) { return document.getElementById(id); }
function track() { return tracks[state.track] ? state.track : "charm"; }
function fmt(n) { return Number(n || 0).toLocaleString("en-US"); }
function ratio(d) {
var o = d && d.overview ? d.overview : {};
var cur = Number(o.current_level_required_value || 0);
var next = Number(o.next_level_required_value || 0);
var total = Number(o.total_value || 0);
if (!d || !d.overview) return 0;
if (next <= cur) return total >= cur && cur > 0 ? 1 : 0;
return Math.max(0, Math.min(1, (total - cur) / (next - cur)));
}
function profileFromOverview(current) {
var user = current && current.user;
return (user && (user.profile || user.user || user)) || {};
}
function profileAvatar(profile) {
return String((profile && (profile.avatar || profile.user_avatar || profile.userAvatar || profile.avatar_url || profile.avatarUrl || profile.profile_avatar || profile.profileAvatar)) || "");
}
function applyUserProfile(current) {
state.userAvatar = profileAvatar(profileFromOverview(current));
render();
}
function setAvatarSource(src, fallback) {
var avatar = byId("avatar");
if (!avatar) return;
var fallbackSrc = fallback || "";
var nextSrc = src || fallbackSrc;
avatar.onerror = function () {
if (fallbackSrc && avatar.getAttribute("src") !== fallbackSrc) {
avatar.src = fallbackSrc;
}
};
if (avatar.getAttribute("src") !== nextSrc) avatar.src = nextSrc;
}
function resizeApp() {
var scale = Math.min(window.innerWidth / 375, 1.25);
document.documentElement.style.setProperty("--app-scale", String(scale));
byId("appViewport").style.height = "826px";
document.body.style.minHeight = Math.ceil(826 * scale) + "px";
}
function syncLoading() {
var overlay = byId("loadingOverlay");
overlay.classList.toggle("active", state.loading);
overlay.setAttribute("aria-hidden", state.loading ? "false" : "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 rowsHTML(name, detail) {
var tiers = ((detail && detail.tiers) || []).slice().sort(function (a, b) {
return Number(a.min_level || 0) - Number(b.min_level || 0);
});
function rangeText(index) {
var tier = tiers[index - 1];
if (!tier) return "1-10";
var min = Number(tier.min_level || 0);
var max = Number(tier.max_level || 0);
if (!min && !max) return "1-10";
return min + "-" + max;
}
return [1, 2, 3, 4, 5].map(function (i) {
return '<div class="tier-row r' + i + '"><div class="range">' + rangeText(i) + '</div><img class="badge-icon" src="' + A + name + '-icon-' + i + '.png" alt=""><img class="frame-icon" src="' + A + name + '-frame-' + i + '.png" alt=""></div>';
}).join("");
}
function upgradesHTML(cfg) {
var cls = cfg.upgrades.length === 3 ? "upgrade-three" : "upgrade-one";
return cfg.upgrades.map(function (item) {
return '<div class="upgrade-item ' + cls + '"><img class="upgrade-icon" src="' + item.icon + '" alt=""><div class="upgrade-name">' + item.name + '</div><div class="upgrade-exp">' + item.exp + '</div></div>';
}).join("");
}
function buttonHTML(cfg) {
var corner = cfg.button === "gold" ? A + "corner-gold.png" : A + "corner-purple.png";
var texture = cfg.button === "gold" ? A + "button-gold.png" : A + "button-purple.png";
return '<img class="consume-bg" src="' + texture + '" alt=""><img class="corner c1" src="' + corner + '" alt=""><img class="corner c2" src="' + corner + '" alt=""><img class="corner c3" src="' + corner + '" alt=""><img class="corner c4" src="' + corner + '" alt=""><span>To consume</span>';
}
function applyData() {
var d = state.details[state.track];
var o = d && d.overview ? d.overview : {};
var level = Number(o.level || 0);
var next = Number(o.next_level || level + 1);
var percent = Math.round(ratio(d) * 100);
byId("levelName").textContent = "Level " + level;
byId("lvCurrent").textContent = "Lv." + level;
byId("lvNext").textContent = "Lv." + next;
byId("progressPercent").textContent = percent + "%";
byId("progressFill").style.width = percent + "%";
var curRequired = Number(o.current_level_required_value || 0);
var nextRequired = Number(o.next_level_required_value || 0);
var total = Number(o.total_value || 0);
if (d && d.overview && nextRequired <= curRequired) {
byId("needText").textContent = "Max level reached";
return;
}
var need = Math.max(0, nextRequired - total);
byId("needText").textContent = "You need " + fmt(need) + " more experience points";
}
function render() {
state.track = track();
var cfg = tracks[state.track];
byId("pageBg").src = cfg.bg;
byId("cardBg").src = cfg.cardBg;
byId("decorLeft").src = cfg.decor;
byId("decorRight").src = cfg.decor;
setAvatarSource(state.userAvatar, cfg.avatar);
byId("avatarFrame").src = cfg.frame;
byId("tierRows").innerHTML = rowsHTML(state.track, state.details[state.track]);
byId("upgradeList").innerHTML = upgradesHTML(cfg);
byId("consumeButton").className = "consume " + (cfg.button === "gold" ? "gold" : "");
byId("consumeButton").innerHTML = buttonHTML(cfg);
document.documentElement.style.setProperty("--card-texture", cfg.texture);
document.documentElement.style.setProperty("--card-texture-opacity", cfg.textureOpacity);
document.documentElement.style.setProperty("--card-glow", cfg.glow);
document.documentElement.style.setProperty("--title-shadow", cfg.titleShadow);
document.documentElement.style.setProperty("--title-fill", cfg.titleFill);
document.documentElement.style.setProperty("--title-gradient", cfg.titleGradient);
document.documentElement.style.setProperty("--progress-text", cfg.progress);
document.documentElement.style.setProperty("--progress-track", cfg.progressTrack);
document.documentElement.style.setProperty("--progress-fill", cfg.progressFill);
document.documentElement.style.setProperty("--need-text", cfg.need);
document.documentElement.style.setProperty("--upgrade-title", cfg.upgrade);
document.querySelectorAll(".tab").forEach(function (node) { node.classList.toggle("active", node.dataset.track === state.track); });
applyData();
syncLoading();
resizeApp();
}
function loadOverview() {
if (!window.HyAppAPI || !window.HyAppAPI.level || !window.HyAppAPI.getAccessToken()) return Promise.resolve(null);
return window.HyAppAPI.level.overview().then(function (data) {
((data && data.tracks) || []).forEach(function (item) {
if (!item || !item.track || !tracks[item.track]) return;
if (!state.details[item.track]) state.details[item.track] = {};
state.details[item.track].overview = item;
});
render();
return data;
}).catch(function () { return null; });
}
function loadTrack(name) {
state.track = tracks[name] ? name : "charm";
if (window.history && window.history.replaceState) {
var url = new URL(window.location.href);
url.searchParams.set("track", state.track);
window.history.replaceState(null, "", url.toString());
}
render();
if (!window.HyAppAPI || !window.HyAppAPI.level || !window.HyAppAPI.getAccessToken()) return Promise.resolve(null);
state.loading = true;
render();
return window.HyAppAPI.level.track(state.track).then(function (data) {
state.details[state.track] = data;
}).catch(function (error) {
showToast((error && error.message) || "Network error");
}).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) {
applyUserProfile(current);
return current;
}).catch(function () { return null; });
}
byId("tabs").addEventListener("click", function (event) {
var target = event.target.closest("[data-track]");
if (!target) return;
loadTrack(target.dataset.track);
});
byId("backButton").addEventListener("click", function () { window.HyAppBridge.back(); });
byId("consumeHit").addEventListener("click", function () { window.HyAppBridge.post("openLevelConsume", { track: state.track }); });
window.addEventListener("resize", resizeApp);
window.addEventListener("orientationchange", function () { window.requestAnimationFrame(resizeApp); });
render();
window.HyAppBridge.ready({ page: "level" });
loadTrack(state.track).finally(function () {
loadOverview();
loadUserProfile();
});
})();
</script>
</body>
</html>