hyapp-h5/vip/index.html
2026-05-29 19:55:17 +08:00

1037 lines
36 KiB
HTML

<!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"
/>
<title>VIP</title>
<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;
}
body {
display: block;
}
button {
border: 0;
padding: 0;
background: transparent;
color: inherit;
font: inherit;
cursor: pointer;
outline: none;
}
.app-viewport {
position: relative;
width: 375px;
height: 766px;
margin: 0 auto;
transform: scale(var(--app-scale));
transform-origin: top center;
}
.page {
position: relative;
width: 375px;
height: 766px;
overflow: hidden;
background: #130a2b;
}
.figma-bg {
position: absolute;
inset: 0;
z-index: 0;
width: 375px;
height: 766px;
object-fit: cover;
pointer-events: none;
}
.bg {
position: absolute;
inset: 0;
pointer-events: none;
overflow: hidden;
}
.bg-starry {
position: absolute;
left: -126px;
top: -76px;
width: 631px;
height: 350px;
object-fit: cover;
opacity: 0.3;
transform: rotate(90deg);
}
.bg-meteor {
position: absolute;
left: -16px;
top: -113px;
width: 371px;
height: 371px;
object-fit: cover;
transform: rotate(10.98deg);
}
.bg-palace {
position: absolute;
left: -15px;
top: -67px;
width: 406px;
height: 389px;
object-fit: cover;
}
.bg::after {
content: '';
position: absolute;
left: 0;
right: 0;
top: 244px;
height: 566px;
background: #11071f;
}
.nav {
position: absolute;
left: 0;
top: 0;
z-index: 4;
width: 375px;
height: 44px;
}
.back {
position: absolute;
left: 16px;
top: 10px;
width: 24px;
height: 24px;
background: url('assets/back.png') center/24px 24px no-repeat;
}
.title-vip {
position: absolute;
left: 167px;
top: 13px;
width: 41px;
height: 18px;
object-fit: contain;
}
.hero-badge {
position: absolute;
z-index: 2;
left: 104px;
top: 55px;
width: 167px;
height: 167px;
object-fit: contain;
}
.section-title {
position: absolute;
z-index: 3;
display: flex;
align-items: center;
gap: 4px;
height: 18px;
}
.section-title img {
width: 59px;
height: 14px;
object-fit: fill;
}
.section-title .right {
transform: scaleX(-1);
}
.section-title span {
display: block;
color: #fff;
font-size: 16px;
line-height: 16px;
font-weight: 800;
white-space: nowrap;
text-align: center;
text-shadow: 0 2px 0 #be1e21;
background: linear-gradient(
108deg,
#fff 27%,
#f4cbff 49%,
#fff 72%
);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.decoration-title {
left: 38px;
top: 246px;
}
.vip-title {
left: 69px;
top: 522px;
}
.decor-card {
position: absolute;
z-index: 3;
color: #fff;
text-align: center;
}
.decor-card .frame {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
object-fit: fill;
pointer-events: none;
}
.decor-card .item {
position: absolute;
object-fit: contain;
pointer-events: none;
}
.decor-card .label {
position: absolute;
left: 0;
right: 0;
bottom: 13px;
font-size: 13px;
line-height: 13px;
white-space: nowrap;
}
.card-badge {
left: 16px;
top: 278px;
width: 110px;
height: 110px;
}
.card-badge .item {
left: 25px;
top: 16px;
width: 60px;
height: 60px;
}
.card-frame {
left: 133px;
top: 278px;
width: 110px;
height: 110px;
}
.card-frame .item {
left: 25px;
top: 16px;
width: 60px;
height: 60px;
}
.card-mount {
left: 250px;
top: 278px;
width: 110px;
height: 110px;
}
.card-mount .item {
left: 25px;
top: 16px;
width: 60px;
height: 60px;
}
.card-mic {
left: 16px;
top: 396px;
width: 110px;
height: 110px;
}
.card-mic .item {
left: 25px;
top: 16px;
width: 60px;
height: 60px;
}
.card-entry {
left: 133px;
top: 396px;
width: 227px;
height: 110px;
}
.card-entry .item {
left: 19px;
top: 24px;
width: 189px;
height: 44px;
object-fit: fill;
}
.privileges {
position: absolute;
z-index: 3;
left: 0;
top: 566px;
width: 375px;
height: 200px;
}
.privilege {
position: absolute;
width: 90px;
min-height: 98px;
text-align: center;
color: #fff;
}
.privilege .circle {
position: relative;
width: 56px;
height: 56px;
margin: 0 auto;
border-radius: 50%;
background: #4d2f7d;
}
.privilege .circle img.bg-circle {
position: absolute;
inset: 0;
width: 56px;
height: 56px;
object-fit: contain;
}
.privilege .circle img.icon {
position: absolute;
left: 16px;
top: 16px;
width: 24px;
height: 24px;
object-fit: contain;
}
.privilege .id-mark {
position: absolute;
left: 0;
top: 15px;
width: 56px;
color: #fff;
font-size: 26px;
line-height: 26px;
font-weight: 800;
letter-spacing: 0;
}
.privilege .label {
margin-top: 4px;
font-size: 14px;
line-height: 16px;
text-transform: capitalize;
}
.privilege.profile {
left: 11px;
top: 0;
}
.privilege.nickname {
left: 143px;
top: 0;
}
.privilege.room {
left: 275px;
top: 0;
}
.privilege.id {
left: 11px;
top: 108px;
}
.purchase-bar {
position: fixed;
z-index: 50;
left: 50%;
bottom: 0;
width: 375px;
height: 107px;
overflow: hidden;
transform: translateX(-50%) scale(var(--app-scale));
transform-origin: bottom center;
background: linear-gradient(180deg, #3e249d 0%, #20113a 69%);
}
.purchase-bar::before {
content: '';
position: absolute;
inset: 0;
opacity: 0.23;
background: url('assets/bottom-texture.png') center/100% 100%
no-repeat;
mix-blend-mode: plus-lighter;
}
.price {
position: absolute;
left: 24px;
top: 31px;
display: flex;
align-items: center;
gap: 4px;
color: #ffe4bc;
font-size: 18px;
line-height: 18px;
font-weight: 500;
white-space: nowrap;
}
.price img {
width: 22px;
height: 22px;
object-fit: contain;
}
.activate {
position: absolute;
left: 211px;
top: 17px;
width: 140px;
height: 50px;
border: 1px solid #fff09b;
color: #fff;
font-size: 18px;
line-height: 50px;
font-weight: 700;
text-align: center;
background: #2a1351;
overflow: hidden;
}
.activate::before {
content: '';
position: absolute;
inset: -8px -20px;
opacity: 0.8;
background: url('assets/button-texture.png') center/100% 100%
no-repeat;
}
.activate::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
176deg,
#462a78 36%,
rgba(59, 36, 100, 0) 63%
);
mix-blend-mode: plus-lighter;
}
.activate span {
position: relative;
z-index: 2;
}
.activate .corner {
position: absolute;
z-index: 3;
width: 16px;
height: 16px;
object-fit: contain;
}
.activate .c1 {
left: 0;
top: 0;
}
.activate .c2 {
right: 0;
top: 0;
transform: rotate(90deg);
}
.activate .c3 {
right: 0;
bottom: 0;
transform: rotate(180deg);
}
.activate .c4 {
left: 0;
bottom: 0;
transform: rotate(270deg);
}
.activate[disabled] {
opacity: 0.72;
cursor: default;
}
.home-indicator {
position: absolute;
left: 120px;
bottom: 9px;
z-index: 6;
width: 134px;
height: 5px;
border-radius: 100px;
background: #fff;
}
.loading-overlay {
position: absolute;
inset: 0;
z-index: 30;
display: none;
align-items: center;
justify-content: center;
background: rgba(19, 10, 43, 0.28);
backdrop-filter: blur(1px);
}
.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, 0.18);
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.28);
}
.cat-loader svg {
width: 54px;
height: 54px;
overflow: visible;
animation: cat-float 1.1s ease-in-out infinite;
}
.cat-loader .cat-line {
fill: none;
stroke: #fff;
stroke-width: 3.4;
stroke-linecap: round;
stroke-linejoin: round;
}
.cat-loader .cat-eye {
transform-origin: center;
animation: cat-blink 1.6s ease-in-out infinite;
}
.cat-loader .cat-whisker {
opacity: 0.9;
animation: cat-whisker 1.1s ease-in-out infinite;
}
.toast {
position: fixed;
left: 50%;
bottom: calc(126px * var(--app-scale));
z-index: 60;
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;
}
.bg,
.title-vip,
.hero-badge,
.section-title,
.decor-card,
.privileges {
display: none;
}
.back {
background: transparent;
}
@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(0.18);
}
}
@keyframes cat-whisker {
0%,
100% {
opacity: 0.62;
}
50% {
opacity: 1;
}
}
</style>
</head>
<body>
<div id="appViewport" class="app-viewport">
<main class="page">
<img
class="figma-bg"
src="assets/figma-vip-content-no-status.png"
alt=""
/>
<div class="bg" aria-hidden="true">
<img class="bg-starry" src="assets/bg-starry.png" alt="" />
<img class="bg-meteor" src="assets/bg-meteor.png" alt="" />
<img class="bg-palace" src="assets/bg-palace.png" alt="" />
</div>
<nav class="nav" aria-label="VIP navigation">
<button
class="back"
id="backButton"
aria-label="Back"
></button>
<img
class="title-vip"
src="assets/title-vip.png"
alt="VIP"
/>
</nav>
<img class="hero-badge" src="assets/badge-vip7.png" alt="" />
<div class="section-title decoration-title">
<img src="assets/divider.png" alt="" />
<span>Decoration Privileges</span>
<img class="right" src="assets/divider.png" alt="" />
</div>
<div class="decor-card card-badge">
<img
class="frame"
src="assets/privilege-frame.png"
alt=""
/>
<img class="item" src="assets/badge-vip7.png" alt="" />
<div class="label">SVIP Badge</div>
</div>
<div class="decor-card card-frame">
<img
class="frame"
src="assets/privilege-frame.png"
alt=""
/>
<img class="item" src="assets/avatar-frame.png" alt="" />
<div class="label">Frame</div>
</div>
<div class="decor-card card-mount">
<img
class="frame"
src="assets/privilege-frame.png"
alt=""
/>
<img class="item" src="assets/mount.png" alt="" />
<div class="label">Mount</div>
</div>
<div class="decor-card card-mic">
<img
class="frame"
src="assets/privilege-frame.png"
alt=""
/>
<img class="item" src="assets/mic-animation.png" alt="" />
<div class="label">Mic Animation</div>
</div>
<div class="decor-card card-entry">
<img class="frame" src="assets/entry-frame.png" alt="" />
<img class="item" src="assets/entry-effect.png" alt="" />
<div class="label">Entry Effect</div>
</div>
<div class="section-title vip-title">
<img src="assets/divider.png" alt="" />
<span>VIP Privileges</span>
<img class="right" src="assets/divider.png" alt="" />
</div>
<section class="privileges" aria-label="VIP privileges">
<div class="privilege profile">
<div class="circle">
<img
class="bg-circle"
src="assets/privilege-circle.png"
alt=""
/>
<img
class="icon"
src="assets/icon-profile.png"
alt=""
/>
</div>
<div class="label">Gif Profile<br />Picture</div>
</div>
<div class="privilege nickname">
<div class="circle">
<img
class="bg-circle"
src="assets/privilege-circle.png"
alt=""
/>
<img
class="icon"
src="assets/icon-nickname.png"
alt=""
/>
</div>
<div class="label">Colorful<br />Nickname</div>
</div>
<div class="privilege room">
<div class="circle">
<img
class="bg-circle"
src="assets/privilege-circle.png"
alt=""
/>
<img
class="icon"
src="assets/icon-room.png"
alt=""
/>
</div>
<div class="label">Gif Room<br />Picture</div>
</div>
<div class="privilege id">
<div class="circle">
<img
class="bg-circle"
src="assets/privilege-circle.png"
alt=""
/>
<div class="id-mark">ID</div>
</div>
<div class="label">Colorful ID</div>
</div>
</section>
<div
id="loadingOverlay"
class="loading-overlay"
aria-hidden="true"
>
<div class="cat-loader" role="status" aria-label="Loading">
<svg viewBox="0 0 64 64" aria-hidden="true">
<path class="cat-line" d="M17 27 13 12 25 20" />
<path class="cat-line" d="M47 27 51 12 39 20" />
<path
class="cat-line"
d="M14 34c0-12 8-20 18-20s18 8 18 20c0 11-8 18-18 18S14 45 14 34Z"
/>
<path class="cat-line cat-eye" d="M25 33h.1" />
<path class="cat-line cat-eye" d="M39 33h.1" />
<path class="cat-line" d="M32 39v2" />
<path class="cat-line" d="M27 44c2 2 8 2 10 0" />
<path class="cat-line cat-whisker" d="M21 39h-8" />
<path
class="cat-line cat-whisker"
d="M22 43l-8 3"
/>
<path class="cat-line cat-whisker" d="M43 39h8" />
<path class="cat-line cat-whisker" d="M42 43l8 3" />
</svg>
</div>
</div>
</main>
</div>
<section class="purchase-bar" aria-label="VIP purchase">
<div class="price">
<img src="assets/coin.png" alt="" />
<span id="priceText">30.5 M / 30 Days</span>
</div>
<button class="activate" id="activateButton">
<img class="corner c1" src="assets/corner.png" alt="" />
<img class="corner c2" src="assets/corner.png" alt="" />
<img class="corner c3" src="assets/corner.png" alt="" />
<img class="corner c4" src="assets/corner.png" alt="" />
<span id="activateText">Activate</span>
</button>
<div class="home-indicator"></div>
</section>
<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 DESIGN_WIDTH = 375;
var DESIGN_HEIGHT = 766;
var PURCHASE_HEIGHT = 107;
var state = {
loading: false,
purchasing: false,
packages: [],
currentVip: null,
selectedPackage: null,
};
function byId(id) {
return document.getElementById(id);
}
function queryValue(name) {
return new URLSearchParams(window.location.search).get(
name
);
}
function mockEnabled() {
return queryValue('mock') === '1';
}
function resizeApp() {
var widthScale =
window.innerWidth <= 480
? window.innerWidth / DESIGN_WIDTH
: 1;
var scale = Math.max(0.1, widthScale);
document.documentElement.style.setProperty(
'--app-scale',
String(scale)
);
document.body.style.minHeight =
Math.ceil((DESIGN_HEIGHT + PURCHASE_HEIGHT) * scale) +
'px';
}
function setLoading(loading) {
state.loading = loading;
byId('loadingOverlay').classList.toggle('active', loading);
}
function toast(message) {
var node = byId('toast');
node.textContent = message || '';
node.classList.add('show');
window.clearTimeout(toast.timer);
toast.timer = window.setTimeout(function () {
node.classList.remove('show');
}, 1800);
}
function formatCoin(value) {
var num = Number(value || 0);
if (num >= 1000000) {
var m = num / 1000000;
return (
(Math.round(m * 10) / 10)
.toString()
.replace(/\.0$/, '') + ' M'
);
}
if (num >= 1000) {
var k = num / 1000;
return (
(Math.round(k * 10) / 10)
.toString()
.replace(/\.0$/, '') + ' K'
);
}
return String(num);
}
function formatDays(durationMS) {
var days = Math.max(
1,
Math.round(Number(durationMS || 0) / 86400000)
);
return days + ' Days';
}
function samplePackages() {
return {
current_vip: {
level: 0,
name: '',
active: false,
started_at_ms: 0,
expires_at_ms: 0,
},
packages: [
{
level: 7,
name: 'VIP7',
price_coin: 30500000,
duration_ms: 2592000000,
can_purchase: true,
sort_order: 7,
reward_items: [],
},
],
};
}
function choosePackage(packages, currentVip) {
var requested = Number(queryValue('level') || 0);
var list = (packages || []).slice().sort(function (a, b) {
return (
Number(a.sort_order || a.level || 0) -
Number(b.sort_order || b.level || 0)
);
});
if (requested) {
return (
list.find(function (item) {
return Number(item.level) === requested;
}) ||
list[list.length - 1] ||
null
);
}
if (currentVip && currentVip.level) {
return (
list.find(function (item) {
return (
Number(item.level) ===
Number(currentVip.level)
);
}) ||
list[list.length - 1] ||
null
);
}
return (
list.find(function (item) {
return item.can_purchase;
}) ||
list[list.length - 1] ||
null
);
}
function render() {
var selected = state.selectedPackage;
var currentVip = state.currentVip || {};
var priceText = selected
? formatCoin(selected.price_coin) +
' / ' +
formatDays(selected.duration_ms)
: '30.5 M / 30 Days';
var activeForSelected =
currentVip.active &&
selected &&
Number(currentVip.level || 0) >=
Number(selected.level || 0);
var locked =
selected &&
selected.can_purchase === false &&
!activeForSelected;
byId('priceText').textContent = priceText;
byId('activateText').textContent = activeForSelected
? 'Activated'
: 'Activate';
byId('activateButton').disabled =
state.loading ||
state.purchasing ||
activeForSelected ||
locked;
}
function loadVIP() {
if (mockEnabled()) return Promise.resolve(samplePackages());
return window.HyAppAPI.vip.packages();
}
function loadMine() {
if (mockEnabled())
return Promise.resolve(samplePackages().current_vip);
return window.HyAppAPI.vip.me();
}
function hydrate() {
setLoading(true);
return loadVIP()
.then(function (data) {
state.packages = data.packages || [];
state.currentVip = data.current_vip || null;
state.selectedPackage = choosePackage(
state.packages,
state.currentVip
);
render();
return loadMine()
.then(function (vip) {
state.currentVip = vip || state.currentVip;
state.selectedPackage = choosePackage(
state.packages,
state.currentVip
);
render();
})
.catch(function () {
render();
});
})
.catch(function (error) {
toast((error && error.message) || 'Load failed');
})
.finally(function () {
setLoading(false);
render();
});
}
function commandId(level) {
var appCode =
window.HyAppAPI && window.HyAppAPI.getAppCode
? window.HyAppAPI.getAppCode()
: 'lalu';
return (
'vip_' +
appCode +
'_' +
level +
'_' +
Date.now() +
'_' +
Math.random().toString(36).slice(2, 8)
);
}
function purchase() {
var selected = state.selectedPackage;
if (!selected || state.purchasing) return;
state.purchasing = true;
setLoading(true);
render();
window.HyAppAPI.vip
.purchase({
command_id: commandId(selected.level),
level: Number(selected.level),
})
.then(function (data) {
state.currentVip = data.vip || state.currentVip;
toast('Activated');
return hydrate();
})
.catch(function (error) {
toast(
(error && error.message) || 'Purchase failed'
);
})
.finally(function () {
state.purchasing = false;
setLoading(false);
render();
});
}
window.addEventListener('resize', resizeApp);
window.addEventListener('orientationchange', resizeApp);
byId('backButton').addEventListener('click', function () {
window.HyAppBridge.back();
});
byId('activateButton').addEventListener('click', purchase);
resizeApp();
render();
window.HyAppParams.parse();
window.HyAppBridge.ready({ page: 'vip' });
hydrate();
})();
</script>
</body>
</html>