hyapp-h5/vip/index.html
2026-06-02 15:53:50 +08:00

2129 lines
81 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;
touch-action: pan-y;
background: #130a2b;
}
.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-ornaments {
position: absolute;
left: 0;
top: 0;
width: 375px;
height: 244px;
object-fit: cover;
}
.bg::after {
content: '';
position: absolute;
left: 0;
right: 0;
top: 244px;
height: 566px;
background: #11071f;
}
.nav {
position: fixed;
left: 50%;
top: 0;
z-index: 70;
width: 375px;
height: 44px;
transform: translateX(-50%) scale(var(--app-scale));
transform-origin: top center;
}
.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;
border-radius: 1px;
transform: rotate(45deg);
}
.title-vip {
position: absolute;
left: 50%;
top: 10px;
width: 50px;
height: 22px;
color: #5f270f;
font-family: Georgia, 'Times New Roman', serif;
font-size: 23px;
line-height: 22px;
font-style: italic;
font-weight: 900;
text-align: center;
letter-spacing: 0;
transform: translateX(-50%) skewX(-7deg);
-webkit-text-stroke: 1px #55250f;
text-shadow:
0 1px 0 #cb7628,
0 2px 1px rgba(43, 16, 5, 0.75),
0 0 4px rgba(255, 199, 83, 0.48);
}
.title-vip::before {
content: attr(aria-label);
position: absolute;
inset: 0;
background: linear-gradient(180deg, #fffbe0 0%, #ffe27a 30%, #d87525 72%, #6e2b12 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
-webkit-text-stroke: 0.35px #fff0a8;
}
.hero-badge {
position: absolute;
z-index: 2;
left: 104px;
top: 55px;
width: 167px;
height: 167px;
}
.hero-badge-image {
display: block;
width: 167px;
height: 167px;
object-fit: contain;
}
.hero-badge-image.is-dynamic-hidden {
visibility: hidden;
}
.hero-badge-animation {
position: absolute;
inset: 0;
z-index: 4;
display: none;
pointer-events: none;
}
.hero-badge-animation.is-active {
display: block;
}
.hero-badge-animation video,
.hero-badge-animation canvas {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
.vip-switch {
position: absolute;
z-index: 12;
top: 116px;
width: 36px;
height: 64px;
}
.vip-switch::before {
content: '';
position: absolute;
left: 13px;
top: 23px;
width: 13px;
height: 13px;
border-left: 3px solid #fff;
border-bottom: 3px solid #fff;
border-radius: 2px;
filter: drop-shadow(0 0 4px rgba(230, 151, 255, 0.72));
transform: rotate(45deg);
}
.vip-switch-next::before {
left: 8px;
transform: rotate(225deg);
}
.vip-switch-prev {
left: 16px;
}
.vip-switch-next {
right: 16px;
}
.vip-switch[disabled] {
opacity: 0.36;
cursor: default;
}
.vip-switch.is-hidden {
display: none;
}
.vip-level-stamp {
position: absolute;
left: 50%;
z-index: 3;
transform: translateX(-50%);
color: #fff5af;
font-family: Georgia, 'Times New Roman', serif;
font-weight: 900;
font-style: italic;
letter-spacing: 0;
text-align: center;
text-shadow:
0 1px 0 #7a2b14,
0 0 4px rgba(117, 31, 255, 0.9),
0 0 6px rgba(255, 226, 92, 0.58);
-webkit-text-stroke: 0.7px #7a2b14;
}
.vip-level-stamp.is-hidden {
display: none;
}
.hero-vip-stamp {
bottom: 40px;
min-width: 42px;
padding: 0 3px;
font-size: 17px;
line-height: 18px;
}
.section-title {
position: absolute;
z-index: 3;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 18px;
}
.section-title img {
display: none;
}
.section-title::before,
.section-title::after {
content: '';
width: 74px;
height: 16px;
flex: 1 1 74px;
max-width: 96px;
background:
radial-gradient(circle at 100% 50%, #f1c7ff 0 1.6px, transparent 2px),
radial-gradient(ellipse at 86% 50%, transparent 0 4px, #d997ff 4.4px 5.3px, transparent 5.7px),
radial-gradient(ellipse at 73% 50%, transparent 0 5px, rgba(217, 151, 255, 0.95) 5.3px 6.2px, transparent 6.6px),
linear-gradient(90deg, transparent 0, rgba(217, 151, 255, 0.12) 18%, rgba(217, 151, 255, 0.95) 72%, transparent 100%) center / 100% 1px no-repeat;
opacity: 1;
}
.section-title::after {
transform: scaleX(-1);
}
.section-title span {
display: block;
color: #fff;
font-family: Arial, Helvetica, sans-serif;
font-size: 17px;
line-height: 18px;
font-weight: 900;
white-space: nowrap;
text-align: center;
text-shadow:
0 1px 0 #a3229d,
0 2px 0 #b31942,
1px 0 0 #b94cff,
-1px 0 0 #b94cff,
0 -1px 0 #b94cff,
0 0 5px rgba(233, 116, 255, 0.72);
-webkit-text-stroke: 0.35px #f0b5ff;
-webkit-text-fill-color: #fff;
}
.decoration-title {
left: 18px;
top: 246px;
width: 339px;
}
.vip-title {
left: 68px;
top: 522px;
width: 239px;
}
.decor-card {
position: absolute;
z-index: 3;
color: #fff;
text-align: center;
--frame-line: #d9a6ff;
--frame-corner: #ffd9ff;
border: 1px solid var(--frame-line);
background:
radial-gradient(circle at 50% 26%, rgba(98, 52, 132, 0.22), transparent 46%),
rgba(17, 6, 30, 0.62);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.06),
inset 0 0 18px rgba(90, 35, 129, 0.2);
}
.decor-card.is-previewable {
cursor: pointer;
}
.decor-card::before {
content: '';
position: absolute;
inset: -2px;
pointer-events: none;
background:
linear-gradient(var(--frame-corner), var(--frame-corner)) left top / 11px 1px no-repeat,
linear-gradient(var(--frame-corner), var(--frame-corner)) left top / 1px 11px no-repeat,
linear-gradient(var(--frame-corner), var(--frame-corner)) right top / 11px 1px no-repeat,
linear-gradient(var(--frame-corner), var(--frame-corner)) right top / 1px 11px no-repeat,
linear-gradient(var(--frame-corner), var(--frame-corner)) left bottom / 11px 1px no-repeat,
linear-gradient(var(--frame-corner), var(--frame-corner)) left bottom / 1px 11px no-repeat,
linear-gradient(var(--frame-corner), var(--frame-corner)) right bottom / 11px 1px no-repeat,
linear-gradient(var(--frame-corner), var(--frame-corner)) right bottom / 1px 11px no-repeat;
}
.decor-card .frame {
display: none;
}
.decor-card .item {
position: absolute;
object-fit: contain;
pointer-events: none;
}
.hero-badge-image:not([src]),
.hero-badge-image[src=''],
.decor-card .item:not([src]),
.decor-card .item[src=''] {
display: none;
}
.card-vip-stamp {
top: 52px;
min-width: 26px;
font-size: 9px;
line-height: 10px;
}
.decor-card .label {
position: absolute;
left: 4px;
right: 4px;
bottom: 13px;
font-size: 13px;
line-height: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.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: 11px;
top: 31px;
width: 205px;
height: 33px;
object-fit: contain;
}
.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: #3d2766;
}
.privilege .circle img {
display: none;
}
.privilege.profile .circle::before {
content: '';
position: absolute;
left: 15px;
top: 16px;
width: 25px;
height: 22px;
border: 3px solid #fff;
border-radius: 3px;
background:
radial-gradient(circle at 74% 28%, #fff 0 2px, transparent 2.5px),
linear-gradient(135deg, transparent 48%, #fff 49% 55%, transparent 56%);
}
.privilege.nickname .circle::before {
content: '';
position: absolute;
left: 14px;
top: 17px;
width: 28px;
height: 20px;
border-radius: 2px;
background:
radial-gradient(circle at 25% 50%, #3d2766 0 3px, transparent 3.4px),
linear-gradient(#3d2766, #3d2766) 15px 6px / 11px 2px no-repeat,
linear-gradient(#3d2766, #3d2766) 15px 12px / 11px 2px no-repeat,
#fff;
}
.privilege.room .circle::before {
content: '';
position: absolute;
left: 17px;
top: 24px;
width: 22px;
height: 18px;
border-radius: 2px;
background: #fff;
}
.privilege.room .circle::after {
content: '';
position: absolute;
left: 15px;
top: 17px;
width: 24px;
height: 24px;
border-left: 5px solid #fff;
border-top: 5px solid #fff;
transform: rotate(45deg);
}
.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: 3px;
max-width: 184px;
color: #ffe4bc;
font-size: 16px;
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(132px * var(--app-scale));
z-index: 140;
min-width: 168px;
max-width: 328px;
transform: translateX(-50%);
padding: 13px 22px;
border-radius: 24px;
background: rgba(0, 0, 0, 0.82);
color: #fff;
font-size: 15px;
line-height: 20px;
font-weight: 700;
text-align: center;
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.36);
opacity: 0;
pointer-events: none;
transition: opacity 0.18s ease;
}
.toast.show {
opacity: 1;
}
.effect-preview-overlay {
position: fixed;
inset: 0;
z-index: 120;
display: none;
align-items: center;
justify-content: center;
padding: calc(env(safe-area-inset-top) + 28px) 18px
calc(env(safe-area-inset-bottom) + 28px);
background: rgba(0, 0, 0, 0.9);
}
.effect-preview-overlay.is-open {
display: flex;
}
.effect-preview-close {
position: absolute;
right: 16px;
top: calc(env(safe-area-inset-top) + 16px);
z-index: 3;
width: 40px;
height: 40px;
}
.effect-preview-close::before,
.effect-preview-close::after {
content: '';
position: absolute;
left: 10px;
top: 19px;
width: 20px;
height: 2px;
border-radius: 2px;
background: #fff;
box-shadow: 0 0 8px rgba(230, 151, 255, 0.64);
}
.effect-preview-close::before {
transform: rotate(45deg);
}
.effect-preview-close::after {
transform: rotate(-45deg);
}
.effect-preview-stage {
position: relative;
width: var(--preview-width, calc(100vw - 64px));
height: var(--preview-height, calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 156px));
max-width: calc(100vw - 64px);
max-height: calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 156px);
display: flex;
align-items: center;
justify-content: center;
overflow: visible;
}
.effect-preview-image,
.effect-preview-animation {
position: absolute;
inset: 0;
display: block;
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
}
.effect-preview-image {
object-fit: contain;
object-position: center center;
}
.effect-preview-image.is-dynamic-hidden {
visibility: hidden;
}
.effect-preview-animation {
display: none;
pointer-events: none;
}
.effect-preview-animation.is-active {
display: block;
}
.effect-preview-animation video,
.effect-preview-animation canvas {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
object-position: center center;
}
@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">
<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="" />
<img
class="bg-ornaments"
src="assets/top-ornaments.png"
alt=""
/>
</div>
<div class="hero-badge" aria-hidden="true">
<img id="heroBadgeImage" class="hero-badge-image" alt="" />
<div
id="heroBadgeAnimation"
class="hero-badge-animation"
></div>
<div id="heroVipName" class="vip-level-stamp hero-vip-stamp">
VIP
</div>
</div>
<button
type="button"
id="vipPrevButton"
class="vip-switch vip-switch-prev"
aria-label="Previous VIP"
></button>
<button
type="button"
id="vipNextButton"
class="vip-switch vip-switch-next"
aria-label="Next VIP"
></button>
<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>
<button
type="button"
class="decor-card card-badge"
data-vip-slot="badge"
>
<img
class="frame"
src="assets/privilege-frame.png"
alt=""
/>
<img id="badgeItem" class="item" alt="" />
<span
id="badgeVipName"
class="vip-level-stamp card-vip-stamp"
>
VIP
</span>
<span id="badgeLabel" class="label">SVIP Badge</span>
</button>
<button
type="button"
class="decor-card card-frame"
data-vip-slot="avatar_frame"
>
<img
class="frame"
src="assets/privilege-frame.png"
alt=""
/>
<img id="frameItem" class="item" alt="" />
<span
id="frameVipName"
class="vip-level-stamp card-vip-stamp"
>
VIP
</span>
<span id="frameLabel" class="label">Frame</span>
</button>
<button
type="button"
class="decor-card card-mount"
data-vip-slot="vehicle"
>
<img
class="frame"
src="assets/privilege-frame.png"
alt=""
/>
<img id="mountItem" class="item" alt="" />
<span id="mountLabel" class="label">Mount</span>
</button>
<button
type="button"
class="decor-card card-mic"
data-vip-slot="mic_seat_animation"
>
<img
class="frame"
src="assets/privilege-frame.png"
alt=""
/>
<img id="micItem" class="item" alt="" />
<span id="micLabel" class="label">Mic Animation</span>
</button>
<button
type="button"
class="decor-card card-entry"
data-vip-slot="entry_effect"
>
<img class="frame" src="assets/entry-frame.png" alt="" />
<img id="entryItem" class="item" alt="" />
<span id="entryLabel" class="label">Entry Effect</span>
</button>
<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>
<nav class="nav" aria-label="VIP navigation">
<button class="back" id="backButton" aria-label="Back"></button>
<div class="title-vip" aria-label="VIP">VIP</div>
</nav>
<section class="purchase-bar" aria-label="VIP purchase">
<div class="price">
<img src="assets/coin.png" alt="" />
<span id="priceText">30,500,000 / 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>
<div
id="effectPreviewOverlay"
class="effect-preview-overlay"
role="button"
tabindex="0"
aria-label="Close preview"
aria-hidden="true"
>
<button
type="button"
id="effectPreviewClose"
class="effect-preview-close"
aria-label="Close preview"
></button>
<div id="effectPreviewStage" class="effect-preview-stage">
<img id="effectPreviewImage" class="effect-preview-image" alt="" />
<div
id="effectPreviewAnimation"
class="effect-preview-animation"
></div>
</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/effect-player.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,
selectedPackageIndex: -1,
userSelectedPackage: false,
badgeAnimationURL: '',
};
var badgeEffectPlayer = window.HyAppEffectPlayer.create(
'#heroBadgeAnimation',
{
width: 167,
height: 167,
waitForWindowLoad: true,
}
);
var previewEffectPlayer = window.HyAppEffectPlayer.create(
'#effectPreviewAnimation',
{
waitForWindowLoad: false,
}
);
var previewSequence = 0;
var swipeState = null;
var suppressPreviewClick = false;
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 (!Number.isFinite(num)) num = 0;
return String(Math.round(num)).replace(
/\B(?=(\d{3})+(?!\d))/g,
','
);
}
function formatDays(durationMS) {
var days = Math.max(
1,
Math.round(Number(durationMS || 0) / 86400000)
);
return days + ' Days';
}
var DECORATION_SLOTS = [
{
key: 'badge',
itemId: 'badgeItem',
labelId: 'badgeLabel',
stampId: 'badgeVipName',
label: 'SVIP Badge',
types: ['badge', 'vip_badge'],
hints: ['badge', 'svip'],
},
{
key: 'avatar_frame',
itemId: 'frameItem',
labelId: 'frameLabel',
stampId: 'frameVipName',
label: 'Frame',
types: ['avatar_frame', 'profile_frame', 'frame'],
hints: ['avatar', 'frame'],
},
{
key: 'vehicle',
itemId: 'mountItem',
labelId: 'mountLabel',
label: 'Mount',
types: ['vehicle', 'mount', 'car'],
hints: ['vehicle', 'mount', 'car'],
},
{
key: 'mic_seat_animation',
itemId: 'micItem',
labelId: 'micLabel',
label: 'Mic Animation',
types: [
'mic_seat_animation',
'mic_seat_icon',
'mic_animation',
'microphone_effect',
],
hints: ['mic', 'microphone'],
},
{
key: 'entry_effect',
itemId: 'entryItem',
labelId: 'entryLabel',
label: 'Entry Effect',
types: [
'floating_screen',
'entry_effect',
'entrance_effect',
'entry',
],
hints: ['floating', 'entry', 'entrance'],
},
];
function readField(source, names, fallback) {
if (!source) return fallback;
for (var index = 0; index < names.length; index += 1) {
var name = names[index];
if (
Object.prototype.hasOwnProperty.call(source, name) &&
source[name] !== undefined &&
source[name] !== null
) {
return source[name];
}
}
return fallback;
}
function normalizeReward(item) {
return {
resource_id: readField(
item,
['resource_id', 'resourceId'],
0
),
resource_code: readField(
item,
['resource_code', 'resourceCode'],
''
),
resource_type: readField(
item,
['resource_type', 'resourceType'],
''
),
name: readField(item, ['name'], ''),
quantity: Number(readField(item, ['quantity'], 0) || 0),
expires_at_ms: Number(
readField(
item,
['expires_at_ms', 'expiresAtMs'],
0
) || 0
),
cover_url: readField(
item,
['cover_url', 'coverUrl'],
''
),
asset_url: readField(item, ['asset_url', 'assetUrl'], ''),
preview_url: readField(
item,
['preview_url', 'previewUrl'],
''
),
animation_url: readField(
item,
['animation_url', 'animationUrl'],
''
),
};
}
function normalizeVip(vip) {
if (!vip) return null;
return {
level: Number(readField(vip, ['level'], 0) || 0),
name: readField(vip, ['name'], ''),
active: !!readField(vip, ['active'], false),
started_at_ms: Number(
readField(vip, ['started_at_ms', 'startedAtMs'], 0) ||
0
),
expires_at_ms: Number(
readField(vip, ['expires_at_ms', 'expiresAtMs'], 0) ||
0
),
};
}
function normalizePackage(item) {
var rewards = readField(item, ['reward_items', 'rewardItems'], []);
return {
level: Number(readField(item, ['level'], 0) || 0),
name: readField(item, ['name'], ''),
status: readField(item, ['status'], ''),
price_coin: Number(
readField(
item,
[
'price_coin',
'priceCoin',
'coin_price',
'coinPrice',
],
0
) || 0
),
duration_ms: Number(
readField(item, ['duration_ms', 'durationMs'], 0) || 0
),
reward_resource_group_id: Number(
readField(
item,
[
'reward_resource_group_id',
'rewardResourceGroupId',
],
0
) || 0
),
reward_items: Array.isArray(rewards)
? rewards.map(normalizeReward)
: [],
can_purchase: readField(item, ['can_purchase', 'canPurchase'], false),
sort_order: Number(readField(item, ['sort_order', 'sortOrder'], 0) || 0),
purchase_locked_reason: readField(
item,
['purchase_locked_reason', 'purchaseLockedReason'],
''
),
};
}
function normalizePackageResponse(data) {
var packages = readField(data, ['packages', 'levels'], []);
return {
current_vip: normalizeVip(
readField(data, ['current_vip', 'currentVip', 'vip'], null)
),
packages: Array.isArray(packages)
? packages.map(normalizePackage)
: [],
};
}
function lower(value) {
return String(value || '').toLowerCase();
}
function slotMatchesReward(slot, reward) {
var type = lower(reward.resource_type);
var code = lower(reward.resource_code);
if (
slot.types.indexOf(type) >= 0 ||
slot.hints.some(function (hint) {
return code.indexOf(hint) >= 0;
})
) {
return true;
}
return false;
}
function rewardForSlot(rewards, slot) {
return (rewards || []).find(function (reward) {
return slotMatchesReward(slot, reward);
});
}
function coverURL(reward) {
if (!reward) return '';
var cover = String(
reward.cover_url || reward.preview_url || ''
).trim();
if (cover) return cover;
var assetURL = String(reward.asset_url || '').trim();
return isDynamicAssetURL(assetURL) ? '' : assetURL;
}
function animationURL(reward) {
if (!reward) return '';
var explicitURL = String(reward.animation_url || '').trim();
if (explicitURL) return explicitURL;
var assetURL = String(reward.asset_url || '').trim();
return isDynamicAssetURL(assetURL) ? assetURL : '';
}
function isDynamicAssetURL(url) {
var clean = String(url || '').split('?')[0].split('#')[0];
return /\.(svga|pag|mp4|webm|mov)$/i.test(clean);
}
function playBadgeAnimation() {
var url = state.badgeAnimationURL;
var heroImage = byId('heroBadgeImage');
if (!url) {
heroImage.classList.remove('is-dynamic-hidden');
badgeEffectPlayer.stop();
return;
}
heroImage.classList.remove('is-dynamic-hidden');
badgeEffectPlayer
.play(url)
.then(function () {
if (state.badgeAnimationURL === url) {
heroImage.classList.add('is-dynamic-hidden');
}
})
.catch(function () {
if (state.badgeAnimationURL === url) {
heroImage.classList.remove('is-dynamic-hidden');
}
});
}
function setPreviewData(card, cover, animation, label) {
if (!card) return;
if (cover) {
card.dataset.coverUrl = cover;
} else {
delete card.dataset.coverUrl;
}
if (animation) {
card.dataset.animationUrl = animation;
} else {
delete card.dataset.animationUrl;
}
card.dataset.previewLabel = label || '';
card.classList.toggle('is-previewable', !!(cover || animation));
}
function resetPreview() {
var image = byId('effectPreviewImage');
var animation = byId('effectPreviewAnimation');
var stage = byId('effectPreviewStage');
previewSequence += 1;
previewEffectPlayer.stop();
stage.style.removeProperty('--preview-width');
stage.style.removeProperty('--preview-height');
image.classList.remove('is-dynamic-hidden');
image.removeAttribute('src');
image.alt = '';
animation.classList.remove('is-active');
}
function previewKindFromCard(card) {
if (!card) return 'default';
if (card.classList.contains('card-badge')) return 'badge';
if (card.classList.contains('card-frame')) return 'frame';
if (card.classList.contains('card-mount')) return 'mount';
if (card.classList.contains('card-mic')) return 'mic';
if (card.classList.contains('card-entry')) return 'entry';
return 'default';
}
function previewLimits(kind) {
var limits = {
badge: { width: 0.7, height: 0.58 },
frame: { width: 0.78, height: 0.62 },
mount: { width: 0.76, height: 0.72 },
mic: { width: 0.62, height: 0.56 },
entry: { width: 0.9, height: 0.34 },
default: { width: 0.82, height: 0.68 },
};
return limits[kind] || limits.default;
}
function fitPreviewStage(width, height, kind) {
var stage = byId('effectPreviewStage');
var sourceWidth = Number(width || 0);
var sourceHeight = Number(height || 0);
var limit = previewLimits(kind);
var maxWidth = Math.max(
1,
Math.round((window.innerWidth - 64) * limit.width)
);
var maxHeight = Math.max(
1,
Math.round((window.innerHeight - 156) * limit.height)
);
if (!sourceWidth || !sourceHeight) {
stage.style.setProperty('--preview-width', maxWidth + 'px');
stage.style.setProperty(
'--preview-height',
maxHeight + 'px'
);
return;
}
var scale = Math.min(
maxWidth / sourceWidth,
maxHeight / sourceHeight
);
stage.style.setProperty(
'--preview-width',
Math.round(sourceWidth * scale) + 'px'
);
stage.style.setProperty(
'--preview-height',
Math.round(sourceHeight * scale) + 'px'
);
}
function fitPreviewImage(image, sequence, kind) {
if (sequence !== previewSequence) return;
fitPreviewStage(
image.naturalWidth,
image.naturalHeight,
kind
);
}
function closeEffectPreview() {
var overlay = byId('effectPreviewOverlay');
overlay.classList.remove('is-open');
overlay.setAttribute('aria-hidden', 'true');
resetPreview();
}
function openEffectPreview(card) {
var cover = card.dataset.coverUrl || '';
var animation = card.dataset.animationUrl || '';
if (!cover && !animation) return;
var overlay = byId('effectPreviewOverlay');
var image = byId('effectPreviewImage');
var previewKind = previewKindFromCard(card);
resetPreview();
var sequence = previewSequence;
var animationStarted = false;
function startAnimation() {
if (
animationStarted ||
!animation ||
sequence !== previewSequence
) {
return;
}
animationStarted = true;
previewEffectPlayer
.play(animation)
.then(function () {
if (sequence === previewSequence) {
var playerNode = byId(
'effectPreviewAnimation'
);
var media = playerNode.querySelector(
'video, canvas'
);
if (media && !cover) {
fitPreviewStage(
media.videoWidth ||
media.width ||
0,
media.videoHeight ||
media.height ||
0,
previewKind
);
}
image.classList.add('is-dynamic-hidden');
}
})
.catch(function () {
if (sequence === previewSequence) {
image.classList.remove(
'is-dynamic-hidden'
);
}
});
}
if (cover) {
image.onload = function () {
fitPreviewImage(image, sequence, previewKind);
startAnimation();
};
image.onerror = function () {
fitPreviewStage(0, 0, previewKind);
startAnimation();
};
image.src = cover;
image.alt = card.dataset.previewLabel || '';
if (image.complete && image.naturalWidth) {
fitPreviewImage(image, sequence, previewKind);
}
} else {
fitPreviewStage(0, 0, previewKind);
}
overlay.classList.add('is-open');
overlay.setAttribute('aria-hidden', 'false');
overlay.focus();
if (!cover) startAnimation();
}
function displayVipName(vipPackage) {
if (!vipPackage) return 'VIP';
return vipPackage.name || 'VIP' + vipPackage.level;
}
function setImage(node, url, altText, animationURL) {
if (!node) return;
if (!url) {
node.removeAttribute('src');
node.alt = '';
node.removeAttribute('data-animation-url');
return;
}
node.src = url;
node.alt = altText || '';
if (animationURL) {
node.dataset.animationUrl = animationURL;
} else {
node.removeAttribute('data-animation-url');
}
}
function applyDecorationAssets(vipPackage) {
var rewards = (vipPackage && vipPackage.reward_items) || [];
var vipName = displayVipName(vipPackage);
var badgeReward = rewardForSlot(rewards, DECORATION_SLOTS[0]);
var heroURL = coverURL(badgeReward) || coverURL(rewards[0]);
state.badgeAnimationURL = animationURL(badgeReward);
setImage(
byId('heroBadgeImage'),
heroURL,
badgeReward ? badgeReward.name : vipName,
state.badgeAnimationURL
);
byId('heroVipName').textContent = vipName;
byId('heroVipName').classList.add('is-hidden');
playBadgeAnimation();
DECORATION_SLOTS.forEach(function (slot) {
var reward = rewardForSlot(rewards, slot);
var url = coverURL(reward);
var effectURL = animationURL(reward);
var label = reward && reward.name ? reward.name : slot.label;
setImage(
byId(slot.itemId),
url,
label,
effectURL
);
setPreviewData(
document.querySelector(
'[data-vip-slot="' + slot.key + '"]'
),
url,
effectURL,
label
);
byId(slot.labelId).textContent = label;
if (slot.stampId) {
byId(slot.stampId).textContent = vipName;
byId(slot.stampId).classList.add('is-hidden');
}
});
}
function samplePackages() {
return {
current_vip: {
level: 0,
name: '',
active: false,
started_at_ms: 0,
expires_at_ms: 0,
},
packages: [
{
level: 1,
name: 'VIP1',
price_coin: 1000,
duration_ms: 604800000,
can_purchase: true,
sort_order: 10,
reward_items: [],
},
{
level: 2,
name: 'VIP2',
price_coin: 3000,
duration_ms: 604800000,
can_purchase: true,
sort_order: 20,
reward_items: [],
},
],
};
}
function comparePackages(a, b) {
var orderA = Number(a.sort_order || a.level || 0);
var orderB = Number(b.sort_order || b.level || 0);
if (orderA !== orderB) return orderA - orderB;
return Number(a.level || 0) - Number(b.level || 0);
}
function sortedPackages(packages) {
return (packages || []).slice().sort(comparePackages);
}
function packageIndex(vipPackage) {
if (!vipPackage) return -1;
var selectedLevel = Number(vipPackage.level || 0);
for (var index = 0; index < state.packages.length; index += 1) {
if (
Number(state.packages[index].level || 0) ===
selectedLevel
) {
return index;
}
}
return state.packages.indexOf(vipPackage);
}
function setSelectedPackage(vipPackage, userInitiated) {
if (userInitiated) {
state.userSelectedPackage = true;
}
var index = packageIndex(vipPackage);
if (index < 0 && state.packages.length) {
index = 0;
}
state.selectedPackageIndex = index;
state.selectedPackage =
index >= 0 ? state.packages[index] : null;
}
function selectedPackageOrDefault(currentVip) {
if (state.userSelectedPackage && state.selectedPackage) {
var currentSelection = state.packages.find(function (item) {
return (
Number(item.level || 0) ===
Number(state.selectedPackage.level || 0)
);
});
if (currentSelection) return currentSelection;
}
return choosePackage(state.packages, currentVip);
}
function selectPackageByIndex(index) {
if (
state.loading ||
state.purchasing ||
state.packages.length < 2
) {
updateSwitchButtons();
return false;
}
var nextIndex = Math.max(
0,
Math.min(index, state.packages.length - 1)
);
if (nextIndex === state.selectedPackageIndex) {
updateSwitchButtons();
return false;
}
setSelectedPackage(state.packages[nextIndex], true);
render();
return true;
}
function selectPackageByDelta(delta) {
var index = packageIndex(state.selectedPackage);
if (index < 0) index = 0;
return selectPackageByIndex(index + delta);
}
function updateSwitchButtons() {
var prevButton = byId('vipPrevButton');
var nextButton = byId('vipNextButton');
var hasMultiplePackages = state.packages.length > 1;
var index = packageIndex(state.selectedPackage);
if (state.selectedPackageIndex !== index) {
state.selectedPackageIndex = index;
}
prevButton.classList.toggle(
'is-hidden',
!hasMultiplePackages
);
nextButton.classList.toggle(
'is-hidden',
!hasMultiplePackages
);
prevButton.disabled =
!hasMultiplePackages ||
state.loading ||
state.purchasing ||
index <= 0;
nextButton.disabled =
!hasMultiplePackages ||
state.loading ||
state.purchasing ||
index >= state.packages.length - 1;
}
function choosePackage(packages, currentVip) {
var requested = Number(queryValue('level') || 0);
var list = sortedPackages(packages);
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,500,000 / 30 Days';
var selectedLevel = Number((selected && selected.level) || 0);
var currentLevel = Number(currentVip.level || 0);
var sameLevelActive =
currentVip.active &&
selected &&
currentLevel === selectedLevel;
var lowerThanCurrent =
currentVip.active &&
selected &&
currentLevel > selectedLevel;
var locked =
selected &&
((selected.can_purchase === false &&
!sameLevelActive) ||
lowerThanCurrent);
byId('priceText').textContent = priceText;
byId('activateText').textContent = sameLevelActive
? 'Renew'
: 'Activate';
byId('activateButton').disabled =
state.loading ||
state.purchasing ||
locked;
applyDecorationAssets(selected);
updateSwitchButtons();
}
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) {
var normalized = normalizePackageResponse(data || {});
state.packages = sortedPackages(normalized.packages);
state.currentVip = normalized.current_vip;
setSelectedPackage(
selectedPackageOrDefault(state.currentVip),
false
);
render();
return loadMine()
.then(function (vip) {
state.currentVip =
normalizeVip(vip) || state.currentVip;
setSelectedPackage(
selectedPackageOrDefault(
state.currentVip
),
false
);
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 =
normalizeVip(
readField(data, ['vip', 'current_vip', 'currentVip'], null)
) || state.currentVip;
toast('Purchase successful');
return hydrate();
})
.catch(function (error) {
toast(
(error && error.message) || 'Purchase failed'
);
})
.finally(function () {
state.purchasing = false;
setLoading(false);
render();
});
}
function handleSwipeStart(event) {
if (
state.loading ||
state.purchasing ||
event.touches.length !== 1
) {
swipeState = null;
return;
}
var target = event.target;
if (
target.closest &&
target.closest(
'.vip-switch, .effect-preview-overlay'
)
) {
swipeState = null;
return;
}
swipeState = {
x: event.touches[0].clientX,
y: event.touches[0].clientY,
};
}
function handleSwipeEnd(event) {
if (!swipeState || !event.changedTouches.length) return;
var touch = event.changedTouches[0];
var deltaX = touch.clientX - swipeState.x;
var deltaY = touch.clientY - swipeState.y;
swipeState = null;
if (
Math.abs(deltaX) < 44 ||
Math.abs(deltaX) < Math.abs(deltaY) * 1.2
) {
return;
}
suppressPreviewClick = true;
window.setTimeout(function () {
suppressPreviewClick = false;
}, 120);
selectPackageByDelta(deltaX < 0 ? 1 : -1);
}
window.addEventListener('resize', resizeApp);
window.addEventListener('orientationchange', resizeApp);
byId('backButton').addEventListener('click', function () {
window.HyAppBridge.back();
});
byId('vipPrevButton').addEventListener('click', function () {
selectPackageByDelta(-1);
});
byId('vipNextButton').addEventListener('click', function () {
selectPackageByDelta(1);
});
byId('activateButton').addEventListener('click', purchase);
document
.querySelector('.page')
.addEventListener('touchstart', handleSwipeStart, {
passive: true,
});
document
.querySelector('.page')
.addEventListener('touchend', handleSwipeEnd, {
passive: true,
});
document
.querySelector('.page')
.addEventListener('touchcancel', function () {
swipeState = null;
});
byId('effectPreviewOverlay').addEventListener(
'click',
function (event) {
if (event.target === event.currentTarget) {
closeEffectPreview();
}
}
);
byId('effectPreviewClose').addEventListener(
'click',
function (event) {
event.stopPropagation();
closeEffectPreview();
}
);
byId('effectPreviewOverlay').addEventListener(
'keydown',
function (event) {
if (
event.key === 'Enter' ||
event.key === ' ' ||
event.key === 'Escape'
) {
closeEffectPreview();
}
}
);
Array.prototype.forEach.call(
document.querySelectorAll('.decor-card'),
function (card) {
card.addEventListener('click', function () {
if (suppressPreviewClick) {
suppressPreviewClick = false;
return;
}
openEffectPreview(card);
});
}
);
resizeApp();
render();
window.HyAppParams.parse();
window.HyAppBridge.ready({ page: 'vip' });
hydrate();
})();
</script>
</body>
</html>