995 lines
28 KiB
HTML
995 lines
28 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, viewport-fit=cover" />
|
|
<title>Room Reward Event</title>
|
|
<style>
|
|
@font-face {
|
|
font-family: "BalooDa2";
|
|
src: url("../../assets/BalooDa2-Bold-ChYvdM8z-1776148661447.ttf") format("truetype");
|
|
font-weight: 700;
|
|
font-style: normal;
|
|
font-display: swap;
|
|
}
|
|
|
|
:root {
|
|
--page-width: 430px;
|
|
--brown-980: #170600;
|
|
--brown-920: #240900;
|
|
--brown-850: #321000;
|
|
--brown-780: #431802;
|
|
--gold-100: #fff4cf;
|
|
--gold-200: #ffe3a2;
|
|
--gold-400: #f8ba55;
|
|
--gold-650: #a84e16;
|
|
--line: rgba(255, 230, 173, 0.9);
|
|
--hero-image: url("../../assets/room-turnover-reward/hero.png");
|
|
--coin-image: url("../../assets/coin-lz3Tp0nX-177614866144.png");
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
html,
|
|
body {
|
|
margin: 0;
|
|
min-height: 100%;
|
|
background: #0b0300;
|
|
font-family:
|
|
-apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Arial,
|
|
sans-serif;
|
|
}
|
|
|
|
body {
|
|
color: #fff7df;
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
}
|
|
|
|
.page {
|
|
position: relative;
|
|
width: min(100%, var(--page-width));
|
|
min-height: 100vh;
|
|
margin: 0 auto;
|
|
overflow: hidden;
|
|
background:
|
|
radial-gradient(circle at 50% 0, rgba(249, 164, 53, 0.2) 0, rgba(249, 164, 53, 0) 36%),
|
|
linear-gradient(180deg, #2b0d02 0%, #1b0600 42%, #150500 100%);
|
|
padding-bottom: calc(142px + env(safe-area-inset-bottom));
|
|
}
|
|
|
|
.hero {
|
|
position: relative;
|
|
padding-top: env(safe-area-inset-top);
|
|
background: #3c1707;
|
|
}
|
|
|
|
.hero-visual {
|
|
position: relative;
|
|
aspect-ratio: 552 / 390;
|
|
background: center top / cover no-repeat var(--hero-image);
|
|
}
|
|
|
|
.hero-visual::after {
|
|
content: "";
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: -1px;
|
|
height: 38px;
|
|
background: linear-gradient(180deg, rgba(28, 7, 0, 0) 0%, #230800 82%);
|
|
}
|
|
|
|
.content {
|
|
position: relative;
|
|
z-index: 1;
|
|
margin-top: -6px;
|
|
padding: 0 14px;
|
|
}
|
|
|
|
.reward-meter {
|
|
position: relative;
|
|
min-height: 118px;
|
|
margin: 0 -2px 24px;
|
|
display: grid;
|
|
place-items: center;
|
|
border: 3px solid transparent;
|
|
border-image: linear-gradient(90deg, #ffe7b5, #c96a25 18%, #fff0bb 50%, #c96a25 82%, #ffe7b5) 1;
|
|
background:
|
|
linear-gradient(180deg, rgba(74, 24, 2, 0.58), rgba(35, 9, 0, 0.96)) padding-box,
|
|
linear-gradient(180deg, #fff7d2, #d8792e 24%, #fff0bc 50%, #9b3e0d 78%, #ffe7b3) border-box;
|
|
box-shadow:
|
|
0 8px 20px rgba(0, 0, 0, 0.34),
|
|
inset 0 0 0 1px rgba(80, 20, 3, 0.95),
|
|
inset 0 0 32px rgba(247, 137, 40, 0.14);
|
|
}
|
|
|
|
.reward-meter::before,
|
|
.reward-meter::after {
|
|
content: "";
|
|
position: absolute;
|
|
left: 50%;
|
|
width: 78px;
|
|
height: 38px;
|
|
transform: translateX(-50%);
|
|
background:
|
|
radial-gradient(circle at 50% 48%, #f75b7a 0 12px, #ffca71 13px 17px, rgba(255, 202, 113, 0) 18px),
|
|
linear-gradient(90deg, rgba(255, 223, 156, 0), #ffe3a5 25%, #b84816 50%, #ffe3a5 75%, rgba(255, 223, 156, 0));
|
|
clip-path: polygon(50% 0, 100% 48%, 50% 100%, 0 48%);
|
|
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.42));
|
|
}
|
|
|
|
.reward-meter::before {
|
|
top: -20px;
|
|
}
|
|
|
|
.reward-meter::after {
|
|
bottom: -20px;
|
|
}
|
|
|
|
.corner {
|
|
position: absolute;
|
|
width: 56px;
|
|
height: 56px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.corner::before,
|
|
.corner::after {
|
|
content: "";
|
|
position: absolute;
|
|
border-radius: 999px 999px 4px 999px;
|
|
background: linear-gradient(135deg, #fff6d4 0%, #f1a947 42%, #8d2f0b 100%);
|
|
box-shadow: inset 0 0 0 1px rgba(255, 244, 199, 0.78);
|
|
}
|
|
|
|
.corner::before {
|
|
width: 36px;
|
|
height: 18px;
|
|
left: 6px;
|
|
top: 18px;
|
|
transform: rotate(-26deg);
|
|
}
|
|
|
|
.corner::after {
|
|
width: 18px;
|
|
height: 36px;
|
|
left: 18px;
|
|
top: 6px;
|
|
transform: rotate(26deg);
|
|
}
|
|
|
|
.corner.tl {
|
|
left: -8px;
|
|
top: -9px;
|
|
}
|
|
|
|
.corner.tr {
|
|
right: -8px;
|
|
top: -9px;
|
|
transform: scaleX(-1);
|
|
}
|
|
|
|
.corner.bl {
|
|
left: -8px;
|
|
bottom: -9px;
|
|
transform: scaleY(-1);
|
|
}
|
|
|
|
.corner.br {
|
|
right: -8px;
|
|
bottom: -9px;
|
|
transform: scale(-1);
|
|
}
|
|
|
|
.meter-value {
|
|
position: relative;
|
|
z-index: 1;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 9px;
|
|
min-width: 0;
|
|
max-width: calc(100% - 42px);
|
|
color: var(--gold-100);
|
|
font-family: "BalooDa2", -apple-system, BlinkMacSystemFont, Arial, sans-serif;
|
|
font-size: 42px;
|
|
line-height: 1;
|
|
font-weight: 700;
|
|
text-align: center;
|
|
text-shadow:
|
|
0 2px 0 #7c2f0b,
|
|
0 0 12px rgba(255, 190, 75, 0.38);
|
|
}
|
|
|
|
.coin {
|
|
display: inline-block;
|
|
flex: 0 0 auto;
|
|
width: 25px;
|
|
height: 25px;
|
|
background: center / contain no-repeat var(--coin-image);
|
|
filter: drop-shadow(0 1px 1px rgba(63, 19, 0, 0.54));
|
|
}
|
|
|
|
.rule-table {
|
|
position: relative;
|
|
overflow: hidden;
|
|
border: 3px solid #f3bf82;
|
|
background: #210800;
|
|
box-shadow:
|
|
0 0 0 1px #6d2609,
|
|
0 10px 22px rgba(0, 0, 0, 0.35),
|
|
inset 0 0 0 1px rgba(255, 249, 218, 0.32);
|
|
}
|
|
|
|
.rule-table::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 3px;
|
|
border: 1px solid rgba(255, 242, 203, 0.5);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.table-row {
|
|
position: relative;
|
|
z-index: 1;
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
min-height: 52px;
|
|
}
|
|
|
|
.table-row:nth-child(odd) {
|
|
background: #1b0600;
|
|
}
|
|
|
|
.table-row:nth-child(even) {
|
|
background: #321000;
|
|
}
|
|
|
|
.table-row.header {
|
|
min-height: 58px;
|
|
background: #1c0700;
|
|
color: #fff8ec;
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.table-cell {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-width: 0;
|
|
padding: 8px 10px;
|
|
border-left: 1px solid rgba(255, 245, 216, 0.84);
|
|
color: #fff4df;
|
|
font-size: 15px;
|
|
line-height: 1.15;
|
|
font-weight: 700;
|
|
text-align: center;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.table-cell:first-child {
|
|
border-left: 0;
|
|
}
|
|
|
|
.reward-cell {
|
|
color: #ffdc8e;
|
|
gap: 5px;
|
|
}
|
|
|
|
.reward-cell .coin {
|
|
width: 13px;
|
|
height: 13px;
|
|
}
|
|
|
|
.dock {
|
|
position: fixed;
|
|
left: 50%;
|
|
bottom: 0;
|
|
z-index: 20;
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
gap: 8px;
|
|
align-items: center;
|
|
width: min(100%, var(--page-width));
|
|
min-height: 76px;
|
|
padding: 10px 14px calc(10px + env(safe-area-inset-bottom));
|
|
transform: translateX(-50%);
|
|
border-top: 1px solid rgba(255, 222, 160, 0.18);
|
|
background: rgba(20, 5, 0, 0.98);
|
|
box-shadow: 0 -8px 18px rgba(0, 0, 0, 0.42);
|
|
}
|
|
|
|
.profile {
|
|
display: grid;
|
|
grid-template-columns: 54px minmax(0, 1fr);
|
|
gap: 9px;
|
|
align-items: center;
|
|
min-width: 0;
|
|
}
|
|
|
|
.avatar {
|
|
width: 54px;
|
|
height: 54px;
|
|
overflow: hidden;
|
|
border: 2px solid #ffd995;
|
|
border-radius: 999px;
|
|
background: #3b1708;
|
|
box-shadow: 0 0 0 1px rgba(129, 54, 18, 0.9);
|
|
}
|
|
|
|
.avatar img {
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.profile-copy {
|
|
min-width: 0;
|
|
}
|
|
|
|
.name {
|
|
overflow: hidden;
|
|
color: #fff3df;
|
|
font-size: 17px;
|
|
font-weight: 800;
|
|
line-height: 1.2;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.id {
|
|
margin-top: 5px;
|
|
color: rgba(255, 239, 205, 0.78);
|
|
font-size: 11px;
|
|
line-height: 1.1;
|
|
}
|
|
|
|
.stats {
|
|
display: grid;
|
|
gap: 8px;
|
|
min-width: 160px;
|
|
}
|
|
|
|
.stat-row {
|
|
display: grid;
|
|
grid-template-columns: max-content 74px;
|
|
gap: 7px;
|
|
align-items: center;
|
|
justify-content: end;
|
|
color: #fff6e9;
|
|
font-size: 12px;
|
|
line-height: 1.1;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.stat-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 4px;
|
|
min-width: 74px;
|
|
height: 27px;
|
|
padding: 0 10px;
|
|
border: 1px solid #c37019;
|
|
border-radius: 999px;
|
|
background: linear-gradient(180deg, #fff1bf 0%, #ffd685 44%, #ffe7ae 100%);
|
|
color: #c45a13;
|
|
font-family: "BalooDa2", -apple-system, BlinkMacSystemFont, Arial, sans-serif;
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.68),
|
|
0 1px 0 rgba(64, 15, 0, 0.52);
|
|
}
|
|
|
|
.stat-pill .coin {
|
|
width: 12px;
|
|
height: 12px;
|
|
}
|
|
|
|
.claim-button {
|
|
grid-column: 1 / -1;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
width: 100%;
|
|
min-height: 38px;
|
|
padding: 0 16px;
|
|
border: 1px solid #c37019;
|
|
border-radius: 999px;
|
|
appearance: none;
|
|
background: linear-gradient(180deg, #fff0bd 0%, #ffc35b 48%, #f3932c 100%);
|
|
color: #7d2b05;
|
|
font-size: 15px;
|
|
font-weight: 900;
|
|
line-height: 1.1;
|
|
text-align: center;
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.78),
|
|
0 2px 0 rgba(104, 36, 2, 0.86),
|
|
0 5px 12px rgba(0, 0, 0, 0.28);
|
|
}
|
|
|
|
.claim-button:not(:disabled):active {
|
|
transform: translateY(1px);
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.78),
|
|
0 1px 0 rgba(104, 36, 2, 0.86),
|
|
0 3px 9px rgba(0, 0, 0, 0.26);
|
|
}
|
|
|
|
.claim-button:disabled {
|
|
background: linear-gradient(180deg, #6a3a18 0%, #47200b 100%);
|
|
color: rgba(255, 233, 196, 0.58);
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.08),
|
|
0 1px 0 rgba(0, 0, 0, 0.36);
|
|
}
|
|
|
|
.claim-button .coin {
|
|
width: 15px;
|
|
height: 15px;
|
|
}
|
|
|
|
.claim-button .coin[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
.toast {
|
|
position: fixed;
|
|
left: 50%;
|
|
bottom: calc(132px + env(safe-area-inset-bottom));
|
|
z-index: 30;
|
|
width: min(calc(100% - 44px), 330px);
|
|
min-height: 38px;
|
|
padding: 10px 14px;
|
|
transform: translateX(-50%);
|
|
border: 1px solid rgba(255, 221, 152, 0.44);
|
|
border-radius: 999px;
|
|
background: rgba(23, 6, 0, 0.94);
|
|
color: #fff4df;
|
|
font-size: 13px;
|
|
line-height: 1.2;
|
|
font-weight: 700;
|
|
text-align: center;
|
|
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.38);
|
|
}
|
|
|
|
@media (max-width: 360px) {
|
|
.content {
|
|
padding-left: 10px;
|
|
padding-right: 10px;
|
|
}
|
|
|
|
.meter-value {
|
|
font-size: 37px;
|
|
}
|
|
|
|
.dock {
|
|
grid-template-columns: 1fr;
|
|
gap: 8px;
|
|
padding-left: 12px;
|
|
padding-right: 12px;
|
|
}
|
|
|
|
.stats {
|
|
min-width: 0;
|
|
}
|
|
|
|
.stat-row {
|
|
grid-template-columns: minmax(0, 1fr) 74px;
|
|
justify-content: stretch;
|
|
}
|
|
|
|
.stat-row span:first-child {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.page {
|
|
padding-bottom: calc(208px + env(safe-area-inset-bottom));
|
|
}
|
|
|
|
.toast {
|
|
bottom: calc(194px + env(safe-area-inset-bottom));
|
|
}
|
|
}
|
|
</style>
|
|
<script>
|
|
window.__H5_ROUTE_PATH__ = "/activity/room-turnover-reward";
|
|
</script>
|
|
<script src="../../assets/yumi-route-entry.js"></script>
|
|
</head>
|
|
|
|
<body>
|
|
<main class="page">
|
|
<section class="hero" aria-label="Room Reward Event">
|
|
<div class="hero-visual"></div>
|
|
</section>
|
|
|
|
<section class="content">
|
|
<div class="reward-meter" aria-label="Current reward">
|
|
<span class="corner tl"></span>
|
|
<span class="corner tr"></span>
|
|
<span class="corner bl"></span>
|
|
<span class="corner br"></span>
|
|
<div class="meter-value">
|
|
<span id="highlightReward">1000</span>
|
|
<span class="coin" aria-hidden="true"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="rule-table" aria-label="Room turnover reward levels">
|
|
<div class="table-row header">
|
|
<div class="table-cell">Room turnover</div>
|
|
<div class="table-cell">Receive a reward</div>
|
|
</div>
|
|
<div id="levelRows"></div>
|
|
</div>
|
|
</section>
|
|
|
|
<aside class="dock" aria-label="Room reward profile">
|
|
<div class="profile">
|
|
<div class="avatar">
|
|
<img id="profileAvatar" src="../../assets/defaultAvatar-CdxWBK1k.png" alt="Room avatar" />
|
|
</div>
|
|
<div class="profile-copy">
|
|
<div class="name" id="profileName">Namename...</div>
|
|
<div class="id">ID: <span id="profileId">123456789</span></div>
|
|
</div>
|
|
</div>
|
|
<div class="stats">
|
|
<div class="stat-row">
|
|
<span>This week's contribution:</span>
|
|
<span class="stat-pill">
|
|
<span id="currentTurnover">100</span>
|
|
<span class="coin" aria-hidden="true"></span>
|
|
</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span>Next week's rewards:</span>
|
|
<span class="stat-pill">
|
|
<span id="currentReward">100</span>
|
|
<span class="coin" aria-hidden="true"></span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<button class="claim-button" id="claimButton" type="button" disabled>
|
|
<span id="claimButtonText">Checking...</span>
|
|
<span class="coin" id="claimButtonCoin" aria-hidden="true" hidden></span>
|
|
</button>
|
|
</aside>
|
|
<div class="toast" id="pageToast" role="status" aria-live="polite" hidden></div>
|
|
</main>
|
|
|
|
<script>
|
|
const DEFAULT_API_BASE = "https://jvapi.haiyihy.com/";
|
|
const DEFAULT_API_PREFIX = "/go";
|
|
const DEFAULT_AVATAR = "../../assets/defaultAvatar-CdxWBK1k.png";
|
|
const query = buildPageQueryParams();
|
|
const resolvedAPIBase = resolveAPIBase();
|
|
const fallbackLevels = Array.from({ length: 7 }, (_, index) => ({
|
|
level: index + 1,
|
|
turnoverThreshold: 7000000,
|
|
rewardGold: 350000,
|
|
enabled: true,
|
|
}));
|
|
|
|
const state = {
|
|
apiBase: resolvedAPIBase,
|
|
apiPrefix: resolveAPIPrefix(resolvedAPIBase),
|
|
token: resolveToken(),
|
|
levels: fallbackLevels,
|
|
highlightReward: numberFromQuery(["highlightRewardGold", "highlightReward", "topRewardGold"], 1000),
|
|
currentTurnover: numberFromQuery(["turnover", "contribution", "amount"], 100),
|
|
currentReward: numberFromQuery(["weekReward", "reward", "currentReward"], 100),
|
|
profileName: textFromQuery(["roomName", "name", "nickname"], "Namename..."),
|
|
profileId: textFromQuery(["roomAccount", "account", "roomId", "id"], "123456789"),
|
|
profileAvatar: textFromQuery(["roomCover", "avatar", "userAvatar"], DEFAULT_AVATAR),
|
|
claimStatus: {
|
|
canClaim: false,
|
|
reason: "loading",
|
|
autoRewardEnabled: false,
|
|
record: null,
|
|
},
|
|
claimLoading: false,
|
|
toastTimer: 0,
|
|
};
|
|
|
|
const els = {
|
|
levelRows: document.getElementById("levelRows"),
|
|
highlightReward: document.getElementById("highlightReward"),
|
|
currentTurnover: document.getElementById("currentTurnover"),
|
|
currentReward: document.getElementById("currentReward"),
|
|
profileName: document.getElementById("profileName"),
|
|
profileId: document.getElementById("profileId"),
|
|
profileAvatar: document.getElementById("profileAvatar"),
|
|
claimButton: document.getElementById("claimButton"),
|
|
claimButtonText: document.getElementById("claimButtonText"),
|
|
claimButtonCoin: document.getElementById("claimButtonCoin"),
|
|
pageToast: document.getElementById("pageToast"),
|
|
};
|
|
|
|
function trimValue(value) {
|
|
return value == null ? "" : String(value).trim();
|
|
}
|
|
|
|
function buildPageQueryParams() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const hash = window.location.hash || "";
|
|
const queryIndex = hash.indexOf("?");
|
|
if (queryIndex >= 0) {
|
|
const hashParams = new URLSearchParams(hash.slice(queryIndex + 1));
|
|
hashParams.forEach((value, key) => {
|
|
if (!params.has(key)) {
|
|
params.append(key, value);
|
|
}
|
|
});
|
|
}
|
|
return params;
|
|
}
|
|
|
|
function textFromQuery(keys, fallback) {
|
|
for (const key of keys) {
|
|
const value = trimValue(query.get(key));
|
|
if (value) {
|
|
return value;
|
|
}
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
function resolveToken() {
|
|
const token = textFromQuery(["token", "accessToken", "authorization"], "");
|
|
if (token.toLowerCase().startsWith("bearer ")) {
|
|
return "Bearer " + token.slice(7).replace(/ /g, "+");
|
|
}
|
|
return token.replace(/ /g, "+");
|
|
}
|
|
|
|
function numberFromQuery(keys, fallback) {
|
|
for (const key of keys) {
|
|
const raw = trimValue(query.get(key));
|
|
if (!raw) {
|
|
continue;
|
|
}
|
|
const number = Number(raw.replace(/,/g, ""));
|
|
if (Number.isFinite(number)) {
|
|
return number;
|
|
}
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
function normalizeBaseURL(base) {
|
|
const value = trimValue(base) || DEFAULT_API_BASE;
|
|
return value.endsWith("/") ? value : value + "/";
|
|
}
|
|
|
|
function resolveAPIBase() {
|
|
const queryBase = trimValue(query.get("apiBase"));
|
|
if (queryBase) {
|
|
return normalizeBaseURL(queryBase);
|
|
}
|
|
if (typeof window.__ROOM_TURNOVER_REWARD_API_BASE__ === "string") {
|
|
return normalizeBaseURL(window.__ROOM_TURNOVER_REWARD_API_BASE__);
|
|
}
|
|
return normalizeBaseURL(DEFAULT_API_BASE);
|
|
}
|
|
|
|
function resolveAPIPrefix(apiBase) {
|
|
const queryPrefix = trimValue(query.get("apiPrefix"));
|
|
if (queryPrefix) {
|
|
return queryPrefix;
|
|
}
|
|
if (typeof window.__ROOM_TURNOVER_REWARD_API_PREFIX__ === "string") {
|
|
return trimValue(window.__ROOM_TURNOVER_REWARD_API_PREFIX__);
|
|
}
|
|
return isOnlineAPIBase(apiBase) ? DEFAULT_API_PREFIX : "";
|
|
}
|
|
|
|
function isOnlineAPIBase(base) {
|
|
try {
|
|
return new URL(base, window.location.href).origin === new URL(DEFAULT_API_BASE).origin;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function joinURLPath(...parts) {
|
|
return "/" + parts
|
|
.map((part) => trimValue(part).replace(/^\/+|\/+$/g, ""))
|
|
.filter(Boolean)
|
|
.join("/");
|
|
}
|
|
|
|
function buildAPIURL(path, searchParams = {}) {
|
|
const apiPath = joinURLPath(state.apiPrefix, path);
|
|
const url = new URL(apiPath.replace(/^\/+/, ""), state.apiBase);
|
|
Object.entries(searchParams).forEach(([key, value]) => {
|
|
const raw = trimValue(value);
|
|
if (raw) {
|
|
url.searchParams.set(key, raw);
|
|
}
|
|
});
|
|
return url;
|
|
}
|
|
|
|
async function requestJSON(path, options = {}) {
|
|
const url = buildAPIURL(path, options.searchParams || {});
|
|
const headers = { Accept: "application/json" };
|
|
if (options.auth && state.token) {
|
|
headers.Authorization = state.token.toLowerCase().startsWith("bearer ")
|
|
? state.token
|
|
: "Bearer " + state.token;
|
|
}
|
|
const fetchOptions = {
|
|
method: options.method || "GET",
|
|
headers,
|
|
};
|
|
if (options.body != null) {
|
|
headers["Content-Type"] = "application/json";
|
|
fetchOptions.body = JSON.stringify(options.body);
|
|
}
|
|
const response = await fetch(url.toString(), fetchOptions);
|
|
const contentType = response.headers.get("content-type") || "";
|
|
if (!contentType.includes("application/json")) {
|
|
if (!response.ok) {
|
|
throw new Error("Request failed: " + response.status);
|
|
}
|
|
return null;
|
|
}
|
|
const json = await response.json();
|
|
if (!response.ok) {
|
|
const message = json && (json.message || json.errorMsg || json.code);
|
|
throw new Error(message || "Request failed: " + response.status);
|
|
}
|
|
return json && (json.body || json.data || json);
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value).replace(/[&<>"']/g, (match) => {
|
|
const entityMap = {
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
'"': """,
|
|
"'": "'",
|
|
};
|
|
return entityMap[match] || match;
|
|
});
|
|
}
|
|
|
|
function formatCompact(value) {
|
|
const number = Number(value || 0);
|
|
if (!Number.isFinite(number)) {
|
|
return "0";
|
|
}
|
|
const absolute = Math.abs(number);
|
|
if (absolute >= 1000000) {
|
|
return trimDecimal(number / 1000000) + "M";
|
|
}
|
|
if (absolute >= 1000) {
|
|
return trimDecimal(number / 1000) + "k";
|
|
}
|
|
return new Intl.NumberFormat("en-US").format(Math.trunc(number));
|
|
}
|
|
|
|
function trimDecimal(value) {
|
|
return Number(value.toFixed(1)).toString();
|
|
}
|
|
|
|
function formatMeterValue(value) {
|
|
const number = Number(value || 0);
|
|
if (!Number.isFinite(number)) {
|
|
return "0";
|
|
}
|
|
return String(Math.trunc(number));
|
|
}
|
|
|
|
function normalizeLevels(levels) {
|
|
const source = Array.isArray(levels) && levels.length ? levels : fallbackLevels;
|
|
return source
|
|
.filter((level) => level && level.turnoverThreshold > 0 && level.rewardGold > 0)
|
|
.sort((a, b) => Number(a.turnoverThreshold) - Number(b.turnoverThreshold));
|
|
}
|
|
|
|
function applyHomeData(body) {
|
|
if (!body || typeof body !== "object") {
|
|
return;
|
|
}
|
|
state.levels = normalizeLevels(body.levelConfigs);
|
|
state.highlightReward = Number(body.expectedRewardGold || body.highlightRewardGold || 0);
|
|
state.currentTurnover = Number(body.turnoverAmount || 0);
|
|
state.currentReward = Number(body.expectedRewardGold || 0);
|
|
state.profileName = trimValue(body.userNickname) || trimValue(body.roomName) || state.profileName;
|
|
state.profileId = trimValue(body.userAccount) || trimValue(body.roomAccount) || trimValue(body.roomId) || state.profileId;
|
|
state.profileAvatar = trimValue(body.userAvatar) || trimValue(body.roomCover) || state.profileAvatar;
|
|
}
|
|
|
|
function applyPublicPageData(body) {
|
|
if (!body || typeof body !== "object") {
|
|
return;
|
|
}
|
|
state.levels = normalizeLevels(body.levelConfigs);
|
|
const hasHighlightOverride =
|
|
query.has("highlightRewardGold") || query.has("highlightReward") || query.has("topRewardGold");
|
|
if (!hasHighlightOverride && body.highlightRewardGold > 0) {
|
|
state.highlightReward = Number(body.highlightRewardGold);
|
|
}
|
|
}
|
|
|
|
function applyClaimStatus(body) {
|
|
const source = body && typeof body === "object" ? body : {};
|
|
const canClaim = Boolean(source.canClaim);
|
|
state.claimStatus = {
|
|
canClaim,
|
|
reason: trimValue(source.reason) || (canClaim ? "available" : "no_claimable_reward"),
|
|
autoRewardEnabled: Boolean(source.autoRewardEnabled),
|
|
record: source.record || null,
|
|
};
|
|
}
|
|
|
|
function claimButtonText() {
|
|
const status = state.claimStatus || {};
|
|
if (state.claimLoading) {
|
|
return "Claiming...";
|
|
}
|
|
if (!state.token) {
|
|
return "Login required";
|
|
}
|
|
if (status.reason === "loading") {
|
|
return "Checking...";
|
|
}
|
|
if (status.canClaim) {
|
|
const rewardGold = Number(status.record && status.record.rewardGold ? status.record.rewardGold : 0);
|
|
return rewardGold > 0 ? "Claim " + formatCompact(rewardGold) : "Claim reward";
|
|
}
|
|
if (status.reason === "claimed") {
|
|
return "Claimed";
|
|
}
|
|
if (status.reason === "auto_reward_enabled") {
|
|
return "Auto issued";
|
|
}
|
|
if (status.reason === "status_unavailable") {
|
|
return "Retry later";
|
|
}
|
|
return "No reward";
|
|
}
|
|
|
|
function renderClaimButton() {
|
|
const status = state.claimStatus || {};
|
|
const rewardGold = Number(status.record && status.record.rewardGold ? status.record.rewardGold : 0);
|
|
const canClaim = Boolean(state.token && status.canClaim && !state.claimLoading);
|
|
els.claimButton.disabled = !canClaim;
|
|
els.claimButton.setAttribute("aria-busy", state.claimLoading ? "true" : "false");
|
|
els.claimButtonText.textContent = claimButtonText();
|
|
els.claimButtonCoin.hidden = !(status.canClaim && rewardGold > 0);
|
|
}
|
|
|
|
function showToast(message) {
|
|
const text = trimValue(message);
|
|
if (!text) {
|
|
return;
|
|
}
|
|
window.clearTimeout(state.toastTimer);
|
|
els.pageToast.textContent = text;
|
|
els.pageToast.hidden = false;
|
|
state.toastTimer = window.setTimeout(() => {
|
|
els.pageToast.hidden = true;
|
|
}, 2200);
|
|
}
|
|
|
|
function render() {
|
|
els.highlightReward.textContent = formatMeterValue(state.highlightReward);
|
|
els.currentTurnover.textContent = formatCompact(state.currentTurnover);
|
|
els.currentReward.textContent = formatCompact(state.currentReward);
|
|
els.profileName.textContent = state.profileName;
|
|
els.profileId.textContent = state.profileId;
|
|
els.profileAvatar.src = state.profileAvatar || DEFAULT_AVATAR;
|
|
els.levelRows.innerHTML = state.levels.map((level) => {
|
|
return `
|
|
<div class="table-row">
|
|
<div class="table-cell">${escapeHtml(formatCompact(level.turnoverThreshold))}</div>
|
|
<div class="table-cell reward-cell">
|
|
<span>${escapeHtml(formatCompact(level.rewardGold))}</span>
|
|
<span class="coin" aria-hidden="true"></span>
|
|
</div>
|
|
</div>`;
|
|
}).join("");
|
|
renderClaimButton();
|
|
}
|
|
|
|
async function fetchPageData() {
|
|
if (state.token) {
|
|
try {
|
|
const home = await requestJSON("/app/h5/room-turnover-reward/home", { auth: true });
|
|
applyHomeData(home);
|
|
} catch (_) {
|
|
} finally {
|
|
render();
|
|
}
|
|
await fetchClaimStatus();
|
|
return;
|
|
}
|
|
try {
|
|
const body = await requestJSON("/public/h5/room-turnover-reward/page", {
|
|
searchParams: { sysOrigin: query.get("sysOrigin") },
|
|
});
|
|
applyPublicPageData(body);
|
|
} catch (_) {
|
|
}
|
|
applyClaimStatus({ canClaim: false, reason: "login_required", autoRewardEnabled: false });
|
|
render();
|
|
}
|
|
|
|
async function fetchClaimStatus() {
|
|
if (!state.token) {
|
|
applyClaimStatus({ canClaim: false, reason: "login_required", autoRewardEnabled: false });
|
|
renderClaimButton();
|
|
return;
|
|
}
|
|
state.claimStatus = {
|
|
canClaim: false,
|
|
reason: "loading",
|
|
autoRewardEnabled: false,
|
|
record: null,
|
|
};
|
|
renderClaimButton();
|
|
try {
|
|
const body = await requestJSON("/app/h5/room-turnover-reward/reward/claim/status", { auth: true });
|
|
applyClaimStatus(body);
|
|
} catch (_) {
|
|
applyClaimStatus({ canClaim: false, reason: "status_unavailable", autoRewardEnabled: false });
|
|
}
|
|
renderClaimButton();
|
|
}
|
|
|
|
async function handleClaimClick() {
|
|
const status = state.claimStatus || {};
|
|
if (state.claimLoading || !state.token || !status.canClaim) {
|
|
return;
|
|
}
|
|
const record = status.record || {};
|
|
const payload = {};
|
|
const recordId = trimValue(record.id);
|
|
if (recordId) {
|
|
payload.recordId = recordId;
|
|
}
|
|
state.claimLoading = true;
|
|
renderClaimButton();
|
|
try {
|
|
const body = await requestJSON("/app/h5/room-turnover-reward/reward/claim", {
|
|
auth: true,
|
|
method: "POST",
|
|
body: payload,
|
|
});
|
|
applyClaimStatus({
|
|
canClaim: false,
|
|
reason: "claimed",
|
|
autoRewardEnabled: false,
|
|
record: body && body.record ? body.record : record,
|
|
});
|
|
showToast(body && body.claimed === false ? "Reward already claimed" : "Reward claimed");
|
|
} catch (error) {
|
|
showToast(error && error.message ? error.message : "Claim failed");
|
|
} finally {
|
|
state.claimLoading = false;
|
|
renderClaimButton();
|
|
}
|
|
fetchClaimStatus();
|
|
}
|
|
|
|
els.claimButton.addEventListener("click", handleClaimClick);
|
|
render();
|
|
fetchPageData();
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|