主播中心/ 邀请

This commit is contained in:
170-carry 2026-04-27 23:27:06 +08:00
parent ac2c52dfc2
commit 3629d44829
19 changed files with 4596 additions and 115 deletions

View File

@ -0,0 +1,744 @@
<!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-1776148661447.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(92px + 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;
}
@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;
}
}
</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>This week's rewards:</span>
<span class="stat-pill">
<span id="currentReward">100</span>
<span class="coin" aria-hidden="true"></span>
</span>
</div>
</div>
</aside>
</main>
<script>
const DEFAULT_API_BASE = "https://jvapi.haiyihy.com/";
const DEFAULT_API_PREFIX = "/go";
const DEFAULT_AVATAR = "../../assets/defaultAvatar-CdxWBK1k.png";
const query = new URLSearchParams(window.location.search);
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),
};
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"),
};
function trimValue(value) {
return value == null ? "" : String(value).trim();
}
function textFromQuery(keys, fallback) {
for (const key of keys) {
const value = trimValue(query.get(key));
if (value) {
return value;
}
}
return fallback;
}
function resolveToken() {
return textFromQuery(["token", "accessToken", "authorization"], "");
}
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 response = await fetch(url.toString(), { headers });
if (!response.ok) {
throw new Error("Request failed: " + response.status);
}
const contentType = response.headers.get("content-type") || "";
if (!contentType.includes("application/json")) {
return null;
}
const json = await response.json();
return json && (json.body || json.data || json);
}
function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, (match) => {
const entityMap = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
};
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 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("");
}
async function fetchPageData() {
try {
if (state.token) {
const home = await requestJSON("/app/h5/room-turnover-reward/home", { auth: true });
applyHomeData(home);
render();
return;
}
const body = await requestJSON("/public/h5/room-turnover-reward/page", {
searchParams: { sysOrigin: query.get("sysOrigin") },
});
applyPublicPageData(body);
render();
} catch (_) {
render();
}
}
render();
fetchPageData();
</script>
</body>
</html>

View File

@ -6,6 +6,7 @@ const LOCAL_FILE_PROXY_API_BASE = "http://127.0.0.1:8800/api/";
const ONLINE_INVITE_API_BASE = "https://jvapi.haiyihy.com/";
const ONLINE_INVITE_API_PREFIX = "/go";
const DEFAULT_AVATAR = "./assets/avatar-sample.png";
const INVITEE_DISPLAY_GIFT_REWARD = 1000;
const DEFAULT_RESET_PERIOD_MS = 30 * 86400 * 1000;
const DEFAULT_REQUEST_HEADERS = {
"Req-Sys-Origin": "origin=LIKEI;originChild=LIKEI",
@ -32,7 +33,7 @@ const DEFAULTS = {
downloadUrl: "",
inviterBindReward: 0,
rechargeReward: 0,
giftReward: 0,
giftReward: INVITEE_DISPLAY_GIFT_REWARD,
};
const FALLBACK_TASKS = {
@ -58,7 +59,7 @@ const state = {
downloadUrl: query.get("downloadUrl") || DEFAULTS.downloadUrl,
inviterBindReward: query.get("inviterReward") || DEFAULTS.inviterBindReward,
rechargeReward: query.get("rechargeReward") || DEFAULTS.rechargeReward,
giftReward: query.get("giftReward") || DEFAULTS.giftReward,
giftReward: INVITEE_DISPLAY_GIFT_REWARD,
taskSet: normalizeTaskSet(query.get("taskSet") || "quota"),
tasks: {
transport: cloneTasks(FALLBACK_TASKS.transport),
@ -979,7 +980,6 @@ function applyLandingData(body) {
state.friendName = trimValue(body.inviterNickname) || state.friendName;
state.inviterAvatar = trimValue(body.inviterAvatar) || DEFAULT_AVATAR;
state.downloadUrl = trimValue(body.downloadUrl) || state.downloadUrl;
state.giftReward = body.inviteeReward?.goldAmount ?? state.giftReward;
const inviteCode = trimValue(body.inviteCode);
if (inviteCode) {

View File

@ -6,6 +6,7 @@ const LOCAL_INVITE_API_BASE = "http://127.0.0.1:2900/";
const ONLINE_INVITE_API_BASE = "https://jvapi.haiyihy.com/";
const ONLINE_INVITE_API_PREFIX = "/go";
const DEFAULT_AVATAR = "./assets/avatar-sample.png";
const INVITEE_DISPLAY_GIFT_REWARD = 1000;
const state = {
apiBase: resolveAPIBase(),
@ -13,7 +14,7 @@ const state = {
inviteCode: resolveInviteCode(),
friendName: trimValue(query.get("friendName")) || "Name",
inviterAvatar: trimValue(query.get("inviterAvatar")) || DEFAULT_AVATAR,
giftReward: Number(query.get("giftReward") || 1000),
giftReward: INVITEE_DISPLAY_GIFT_REWARD,
downloadUrl: trimValue(query.get("downloadUrl")),
};
@ -202,7 +203,6 @@ function applyLandingData(body) {
state.friendName = trimValue(body.inviterNickname) || state.friendName;
state.inviterAvatar = trimValue(body.inviterAvatar) || DEFAULT_AVATAR;
state.downloadUrl = trimValue(body.downloadUrl) || state.downloadUrl;
state.giftReward = body.inviteeReward?.goldAmount ?? state.giftReward;
const inviteCode = trimValue(body.inviteCode);
if (inviteCode) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

View File

@ -0,0 +1,111 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Host Center</title>
<link rel="stylesheet" href="./host-center.css" />
</head>
<body>
<div class="host-center">
<header class="page-header">
<button class="header-icon" type="button" data-action="back" aria-label="Back"><img id="backIcon" alt="" /></button>
<h1 data-i18n="host_center">Host Center</h1>
<button class="lang-button" type="button" data-action="language">EN</button>
</header>
<main class="page-content">
<section class="section-title" data-i18n="my_agency">My agency</section>
<section class="surface-card agency-card">
<div class="avatar">
<img id="agencyAvatar" src="" alt="" />
<span id="avatarFallback">U</span>
</div>
<div class="user-info">
<div id="agencyName" class="agency-name">-</div>
<div class="agency-id"><span data-i18n="user_id_prefix">ID:</span> <span id="agencyId">-</span></div>
</div>
<button class="leave-button" type="button" data-action="leave" data-i18n="leave">Leave</button>
</section>
<section class="surface-card salary-card">
<div class="card-row">
<div class="card-title" data-i18n="my_available_salary">My available salary</div>
<button class="text-link" type="button" data-route="/history-salary" data-i18n="my_salary_details">My salary details</button>
</div>
<div class="salary-value"><img id="moneyIcon" class="money-icon" alt="" /><span id="currentSalary">0</span></div>
</section>
<section class="surface-card task-card">
<div class="task-title" data-i18n="todays_task">Today's task</div>
<div class="task-grid">
<div class="task-item">
<div class="task-label" data-i18n="anchor_type">Anchor type</div>
<div class="task-value" id="anchorType">Chat</div>
</div>
<div class="task-item">
<div class="task-label" data-i18n="minutes">Minutes</div>
<div class="task-value"><span id="minutesProgress">0</span>/<span id="minutesTotal">120</span></div>
</div>
<div class="task-item">
<div class="task-label" data-i18n="gift_task">Gift Task</div>
<div class="task-value" id="giftTask">0</div>
</div>
</div>
</section>
<section class="surface-card menu-card">
<button class="menu-row" type="button" data-route="/platform-policy?from=hostcenter">
<span class="menu-main"><img class="menu-icon" id="policyIcon" alt="" /><span data-i18n="platform_policy">Platform policy</span></span>
<img class="arrow-icon" id="arrowIcon1" alt="" />
</button>
<div class="divider"></div>
<button class="menu-row" type="button" data-route="/exchange-gold-coins">
<span class="menu-main"><img class="menu-icon" id="exchangeIcon" alt="" /><span data-i18n="exchange_coins">Exchange coins</span></span>
<img class="arrow-icon" id="arrowIcon2" alt="" />
</button>
<div class="divider"></div>
<button class="menu-row" type="button" data-route="/transfer">
<span class="menu-main"><img class="menu-icon" id="transferIcon" alt="" /><span data-i18n="transfer">Transfer</span></span>
<img class="arrow-icon" id="arrowIcon3" alt="" />
</button>
<div class="divider" id="cashDivider" hidden></div>
<button class="menu-row" type="button" data-action="cash-withdraw" id="cashWithdrawRow" hidden>
<span class="menu-main"><img class="menu-icon" id="withdrawIcon" alt="" /><span data-i18n="cash_withdraw">Cash withdraw</span></span>
<img class="arrow-icon" id="arrowIcon4" alt="" />
</button>
<div class="divider"></div>
<button class="menu-row" type="button" data-route="/host-setting">
<span class="menu-main"><img class="menu-icon" id="bankIcon" alt="" /><span data-i18n="bank_accounts">Bank accounts</span></span>
<img class="arrow-icon" id="arrowIcon5" alt="" />
</button>
</section>
</main>
<div class="toast" id="toast" hidden></div>
<div class="modal-layer" id="infoMissingModal" hidden>
<div class="modal-card">
<div class="modal-title" data-i18n="prompt">Prompt</div>
<div class="modal-message" data-i18n="fill_info_tips">Please complete your information first.</div>
<div class="modal-submessage" data-i18n="card_issue_tips">Please make sure your bank card and KYC information are available.</div>
<button class="modal-primary" type="button" data-action="improve" data-i18n="to_improve">To improve</button>
</div>
</div>
<div class="modal-layer" id="leaveModal" hidden>
<div class="modal-card">
<div class="modal-title" data-i18n="tips">Tips</div>
<div class="modal-message" data-i18n="leave_agent_tips">Are you sure you want to leave?</div>
<div class="modal-actions">
<button class="modal-secondary" type="button" data-action="cancel-leave" data-i18n="cancel">Cancel</button>
<button class="modal-primary" type="button" data-action="confirm-leave" data-i18n="confirm">Confirm</button>
</div>
</div>
</div>
</div>
<script type="module" src="./host-center.js"></script>
</body>
</html>

View File

@ -1,111 +1,14 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Host Center</title>
<link rel="stylesheet" href="./host-center.css" />
</head>
<body>
<div class="host-center">
<header class="page-header">
<button class="header-icon" type="button" data-action="back" aria-label="Back"><img id="backIcon" alt="" /></button>
<h1 data-i18n="host_center">Host Center</h1>
<button class="lang-button" type="button" data-action="language">EN</button>
</header>
<main class="page-content">
<section class="section-title" data-i18n="my_agency">My agency</section>
<section class="surface-card agency-card">
<div class="avatar">
<img id="agencyAvatar" src="" alt="" />
<span id="avatarFallback">U</span>
</div>
<div class="user-info">
<div id="agencyName" class="agency-name">-</div>
<div class="agency-id"><span data-i18n="user_id_prefix">ID:</span> <span id="agencyId">-</span></div>
</div>
<button class="leave-button" type="button" data-action="leave" data-i18n="leave">Leave</button>
</section>
<section class="surface-card salary-card">
<div class="card-row">
<div class="card-title" data-i18n="my_available_salary">My available salary</div>
<button class="text-link" type="button" data-route="/history-salary" data-i18n="my_salary_details">My salary details</button>
</div>
<div class="salary-value"><img id="moneyIcon" class="money-icon" alt="" /><span id="currentSalary">0</span></div>
</section>
<section class="surface-card task-card">
<div class="task-title" data-i18n="todays_task">Today's task</div>
<div class="task-grid">
<div class="task-item">
<div class="task-label" data-i18n="anchor_type">Anchor type</div>
<div class="task-value" id="anchorType">Chat</div>
</div>
<div class="task-item">
<div class="task-label" data-i18n="minutes">Minutes</div>
<div class="task-value"><span id="minutesProgress">0</span>/<span id="minutesTotal">120</span></div>
</div>
<div class="task-item">
<div class="task-label" data-i18n="gift_task">Gift Task</div>
<div class="task-value" id="giftTask">0</div>
</div>
</div>
</section>
<section class="surface-card menu-card">
<button class="menu-row" type="button" data-route="/platform-policy?from=hostcenter">
<span class="menu-main"><img class="menu-icon" id="policyIcon" alt="" /><span data-i18n="platform_policy">Platform policy</span></span>
<img class="arrow-icon" id="arrowIcon1" alt="" />
</button>
<div class="divider"></div>
<button class="menu-row" type="button" data-route="/exchange-gold-coins">
<span class="menu-main"><img class="menu-icon" id="exchangeIcon" alt="" /><span data-i18n="exchange_coins">Exchange coins</span></span>
<img class="arrow-icon" id="arrowIcon2" alt="" />
</button>
<div class="divider"></div>
<button class="menu-row" type="button" data-route="/transfer">
<span class="menu-main"><img class="menu-icon" id="transferIcon" alt="" /><span data-i18n="transfer">Transfer</span></span>
<img class="arrow-icon" id="arrowIcon3" alt="" />
</button>
<div class="divider" id="cashDivider" hidden></div>
<button class="menu-row" type="button" data-action="cash-withdraw" id="cashWithdrawRow" hidden>
<span class="menu-main"><img class="menu-icon" id="withdrawIcon" alt="" /><span data-i18n="cash_withdraw">Cash withdraw</span></span>
<img class="arrow-icon" id="arrowIcon4" alt="" />
</button>
<div class="divider"></div>
<button class="menu-row" type="button" data-route="/host-setting">
<span class="menu-main"><img class="menu-icon" id="bankIcon" alt="" /><span data-i18n="bank_accounts">Bank accounts</span></span>
<img class="arrow-icon" id="arrowIcon5" alt="" />
</button>
</section>
</main>
<div class="toast" id="toast" hidden></div>
<div class="modal-layer" id="infoMissingModal" hidden>
<div class="modal-card">
<div class="modal-title" data-i18n="prompt">Prompt</div>
<div class="modal-message" data-i18n="fill_info_tips">Please complete your information first.</div>
<div class="modal-submessage" data-i18n="card_issue_tips">Please make sure your bank card and KYC information are available.</div>
<button class="modal-primary" type="button" data-action="improve" data-i18n="to_improve">To improve</button>
</div>
</div>
<div class="modal-layer" id="leaveModal" hidden>
<div class="modal-card">
<div class="modal-title" data-i18n="tips">Tips</div>
<div class="modal-message" data-i18n="leave_agent_tips">Are you sure you want to leave?</div>
<div class="modal-actions">
<button class="modal-secondary" type="button" data-action="cancel-leave" data-i18n="cancel">Cancel</button>
<button class="modal-primary" type="button" data-action="confirm-leave" data-i18n="confirm">Confirm</button>
</div>
</div>
</div>
</div>
<script type="module" src="./host-center.js"></script>
</body>
</html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var search = window.location.search
window.location = "https://h5.haiyihy.com/hyapp/host-center/index.html" + search
</script>
</body>
</html>

View File

@ -0,0 +1,125 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Stream Center</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div class="host-center" aria-label="Stream Center" data-i18n-aria="page_label">
<div class="hero-bg" aria-hidden="true"></div>
<header class="status-bar" aria-hidden="true">
<span class="status-time">9:41</span>
<span class="status-icons">
<span class="cellular"><i></i><i></i><i></i><i></i></span>
<span class="wifi"><i></i><i></i><i></i></span>
<span class="battery"><i></i></span>
</span>
</header>
<nav class="title-bar">
<button class="back-button" type="button" aria-label="Back" data-i18n-aria="back">
<svg viewBox="0 0 32 32" aria-hidden="true">
<path d="M20 8 12 16l8 8" />
</svg>
</button>
<h1 data-i18n="title">Stream Center</h1>
<div class="language-switcher">
<button class="language-button" type="button" aria-label="Change language" data-i18n-aria="language_button_aria" aria-expanded="false">EN</button>
<div class="language-menu" hidden>
<button type="button" data-lang="en">EN</button>
<button type="button" data-lang="ar">AR</button>
<button type="button" data-lang="tr">TR</button>
<button type="button" data-lang="id">ID</button>
</div>
</div>
</nav>
<main class="content">
<section class="card profile-card">
<div class="avatar-shell">
<img class="avatar-image" id="profileAvatar" alt="" hidden />
<svg class="avatar-fallback" id="profileAvatarFallback" viewBox="0 0 72 72" role="img" aria-label="Oscar avatar">
<defs>
<linearGradient id="avatarBg" x1="12" y1="8" x2="60" y2="64">
<stop stop-color="#5AF6E4" />
<stop offset="1" stop-color="#D9FFF5" />
</linearGradient>
</defs>
<circle cx="36" cy="36" r="35" fill="url(#avatarBg)" />
<path d="M22 30 20 18l10 7M50 30l2-12-10 7" fill="#101820" />
<path d="M20 31c3-9 28-9 32 0 4 10-3 21-16 21S16 41 20 31Z" fill="#F7FBF7" />
<circle cx="29" cy="36" r="4" fill="#101820" />
<circle cx="46" cy="36" r="4" fill="#101820" />
<circle cx="25" cy="41" r="5" fill="#F0B64A" opacity=".9" />
<path d="M35 42h3l-1.5 2Z" fill="#101820" />
<path d="M36 45c-2 2-5 3-8 2M38 45c2 2 5 3 8 2" fill="none" stroke="#101820" stroke-width="2" stroke-linecap="round" />
</svg>
</div>
<div class="profile-copy">
<div class="name" id="profileName">Oscar</div>
<div class="meta" id="profileUid" data-i18n="uid">UID: 1234567890</div>
<button class="agency" type="button">
<span data-i18n="agency">Agency: Bilibili(101013)</span>
<span class="chevron">&rsaquo;</span>
</button>
</div>
</section>
<section class="card metrics-card">
<div class="metric">
<div class="label" data-i18n-html="streaming_duration">Streaming Duration<br />(Minutes)</div>
<div class="value" id="streamingDuration">0</div>
</div>
<div class="vertical-line" aria-hidden="true"></div>
<div class="metric gift-metric">
<div class="label" data-i18n="target">Target</div>
<button class="gift-value" type="button" aria-live="polite">
<span class="amount-text" id="nextTargetValue">-</span>
<span class="chevron">&rsaquo;</span>
</button>
</div>
</section>
<section class="card income-card">
<div class="income-head">
<div class="income-title">
<span data-i18n="my_income">My Income</span>
<svg viewBox="0 0 20 20" aria-hidden="true">
<circle cx="10" cy="10" r="8" />
<path d="M10 9v5M10 6.3v.2" />
</svg>
</div>
<a class="cash-link" href="../withdraw-exchange/index.html" aria-label="Open withdraw and exchange page" data-i18n-aria="open_withdraw_exchange">
<span data-i18n="withdraw_exchange">Withdraw/Exchange</span>
<span class="chevron">&rsaquo;</span>
</a>
</div>
<a class="income-value" href="../withdraw-exchange/index.html" aria-label="Open withdraw and exchange page" data-i18n-aria="open_withdraw_exchange">
<span class="income-currency" aria-hidden="true">$</span>
<span class="amount-text" id="currentSalary">0.00</span>
<span class="chevron">&rsaquo;</span>
</a>
</section>
<section class="card policy-entry-card">
<a class="policy-entry-link" id="anchorPolicyLink" href="./policy.html" aria-label="Open anchor policy" data-i18n-aria="open_anchor_policy">
<span class="policy-entry-left">
<svg viewBox="0 0 28 28" aria-hidden="true">
<path d="M8 5h10l3 3v15H8Z" />
<path d="M18 5v5h4M11 14h7M11 18h6" />
</svg>
<span data-i18n="anchor_policy">Anchor Policy</span>
</span>
<span class="chevron">&rsaquo;</span>
</a>
</section>
</main>
<div class="home-indicator" aria-hidden="true"></div>
</div>
<script src="./script.js" defer></script>
</body>
</html>

View File

@ -0,0 +1,27 @@
{
"document_title": "مركز البث",
"page_label": "مركز البث",
"back": "رجوع",
"title": "مركز البث",
"language_button_aria": "تغيير اللغة",
"uid": "معرّف المستخدم: 1234567890",
"agency": "الوكالة: Bilibili(101013)",
"streaming_duration": "مدة البث<br />(بالدقائق)",
"target": "الهدف",
"my_income": "دخلي",
"withdraw_exchange": "سحب/استبدال",
"open_withdraw_exchange": "فتح صفحة السحب والاستبدال",
"open_anchor_policy": "فتح سياسة المضيف",
"anchor_policy": "سياسة المضيف",
"policy_loading": "جار تحميل السياسة...",
"policy_empty": "لا توجد بيانات سياسة",
"policy_missing_token": "رمز الدخول مفقود",
"policy_load_failed": "فشل تحميل السياسة",
"policy_level": "المستوى",
"policy_online": "الاتصال (ساعة)",
"policy_effective_day": "الأيام الصالحة",
"policy_target": "الهدف",
"policy_host_salary": "راتب المضيف",
"policy_agent_salary": "راتب الوكيل",
"policy_total_salary": "إجمالي الراتب"
}

View File

@ -0,0 +1,27 @@
{
"document_title": "Stream Center",
"page_label": "Stream Center",
"back": "Back",
"title": "Stream Center",
"language_button_aria": "Change language",
"uid": "UID: 1234567890",
"agency": "Agency: Bilibili(101013)",
"streaming_duration": "Streaming Duration<br />(Minutes)",
"target": "Target",
"my_income": "My Income",
"withdraw_exchange": "Withdraw/Exchange",
"open_withdraw_exchange": "Open withdraw and exchange page",
"open_anchor_policy": "Open anchor policy",
"anchor_policy": "Anchor Policy",
"policy_loading": "Loading policy...",
"policy_empty": "No policy data",
"policy_missing_token": "Missing token",
"policy_load_failed": "Failed to load policy",
"policy_level": "Level",
"policy_online": "Online (Hour)",
"policy_effective_day": "Valid days",
"policy_target": "Target",
"policy_host_salary": "Host salary",
"policy_agent_salary": "Agent salary",
"policy_total_salary": "Total salary"
}

View File

@ -0,0 +1,27 @@
{
"document_title": "Pusat Streaming",
"page_label": "Pusat Streaming",
"back": "Kembali",
"title": "Pusat Streaming",
"language_button_aria": "Ubah bahasa",
"uid": "UID: 1234567890",
"agency": "Agensi: Bilibili(101013)",
"streaming_duration": "Durasi Streaming<br />(Menit)",
"target": "Target",
"my_income": "Penghasilan Saya",
"withdraw_exchange": "Tarik/Tukar",
"open_withdraw_exchange": "Buka halaman tarik dan tukar",
"open_anchor_policy": "Buka kebijakan host",
"anchor_policy": "Kebijakan Host",
"policy_loading": "Memuat kebijakan...",
"policy_empty": "Tidak ada data kebijakan",
"policy_missing_token": "Token tidak ditemukan",
"policy_load_failed": "Gagal memuat kebijakan",
"policy_level": "Level",
"policy_online": "Online (Jam)",
"policy_effective_day": "Hari valid",
"policy_target": "Target",
"policy_host_salary": "Gaji host",
"policy_agent_salary": "Gaji agensi",
"policy_total_salary": "Total gaji"
}

View File

@ -0,0 +1,27 @@
{
"document_title": "Yayın Merkezi",
"page_label": "Yayın Merkezi",
"back": "Geri",
"title": "Yayın Merkezi",
"language_button_aria": "Dili değiştir",
"uid": "UID: 1234567890",
"agency": "Ajans: Bilibili(101013)",
"streaming_duration": "Yayın Süresi<br />(Dakika)",
"target": "Hedef",
"my_income": "Gelirim",
"withdraw_exchange": "Çek/Değiştir",
"open_withdraw_exchange": "Çekme ve değişim sayfasını aç",
"open_anchor_policy": "Host politikasını aç",
"anchor_policy": "Host Politikası",
"policy_loading": "Politika yükleniyor...",
"policy_empty": "Politika verisi yok",
"policy_missing_token": "Token eksik",
"policy_load_failed": "Politika yüklenemedi",
"policy_level": "Seviye",
"policy_online": "Online (Saat)",
"policy_effective_day": "Geçerli gün",
"policy_target": "Hedef",
"policy_host_salary": "Host maaşı",
"policy_agent_salary": "Ajans maaşı",
"policy_total_salary": "Toplam maaş"
}

View File

@ -0,0 +1,266 @@
# 原 Vue Host Center 逻辑清单
来源:
- `chatapp-h5/h5/js/index-CN7d9oZS-1776148658686.js`
- `chatapp-h5/h5/js/pageConfig-BUpLPO7w-1776148658686.js`
- `chatapp-h5/h5/js/teamBill-BWXvrc1S-1776148658686.js`
- `chatapp-h5/h5/js/index-CIAVq8iD-1776148658686.js`
- 后端参数定义参考 `chatapp3-java/rc-service`
## 页面初始化
- 初始化 i18n读取当前 locale并调用语言切换逻辑。
- 初始化路由对象。
- 初始化 identity store并设置身份为 `HOST`
- 清空 payee store。
- 使用 `usePageConfig("HOST_CENTER", { needsTeamInfo: true, forceRefresh: true, onDataLoaded })`
- 页面 mounted 后并发或顺序触发:
- 设置身份:`identity = "HOST"`
- 获取我的代理资料
- 清空收款人缓存
- 获取银行卡列表
- 获取提现/KYC 信息
- 获取主播工资提现是否可用
## 用户信息获取
- `usePageConfig` 内部优先读取缓存用户信息。
- 非强制刷新时读取本地缓存用户资料。
- 强制刷新时请求 `/team/member/profile`
- 页面展示用字段:
- `id`
- `account`
- `userNickname`
- `name`
- `userAvatar`
- `ownSpecialId.account`
- Host Center 当前代理/团长资料来自 `/team/team-own/profile`
- 展示名称:`userNickname || name`
- 展示 ID`ownSpecialId.account || account`
- 头像加载失败时走 `imageHandler` fallback。
## App 连接与请求头
- 判断是否在 App/WebView 环境。
- 若已连接且缓存有效,复用缓存请求头。
- 若未连接,调用 App bridge 获取 access origin 参数。
- 解析 App 返回参数后设置 HTTP 请求头。
- 关键请求头来源:
- `Authorization`
- `Req-Lang`
- `Req-App-Intel`
- `Req-Imei`
- `Req-Sys-Origin`
- 连接失败时可能跳转 `/not_app`
## 路由与身份守卫
- 请求 `/app/h5/identity` 获取身份。
- 根据身份字段计算 roles
- `freightAgent`
- `agent`
- `bdLeader`
- `bd`
- `anchor`
- `admin`
- 主角色权重:
- `guest`
- `anchor`
- `agent`
- `bd`
- `bdLeader`
- `freightAgent`
- `admin`
- 默认跳转页:
- `admin` -> `/admin-center`
- `freightAgent` -> `/coin-seller`
- `agent` -> `/agency-center`
- `bdLeader` -> `/bd-leader-center`
- `bd` -> `/bd-center`
- `anchor` -> `/host-center`
- `guest` -> `/apply`
- 当前用户没有 anchor 身份时跳到对应默认页。
## 收入/工资逻辑
- 可用工资来自 `usePageConfig``salaryInfo.currentSalary`
- `usePageConfig` 会请求 `/wallet/bank/balance`
- 响应 body 是 number 时展示 `body.toFixed(2)`
- 请求失败时 `currentSalary = 0` 并 toast`Failed to load balance. Please try again.`
- 原 Host Center 还请求 `/team/member-work`,用于获取历史工资和目标数据。
- `/team/member-work` 返回后:
- `historySalary` 写入本地变量
- 读取 `targets[0]`
- 结算状态映射:
- `SETTLED` -> `Completed`
- `UNPAID` -> `In Progress`
- `HANG_UP` -> `Out of account`
- `PAY_OUT` -> `Completed`
- 目标等级取 `target.settlementResult.level || target.level || "-"`
## 今日任务/直播数据逻辑
- `usePageConfig` 会请求 `/team/member/work/statistics-now`
- 请求参数:
- `dataType=DAILY`
- `memberId=<当前用户 id/account>`
- 展示逻辑:
- `anchorType = "Chat"`
- `minutesProgress = min(ownTime + otherTime, minutesTotal)`
- `minutesTotal = 120`
- `giftTask = acceptGiftValue || 0`
## 银行卡与提现信息逻辑
- 请求 `/wallet/bank-card/list` 获取银行卡列表。
- 取 `body.find(item => item.use)` 作为当前使用银行卡。
- 请求 `/wallet/withdraw-info/list` 获取提现/KYC 信息。
- 若 `body.length > 0`
- 取第一条
- `passportFrontUrl` 按 JSON 字符串解析成 `previewUrls`
- 合并到提现信息对象
- 请求 `/wallet/bank/host-salary/withdraw/enabled`
- `body` 转 Boolean 后控制是否展示 `Cash withdraw` 入口。
- 点击提现:
- 如果没有提现信息或没有当前使用银行卡,打开 `infoMissing` 弹窗。
- 否则跳转 `/cash-withdraw`
## 弹窗逻辑
- 弹窗状态:
- `infoMissing`
- `leave`
- `maskLayerShow = infoMissing || leave`
- 打开弹窗前先关闭其它弹窗。
- 点击遮罩关闭弹窗。
- `infoMissing` 弹窗:
- 标题:`prompt`
- 文案:`fill_info_tips`
- 辅助文案:`card_issue_tips`
- 按钮跳转 `/host-setting`
- `leave` 弹窗:
- 标题:`tips`
- 文案:`leave_agent_tips`
- 取消关闭弹窗
- 确认执行退出团队申请
## 退出团队逻辑
- 请求 `/team/user/apply`
- 方法:`POST`
- Body
- `reason: "QUIT"`
- `teamId: <当前团队 id>`
- `teamId` 来源:
- 原 Vue 使用 `pe()` 读取当前团队 id
- 静态实现里曾兼容读取 `teamInfo.id || agency.teamId || agency.id`
- 失败时 toast `errorMsg`
## 点击跳转
- 工资详情:`/history-salary`
- 平台政策:`/platform-policy?from=hostcenter`
- 主播设置/银行卡:`/host-setting`
- 兑换金币:`/exchange-gold-coins`
- 转账:`/transfer`
- 提现:`/cash-withdraw`
- 语言切换:`/language`
- 返回:
- App bridge `closePage`
- Flutter bridge `close_page`
- 浏览器 `history.back()`
## 原接口与可传参数
### GET `/team/member/profile`
- 用途:获取当前用户信息。
- 显式查询参数:无。
- 隐式参数App 请求头中用户、语言、来源等上下文。
### GET `/team/team-own/profile`
- 用途:获取我的团长/代理资料。
- 显式查询参数:无。
- 隐式参数App 请求头中的当前用户上下文。
### GET `/team/entry`
- 用途:获取当前用户团队入口信息。
- 显式查询参数:无。
- 返回中使用 `body.teamProfile || body` 作为团队信息。
### GET `/wallet/bank/balance`
- 用途:获取工资/银行卡余额。
- 显式查询参数:无。
- 返回可为数字,也可能为对象。
### GET `/team/member/work/statistics-now`
- 用途:获取成员当天目标/工作统计。
- 查询参数:
- `dataType`: `DAILY | MONTH`
- `memberId`: 成员 ID必传
- `size`: 数量,可选
- Host Center 使用:
- `dataType=DAILY`
- `memberId=<当前用户 id/account>`
### GET `/team/member-work`
- 用途:成员工资情况。
- 查询参数:
- `userId`: 用户 ID必传
- 返回使用:
- `historySalary`
- `targets`
- `targets[].status`
- `targets[].target.settlementResult.level`
- `targets[].target.level`
### GET `/wallet/bank-card/list`
- 用途:银行卡列表。
- 显式查询参数:无。
- 返回使用:
- `body[]`
- `item.use`
### GET `/wallet/withdraw-info/list`
- 用途:提现/KYC 信息列表。
- 显式查询参数:无。
- 返回使用:
- `body[0]`
- `body[0].passportFrontUrl`
### GET `/wallet/bank/host-salary/withdraw/enabled`
- 用途:主播工资是否允许提现。
- 显式查询参数:无。
- 返回使用:
- `body` boolean
### GET `/app/h5/identity`
- 用途:获取当前 H5 身份。
- 显式查询参数:无。
- 返回使用字段:
- `freightAgent`
- `agent`
- `bdLeader`
- `bd`
- `anchor`
- `admin`
### POST `/team/user/apply`
- 用途:用户申请加入或退出团队。
- Body
- `reason`: `JOIN | QUIT`
- `teamId`: 团队 ID必传
- `url`: 跳转 URL可选
- Host Center 退出团队使用:
- `reason: "QUIT"`
- `teamId: <当前团队 id>`

View File

@ -0,0 +1,55 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Anchor Policy</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div class="host-center policy-page" aria-label="Anchor Policy" data-i18n-aria="anchor_policy">
<div class="hero-bg" aria-hidden="true"></div>
<header class="status-bar" aria-hidden="true">
<span class="status-time">9:41</span>
<span class="status-icons">
<span class="cellular"><i></i><i></i><i></i><i></i></span>
<span class="wifi"><i></i><i></i><i></i></span>
<span class="battery"><i></i></span>
</span>
</header>
<nav class="title-bar">
<button class="back-button" type="button" aria-label="Back" data-i18n-aria="back">
<svg viewBox="0 0 32 32" aria-hidden="true">
<path d="M20 8 12 16l8 8" />
</svg>
</button>
<h1 data-i18n="anchor_policy">Anchor Policy</h1>
<div class="language-switcher">
<button class="language-button" type="button" aria-label="Change language" data-i18n-aria="language_button_aria" aria-expanded="false">EN</button>
<div class="language-menu" hidden>
<button type="button" data-lang="en">EN</button>
<button type="button" data-lang="ar">AR</button>
<button type="button" data-lang="tr">TR</button>
<button type="button" data-lang="id">ID</button>
</div>
</div>
</nav>
<main class="content policy-content">
<section class="card policy-card" aria-labelledby="anchorPolicyTitle">
<div class="policy-head">
<h2 id="anchorPolicyTitle" data-i18n="anchor_policy">Anchor Policy</h2>
<span class="policy-region" id="policyRegion"></span>
</div>
<div class="policy-status" id="policyStatus" data-i18n="policy_loading">Loading policy...</div>
<div class="policy-list" id="policyList" hidden></div>
</section>
</main>
<div class="home-indicator" aria-hidden="true"></div>
</div>
<script src="./policy.js" defer></script>
</body>
</html>

View File

@ -0,0 +1,375 @@
(function () {
const supportedLanguages = ["en", "ar", "tr", "id"];
const languageLabels = {
en: "EN",
ar: "AR",
tr: "TR",
id: "ID"
};
const API_BASE_URL = "https://jvapi.haiyihy.com";
const fallbackMessages = {
en: {
document_title: "Anchor Policy",
back: "Back",
language_button_aria: "Change language",
anchor_policy: "Anchor Policy",
policy_loading: "Loading policy...",
policy_empty: "No policy data",
policy_missing_token: "Missing token",
policy_load_failed: "Failed to load policy",
policy_level: "Level",
policy_online: "Online (Hour)",
policy_effective_day: "Valid days",
policy_target: "Target",
policy_host_salary: "Host salary",
policy_agent_salary: "Agent salary",
policy_total_salary: "Total salary"
},
ar: {
document_title: "سياسة المضيف",
back: "رجوع",
language_button_aria: "تغيير اللغة",
anchor_policy: "سياسة المضيف",
policy_loading: "جار تحميل السياسة...",
policy_empty: "لا توجد بيانات سياسة",
policy_missing_token: "رمز الدخول مفقود",
policy_load_failed: "فشل تحميل السياسة",
policy_level: "المستوى",
policy_online: "الاتصال (ساعة)",
policy_effective_day: "الأيام الصالحة",
policy_target: "الهدف",
policy_host_salary: "راتب المضيف",
policy_agent_salary: "راتب الوكيل",
policy_total_salary: "إجمالي الراتب"
},
tr: {
document_title: "Host Politikası",
back: "Geri",
language_button_aria: "Dili değiştir",
anchor_policy: "Host Politikası",
policy_loading: "Politika yükleniyor...",
policy_empty: "Politika verisi yok",
policy_missing_token: "Token eksik",
policy_load_failed: "Politika yüklenemedi",
policy_level: "Seviye",
policy_online: "Online (Saat)",
policy_effective_day: "Geçerli gün",
policy_target: "Hedef",
policy_host_salary: "Host maaşı",
policy_agent_salary: "Ajans maaşı",
policy_total_salary: "Toplam maaş"
},
id: {
document_title: "Kebijakan Host",
back: "Kembali",
language_button_aria: "Ubah bahasa",
anchor_policy: "Kebijakan Host",
policy_loading: "Memuat kebijakan...",
policy_empty: "Tidak ada data kebijakan",
policy_missing_token: "Token tidak ditemukan",
policy_load_failed: "Gagal memuat kebijakan",
policy_level: "Level",
policy_online: "Online (Jam)",
policy_effective_day: "Hari valid",
policy_target: "Target",
policy_host_salary: "Gaji host",
policy_agent_salary: "Gaji agensi",
policy_total_salary: "Total gaji"
}
};
const params = readURLParams();
let currentLang = normalizeLanguage(params.get("lang"));
let currentMessages = fallbackMessages[currentLang] || fallbackMessages.en;
let currentPolicy = null;
let policyStatusKey = "policy_loading";
function readURLParams() {
const result = new URLSearchParams(window.location.search);
const hashQuery = window.location.hash.includes("?") ? window.location.hash.split("?")[1] : "";
const hashParams = new URLSearchParams(hashQuery);
hashParams.forEach((value, key) => {
if (!result.has(key)) result.set(key, value);
});
return result;
}
function readRawParam(name) {
const queries = [window.location.search.replace(/^\?/, "")];
if (window.location.hash.includes("?")) queries.push(window.location.hash.split("?")[1]);
for (const query of queries) {
for (const part of query.split("&")) {
if (!part) continue;
const equalIndex = part.indexOf("=");
const rawKey = equalIndex >= 0 ? part.slice(0, equalIndex) : part;
const rawValue = equalIndex >= 0 ? part.slice(equalIndex + 1) : "";
let decodedKey = "";
try {
decodedKey = decodeURIComponent(rawKey);
} catch (error) {
continue;
}
if (decodedKey !== name) continue;
try {
return decodeURIComponent(rawValue.replace(/\+/g, "%2B"));
} catch (error) {
return rawValue;
}
}
}
return "";
}
function normalizeLanguage(lang) {
const value = String(lang || "en").toLowerCase();
return supportedLanguages.includes(value) ? value : "en";
}
async function loadMessages(lang) {
const normalizedLang = normalizeLanguage(lang);
if (window.location.protocol === "file:") return fallbackMessages[normalizedLang];
try {
const response = await fetch(`./locales/${normalizedLang}.json`, { cache: "no-store" });
if (response.ok) return await response.json();
} catch (error) {
return fallbackMessages[normalizedLang];
}
return fallbackMessages[normalizedLang];
}
function message(key, fallback) {
return currentMessages[key] || fallbackMessages.en[key] || fallback || key;
}
function renderMessages(messages) {
document.querySelectorAll("[data-i18n]").forEach((node) => {
const value = messages[node.dataset.i18n];
if (value) node.textContent = value;
});
document.querySelectorAll("[data-i18n-aria]").forEach((node) => {
const value = messages[node.dataset.i18nAria];
if (value) node.setAttribute("aria-label", value);
});
}
function apiBaseURL() {
const override = window.HYAPP_API_BASE_URL || params.get("apiBase");
if (override) return String(override).replace(/\/+$/, "");
return API_BASE_URL.replace(/\/+$/, "");
}
function renderPolicyStatus(key, fallback) {
policyStatusKey = key;
const status = document.querySelector("#policyStatus");
const list = document.querySelector("#policyList");
const region = document.querySelector("#policyRegion");
if (status) {
status.textContent = message(key, fallback);
status.hidden = false;
}
if (list) {
list.hidden = true;
list.replaceChildren();
}
if (region) region.textContent = "";
}
function formatPlainValue(value) {
if (value === null || value === undefined || value === "") return "0";
return String(value);
}
function formatTarget(value) {
const number = Number(value);
if (!Number.isFinite(number)) return formatPlainValue(value);
if (number >= 1000000) return `${(number / 1000000).toFixed(1)}M`;
if (number >= 1000) return `${(number / 1000).toFixed(1)}K`;
return String(number);
}
function formatSalary(value, policyType) {
const text = formatPlainValue(value);
return policyType === "SALARY_DIAMOND" ? text : `$${text}`;
}
function createPolicyCell(label, value) {
const cell = document.createElement("div");
cell.className = "policy-cell";
const labelNode = document.createElement("div");
labelNode.className = "policy-cell-label";
labelNode.textContent = label;
const valueNode = document.createElement("div");
valueNode.className = "policy-cell-value";
valueNode.textContent = value;
cell.append(labelNode, valueNode);
return cell;
}
function createPolicyRow(item, policyType) {
const row = document.createElement("article");
row.className = "policy-row";
const head = document.createElement("div");
head.className = "policy-row-head";
const level = document.createElement("div");
level.className = "policy-level";
level.textContent = `${message("policy_level")} ${formatPlainValue(item.level)}`;
const effectiveDay = document.createElement("div");
effectiveDay.className = "policy-effective-day";
effectiveDay.textContent = `${message("policy_effective_day")}: ${formatPlainValue(item.effectiveDay)}`;
head.append(level, effectiveDay);
const grid = document.createElement("div");
grid.className = "policy-grid";
grid.append(
createPolicyCell(message("policy_online"), formatPlainValue(item.onlineTime)),
createPolicyCell(message("policy_target"), formatTarget(item.target)),
createPolicyCell(message("policy_host_salary"), formatSalary(item.memberSalary, policyType)),
createPolicyCell(message("policy_agent_salary"), formatSalary(item.ownSalary, policyType)),
createPolicyCell(message("policy_total_salary"), formatSalary(item.totalSalary, policyType))
);
row.append(head, grid);
return row;
}
function renderAnchorPolicy(policy) {
const rows = Array.isArray(policy?.policy) ? policy.policy : [];
const list = document.querySelector("#policyList");
const status = document.querySelector("#policyStatus");
const region = document.querySelector("#policyRegion");
if (!list || !status) return;
if (region) {
region.textContent = [policy?.regionCode || policy?.region, policy?.countryCode].filter(Boolean).join(" · ");
}
if (!rows.length || policy?.empty) {
renderPolicyStatus("policy_empty");
return;
}
status.hidden = true;
list.hidden = false;
list.replaceChildren(...rows.map((item) => createPolicyRow(item, policy?.policyType)));
}
async function fetchAnchorPolicy() {
renderPolicyStatus("policy_loading");
const token = readRawParam("token");
if (!token) {
renderPolicyStatus("policy_missing_token");
return;
}
try {
const url = new URL("/app/h5/host-center/policy", `${apiBaseURL()}/`);
url.searchParams.set("token", token);
if (params.get("policyType")) url.searchParams.set("policyType", params.get("policyType"));
const response = await fetch(url.toString(), { cache: "no-store" });
const data = await response.json().catch(() => ({}));
if (!response.ok || data.status === false) {
throw new Error(data.errorMsg || data.message || `Request failed: ${response.status}`);
}
currentPolicy = data.body || data.data || {};
renderAnchorPolicy(currentPolicy);
} catch (error) {
console.error("Failed to load anchor policy:", error);
currentPolicy = null;
renderPolicyStatus("policy_load_failed", error.message);
}
}
function syncBackFallback() {
const backButton = document.querySelector(".back-button");
if (!backButton) return;
backButton.addEventListener("click", () => {
if (window.history.length > 1) {
window.history.back();
return;
}
const nextParams = new URLSearchParams(params);
const token = readRawParam("token");
if (token) nextParams.set("token", token);
if (currentLang) nextParams.set("lang", currentLang);
const query = nextParams.toString();
window.location.href = `./index.html${query ? `?${query}` : ""}`;
});
}
function setLanguageMenuOpen(open) {
const languageButton = document.querySelector(".language-button");
const languageMenu = document.querySelector(".language-menu");
if (!languageButton || !languageMenu) return;
languageMenu.hidden = !open;
languageButton.setAttribute("aria-expanded", String(open));
}
function renderLanguageControls(messages) {
const languageButton = document.querySelector(".language-button");
if (languageButton) {
languageButton.textContent = languageLabels[currentLang];
languageButton.setAttribute("aria-label", messages.language_button_aria || "Change language");
}
document.querySelectorAll(".language-menu [data-lang]").forEach((button) => {
const isActive = button.dataset.lang === currentLang;
button.classList.toggle("is-active", isActive);
button.setAttribute("aria-pressed", String(isActive));
});
}
async function applyLanguage(lang) {
currentLang = normalizeLanguage(lang);
const messages = await loadMessages(currentLang);
currentMessages = messages;
document.documentElement.lang = currentLang;
document.documentElement.dir = currentLang === "ar" ? "rtl" : "ltr";
document.title = messages.document_title || messages.anchor_policy || "Anchor Policy";
renderMessages(messages);
renderLanguageControls(messages);
if (currentPolicy) {
renderAnchorPolicy(currentPolicy);
} else {
renderPolicyStatus(policyStatusKey);
}
}
document.querySelector(".language-button")?.addEventListener("click", (event) => {
event.stopPropagation();
const languageMenu = document.querySelector(".language-menu");
setLanguageMenuOpen(Boolean(languageMenu?.hidden));
});
document.querySelectorAll(".language-menu [data-lang]").forEach((button) => {
button.addEventListener("click", (event) => {
event.stopPropagation();
setLanguageMenuOpen(false);
applyLanguage(button.dataset.lang);
});
});
document.addEventListener("click", () => {
setLanguageMenuOpen(false);
});
syncBackFallback();
applyLanguage(currentLang).then(fetchAnchorPolicy);
})();

View File

@ -0,0 +1,632 @@
(function () {
const supportedLanguages = ["en", "ar", "tr", "id"];
const languageLabels = {
en: "EN",
ar: "AR",
tr: "TR",
id: "ID"
};
const API_BASE_URL = "https://jvapi.haiyihy.com";
const fallbackMessages = {
en: {
document_title: "Stream Center",
page_label: "Stream Center",
back: "Back",
title: "Stream Center",
language_button_aria: "Change language",
uid: "UID: 1234567890",
agency: "Agency: Bilibili(101013)",
streaming_duration: "Streaming Duration<br />(Minutes)",
target: "Target",
my_income: "My Income",
withdraw_exchange: "Withdraw/Exchange",
open_withdraw_exchange: "Open withdraw and exchange page",
open_anchor_policy: "Open anchor policy",
anchor_policy: "Anchor Policy",
policy_loading: "Loading policy...",
policy_empty: "No policy data",
policy_missing_token: "Missing token",
policy_load_failed: "Failed to load policy",
policy_level: "Level",
policy_online: "Online (Hour)",
policy_effective_day: "Valid days",
policy_target: "Target",
policy_host_salary: "Host salary",
policy_agent_salary: "Agent salary",
policy_total_salary: "Total salary"
},
ar: {
document_title: "مركز البث",
page_label: "مركز البث",
back: "رجوع",
title: "مركز البث",
language_button_aria: "تغيير اللغة",
uid: "معرّف المستخدم: 1234567890",
agency: "الوكالة: Bilibili(101013)",
streaming_duration: "مدة البث<br />(بالدقائق)",
target: "الهدف",
my_income: "دخلي",
withdraw_exchange: "سحب/استبدال",
open_withdraw_exchange: "فتح صفحة السحب والاستبدال",
open_anchor_policy: "فتح سياسة المضيف",
anchor_policy: "سياسة المضيف",
policy_loading: "جار تحميل السياسة...",
policy_empty: "لا توجد بيانات سياسة",
policy_missing_token: "رمز الدخول مفقود",
policy_load_failed: "فشل تحميل السياسة",
policy_level: "المستوى",
policy_online: "الاتصال (ساعة)",
policy_effective_day: "الأيام الصالحة",
policy_target: "الهدف",
policy_host_salary: "راتب المضيف",
policy_agent_salary: "راتب الوكيل",
policy_total_salary: "إجمالي الراتب"
},
tr: {
document_title: "Yayın Merkezi",
page_label: "Yayın Merkezi",
back: "Geri",
title: "Yayın Merkezi",
language_button_aria: "Dili değiştir",
uid: "UID: 1234567890",
agency: "Ajans: Bilibili(101013)",
streaming_duration: "Yayın Süresi<br />(Dakika)",
target: "Hedef",
my_income: "Gelirim",
withdraw_exchange: "Çek/Değiştir",
open_withdraw_exchange: "Çekme ve değişim sayfasını aç",
open_anchor_policy: "Host politikasını aç",
anchor_policy: "Host Politikası",
policy_loading: "Politika yükleniyor...",
policy_empty: "Politika verisi yok",
policy_missing_token: "Token eksik",
policy_load_failed: "Politika yüklenemedi",
policy_level: "Seviye",
policy_online: "Online (Saat)",
policy_effective_day: "Geçerli gün",
policy_target: "Hedef",
policy_host_salary: "Host maaşı",
policy_agent_salary: "Ajans maaşı",
policy_total_salary: "Toplam maaş"
},
id: {
document_title: "Pusat Streaming",
page_label: "Pusat Streaming",
back: "Kembali",
title: "Pusat Streaming",
language_button_aria: "Ubah bahasa",
uid: "UID: 1234567890",
agency: "Agensi: Bilibili(101013)",
streaming_duration: "Durasi Streaming<br />(Menit)",
target: "Target",
my_income: "Penghasilan Saya",
withdraw_exchange: "Tarik/Tukar",
open_withdraw_exchange: "Buka halaman tarik dan tukar",
open_anchor_policy: "Buka kebijakan host",
anchor_policy: "Kebijakan Host",
policy_loading: "Memuat kebijakan...",
policy_empty: "Tidak ada data kebijakan",
policy_missing_token: "Token tidak ditemukan",
policy_load_failed: "Gagal memuat kebijakan",
policy_level: "Level",
policy_online: "Online (Jam)",
policy_effective_day: "Hari valid",
policy_target: "Target",
policy_host_salary: "Gaji host",
policy_agent_salary: "Gaji agensi",
policy_total_salary: "Total gaji"
}
};
const params = readURLParams();
let currentLang = normalizeLanguage(params.get("lang"));
let currentMessages = fallbackMessages[currentLang] || fallbackMessages.en;
let currentProfile = null;
let currentSalary = "0.00";
let currentStreamingDuration = "0";
let nextTargetValue = "-";
function readURLParams() {
const result = new URLSearchParams(window.location.search);
const hashQuery = window.location.hash.includes("?") ? window.location.hash.split("?")[1] : "";
const hashParams = new URLSearchParams(hashQuery);
hashParams.forEach((value, key) => {
if (!result.has(key)) result.set(key, value);
});
return result;
}
function readRawParam(name) {
const queries = [window.location.search.replace(/^\?/, "")];
if (window.location.hash.includes("?")) queries.push(window.location.hash.split("?")[1]);
for (const query of queries) {
for (const part of query.split("&")) {
if (!part) continue;
const equalIndex = part.indexOf("=");
const rawKey = equalIndex >= 0 ? part.slice(0, equalIndex) : part;
const rawValue = equalIndex >= 0 ? part.slice(equalIndex + 1) : "";
let decodedKey = "";
try {
decodedKey = decodeURIComponent(rawKey);
} catch (error) {
continue;
}
if (decodedKey !== name) continue;
try {
return decodeURIComponent(rawValue.replace(/\+/g, "%2B"));
} catch (error) {
return rawValue;
}
}
}
return "";
}
function normalizeLanguage(lang) {
const value = String(lang || "en").toLowerCase();
return supportedLanguages.includes(value) ? value : "en";
}
async function loadMessages(lang) {
const normalizedLang = normalizeLanguage(lang);
if (window.location.protocol === "file:") return fallbackMessages[normalizedLang];
try {
const response = await fetch(`./locales/${normalizedLang}.json`, { cache: "no-store" });
if (response.ok) return await response.json();
} catch (error) {
return fallbackMessages[normalizedLang];
}
return fallbackMessages[normalizedLang];
}
function renderMessages(messages) {
document.querySelectorAll("[data-i18n]").forEach((node) => {
const value = messages[node.dataset.i18n];
if (value) node.textContent = value;
});
document.querySelectorAll("[data-i18n-html]").forEach((node) => {
const value = messages[node.dataset.i18nHtml];
if (value) node.innerHTML = value;
});
document.querySelectorAll("[data-i18n-aria]").forEach((node) => {
const value = messages[node.dataset.i18nAria];
if (value) node.setAttribute("aria-label", value);
});
}
function apiBaseURL() {
const override = window.HYAPP_API_BASE_URL || params.get("apiBase");
if (override) return String(override).replace(/\/+$/, "");
return API_BASE_URL.replace(/\/+$/, "");
}
function normalizeAuthorization(token) {
const value = String(token || "").trim();
if (!value) return "";
return /^bearer\s+/i.test(value) ? value : `Bearer ${value}`;
}
function authHeadersFromToken(token) {
return {
Authorization: normalizeAuthorization(token),
"req-lang": currentLang,
"req-client": "H5",
"req-version": "V2",
"req-zone": Intl.DateTimeFormat().resolvedOptions().timeZone || "Asia/Shanghai",
"req-app-intel": "version=1.0.0;build=1;model=H5;sysVersion=Web;channel=Web",
"req-sys-origin": "origin=ATYOU;originChild=ATYOU"
};
}
function appendTokenParam(url) {
const token = readRawParam("token");
if (!token) return false;
url.searchParams.set("token", token);
return true;
}
function profileUidPrefix(messages) {
const template = messages.uid || fallbackMessages.en.uid;
const marker = "1234567890";
return template.includes(marker) ? template.replace(marker, "").trim() : "UID:";
}
function pickProfileName(profile) {
return profile?.userNickname || profile?.name || "Oscar";
}
function pickProfileAccount(profile) {
return profile?.ownSpecialId?.account || profile?.account || profile?.id || "1234567890";
}
function renderProfile(profile, messages = currentMessages) {
if (!profile) return;
const profileCard = document.querySelector(".profile-card");
const nameNode = document.querySelector("#profileName");
const uidNode = document.querySelector("#profileUid");
const avatarImage = document.querySelector("#profileAvatar");
const avatarFallback = document.querySelector("#profileAvatarFallback");
const displayName = pickProfileName(profile);
const displayAccount = pickProfileAccount(profile);
if (profileCard) {
profileCard.dataset.userId = String(profile.id || "");
profileCard.dataset.account = String(profile.account || "");
profileCard.dataset.specialAccount = String(profile?.ownSpecialId?.account || "");
}
if (nameNode) nameNode.textContent = displayName;
if (uidNode) uidNode.textContent = `${profileUidPrefix(messages)} ${displayAccount}`.trim();
if (!avatarImage || !avatarFallback) return;
const avatar = profile.userAvatar ? String(profile.userAvatar) : "";
avatarImage.alt = displayName;
avatarFallback.setAttribute("aria-label", `${displayName} avatar`);
if (!avatar) {
avatarImage.hidden = true;
avatarFallback.hidden = false;
avatarImage.removeAttribute("src");
return;
}
avatarImage.hidden = false;
avatarFallback.hidden = true;
avatarImage.onerror = () => {
avatarImage.hidden = true;
avatarFallback.hidden = false;
avatarImage.removeAttribute("src");
};
avatarImage.src = avatar;
}
function formatCurrentSalary(value) {
const numericValue = Number(value);
if (Number.isFinite(numericValue)) return numericValue.toFixed(2);
return "0.00";
}
function pickCurrentSalary(body) {
if (typeof body === "number") return body;
if (body && typeof body === "object") {
return body.currentSalary ?? body.availableBalance ?? body.balance ?? 0;
}
return 0;
}
function renderCurrentSalary(value) {
currentSalary = formatCurrentSalary(value);
const salaryNode = document.querySelector("#currentSalary");
if (salaryNode) salaryNode.textContent = currentSalary;
}
function formatStreamingDuration(value) {
const numericValue = Number(value);
if (!Number.isFinite(numericValue)) return "0";
const safeValue = Math.max(0, Math.min(numericValue, 120));
return Number.isInteger(safeValue) ? String(safeValue) : String(Number(safeValue.toFixed(2)));
}
function pickStreamingDuration(body) {
const ownTime = Number(body?.ownTime) || 0;
const otherTime = Number(body?.otherTime) || 0;
return ownTime + otherTime;
}
function renderStreamingDuration(value) {
currentStreamingDuration = formatStreamingDuration(value);
const durationNode = document.querySelector("#streamingDuration");
if (durationNode) durationNode.textContent = currentStreamingDuration;
}
function formatTargetValue(value) {
if (value === null || value === undefined || value === "") return "-";
const number = Number(value);
if (Number.isFinite(number) && Number.isInteger(number)) return String(number);
return String(value);
}
function renderNextTargetValue(value) {
nextTargetValue = formatTargetValue(value);
const targetNode = document.querySelector("#nextTargetValue");
if (targetNode) targetNode.textContent = nextTargetValue;
}
function pickFirstMemberWorkTarget(body) {
const targets = Array.isArray(body?.targets) ? body.targets : [];
return targets[0] || body?.target || null;
}
function pickGiftValue(workTarget) {
const target = workTarget?.target || workTarget || {};
return target.acceptGiftValue ?? target.giftValue ?? target.giftScore ?? target.currentGiftValue ?? null;
}
function pickDirectNextTargetValue(workTarget) {
const target = workTarget?.target || workTarget || {};
return target.nextTarget ?? target.nextLevelTarget ?? target.nextTargetValue ?? workTarget?.nextTarget ?? null;
}
function pickPolicyRows(policyBody) {
if (Array.isArray(policyBody?.policy)) return policyBody.policy;
if (Array.isArray(policyBody?.policies)) return policyBody.policies;
return [];
}
function calculateNextTargetValue(giftValue, policyRows) {
const giftNumber = Number(giftValue);
if (!Number.isFinite(giftNumber) || !policyRows.length) return null;
const rows = policyRows
.map((row) => ({
target: Number(row.target),
rawTarget: row.target
}))
.filter((row) => Number.isFinite(row.target))
.sort((a, b) => a.target - b.target);
if (!rows.length) return null;
const next = rows.find((row) => giftNumber < row.target);
return next ? next.rawTarget : null;
}
async function fetchUserProfile() {
const token = readRawParam("token");
if (!token) {
console.warn("Missing token for /team/member/profile");
return null;
}
try {
const url = new URL("/team/member/profile", `${apiBaseURL()}/`);
const response = await fetch(url.toString(), {
cache: "no-store",
headers: authHeadersFromToken(token)
});
const data = await response.json().catch(() => ({}));
if (!response.ok || data.status === false) {
throw new Error(data.errorMsg || data.message || `Request failed: ${response.status}`);
}
currentProfile = data.body || data.data || {};
renderProfile(currentProfile);
return currentProfile;
} catch (error) {
console.error("Failed to load user profile:", error);
return null;
}
}
async function fetchBankBalance() {
const token = readRawParam("token");
if (!token) {
console.warn("Missing token for /wallet/bank/balance");
return;
}
try {
const url = new URL("/wallet/bank/balance", `${apiBaseURL()}/`);
const response = await fetch(url.toString(), {
cache: "no-store",
headers: authHeadersFromToken(token)
});
const data = await response.json().catch(() => ({}));
if (!response.ok || data.status === false) {
throw new Error(data.errorMsg || data.message || `Request failed: ${response.status}`);
}
renderCurrentSalary(pickCurrentSalary(data.body ?? data.data));
} catch (error) {
console.error("Failed to load bank balance:", error);
renderCurrentSalary(0);
}
}
async function fetchStreamingDuration(memberId) {
const token = readRawParam("token");
if (!token) {
console.warn("Missing token for /team/member/work/statistics-now");
return;
}
if (!memberId) {
console.warn("Missing memberId for /team/member/work/statistics-now");
return;
}
try {
const url = new URL("/team/member/work/statistics-now", `${apiBaseURL()}/`);
url.searchParams.set("dataType", "DAILY");
url.searchParams.set("memberId", String(memberId));
const response = await fetch(url.toString(), {
cache: "no-store",
headers: authHeadersFromToken(token)
});
const data = await response.json().catch(() => ({}));
if (!response.ok || data.status === false) {
throw new Error(data.errorMsg || data.message || `Request failed: ${response.status}`);
}
renderStreamingDuration(pickStreamingDuration(data.body ?? data.data));
} catch (error) {
console.error("Failed to load streaming duration:", error);
renderStreamingDuration(0);
}
}
async function fetchMemberWork(memberId) {
if (!memberId) {
console.warn("Missing memberId for /team/member-work");
return null;
}
try {
const url = new URL("/team/member-work", `${apiBaseURL()}/`);
url.searchParams.set("userId", String(memberId));
const token = readRawParam("token");
if (!token) {
console.warn("Missing token for /team/member-work");
return null;
}
const response = await fetch(url.toString(), {
cache: "no-store",
headers: authHeadersFromToken(token)
});
const data = await response.json().catch(() => ({}));
if (!response.ok || data.status === false) {
throw new Error(data.errorMsg || data.message || `Request failed: ${response.status}`);
}
return data.body || data.data || null;
} catch (error) {
console.error("Failed to load member work:", error);
return null;
}
}
async function fetchAnchorPolicyForTarget() {
try {
const url = new URL("/app/h5/host-center/policy", `${apiBaseURL()}/`);
if (!appendTokenParam(url)) {
console.warn("Missing token for /app/h5/host-center/policy");
return null;
}
if (params.get("policyType")) url.searchParams.set("policyType", params.get("policyType"));
const response = await fetch(url.toString(), { cache: "no-store" });
const data = await response.json().catch(() => ({}));
if (!response.ok || data.status === false) {
throw new Error(data.errorMsg || data.message || `Request failed: ${response.status}`);
}
return data.body || data.data || null;
} catch (error) {
console.error("Failed to load anchor policy for next target:", error);
return null;
}
}
async function fetchNextTargetValue(memberId) {
const memberWork = await fetchMemberWork(memberId);
const workTarget = pickFirstMemberWorkTarget(memberWork);
if (!workTarget) {
renderNextTargetValue("-");
return;
}
const directNextTarget = pickDirectNextTargetValue(workTarget);
const giftValue = pickGiftValue(workTarget);
const policy = await fetchAnchorPolicyForTarget();
const calculatedNextTarget = calculateNextTargetValue(giftValue, pickPolicyRows(policy));
renderNextTargetValue(calculatedNextTarget ?? directNextTarget ?? "-");
}
function syncLocalizedLinks() {
const nextParams = new URLSearchParams(params);
const token = readRawParam("token");
if (token) nextParams.set("token", token);
if (currentLang) nextParams.set("lang", currentLang);
const query = nextParams.toString();
document.querySelectorAll('a[href^="../withdraw-exchange/index.html"]').forEach((link) => {
link.setAttribute("href", `../withdraw-exchange/index.html${query ? `?${query}` : ""}`);
});
}
function syncPolicyLink() {
const link = document.querySelector("#anchorPolicyLink");
if (!link) return;
const nextParams = new URLSearchParams(params);
const token = readRawParam("token");
if (token) nextParams.set("token", token);
if (currentLang) nextParams.set("lang", currentLang);
const query = nextParams.toString();
link.setAttribute("href", `./policy.html${query ? `?${query}` : ""}`);
}
function setLanguageMenuOpen(open) {
const languageButton = document.querySelector(".language-button");
const languageMenu = document.querySelector(".language-menu");
if (!languageButton || !languageMenu) return;
languageMenu.hidden = !open;
languageButton.setAttribute("aria-expanded", String(open));
}
function renderLanguageControls(messages) {
const languageButton = document.querySelector(".language-button");
if (languageButton) {
languageButton.textContent = languageLabels[currentLang];
languageButton.setAttribute("aria-label", messages.language_button_aria || "Change language");
}
document.querySelectorAll(".language-menu [data-lang]").forEach((button) => {
const isActive = button.dataset.lang === currentLang;
button.classList.toggle("is-active", isActive);
button.setAttribute("aria-pressed", String(isActive));
});
}
async function applyLanguage(lang) {
currentLang = normalizeLanguage(lang);
const messages = await loadMessages(currentLang);
currentMessages = messages;
document.documentElement.lang = currentLang;
document.documentElement.dir = currentLang === "ar" ? "rtl" : "ltr";
document.title = messages.document_title || messages.title || "Stream Center";
renderMessages(messages);
renderLanguageControls(messages);
syncLocalizedLinks();
syncPolicyLink();
renderProfile(currentProfile, messages);
}
document.querySelector(".language-button")?.addEventListener("click", (event) => {
event.stopPropagation();
const languageMenu = document.querySelector(".language-menu");
setLanguageMenuOpen(Boolean(languageMenu?.hidden));
});
document.querySelectorAll(".language-menu [data-lang]").forEach((button) => {
button.addEventListener("click", (event) => {
event.stopPropagation();
setLanguageMenuOpen(false);
applyLanguage(button.dataset.lang);
});
});
document.addEventListener("click", () => {
setLanguageMenuOpen(false);
});
document.querySelector(".back-button")?.addEventListener("click", () => {
if (window.history.length > 1) window.history.back();
});
applyLanguage(currentLang).then(() => {
fetchUserProfile().then((profile) => {
const memberId = profile?.id || profile?.account;
if (memberId) {
fetchStreamingDuration(memberId);
fetchNextTargetValue(memberId);
}
});
fetchBankBalance();
});
})();

View File

@ -0,0 +1,791 @@
:root {
--page-bg: #f6f7f7;
--card-bg: rgba(255, 255, 255, 0.96);
--text: #25282e;
--muted: #8f9196;
--line: #eef0f2;
--mint: #43e7d8;
--gold: #e3b55b;
--shadow: 0 8px 22px rgba(36, 46, 60, 0.055);
}
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
min-height: 100%;
margin: 0;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
}
body {
display: flex;
justify-content: center;
background: #111;
color: var(--text);
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
button {
border: 0;
padding: 0;
background: transparent;
color: inherit;
font: inherit;
}
a {
color: inherit;
text-decoration: none;
}
.host-center {
position: relative;
width: 100%;
max-width: 430px;
min-height: 100vh;
overflow: hidden;
background: var(--page-bg);
}
.hero-bg {
position: absolute;
inset: 0 0 auto;
height: 215px;
background:
radial-gradient(circle at 72% 4%, rgba(209, 255, 248, 0.98) 0, rgba(209, 255, 248, 0.88) 23%, rgba(209, 255, 248, 0) 48%),
radial-gradient(circle at 14% 20%, rgba(185, 250, 244, 0.78) 0, rgba(185, 250, 244, 0) 35%),
linear-gradient(151deg, #ccf7ef 0%, #ebfffa 58%, rgba(242, 255, 252, 0.7) 100%);
clip-path: polygon(0 0, 100% 0, 100% 76%, 70% 47%, 40% 58%, 0 78%);
}
.status-bar,
.title-bar,
.content,
.home-indicator {
position: relative;
z-index: 1;
}
.status-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 47px;
padding: 18px 29px 0 38px;
color: #11161c;
direction: ltr;
font-size: 15px;
font-weight: 800;
}
.status-icons {
display: flex;
align-items: center;
gap: 6px;
}
.cellular {
display: flex;
align-items: flex-end;
gap: 2px;
width: 18px;
height: 12px;
}
.cellular i {
width: 3px;
border-radius: 2px;
background: #11161c;
}
.cellular i:nth-child(1) {
height: 4px;
}
.cellular i:nth-child(2) {
height: 6px;
}
.cellular i:nth-child(3) {
height: 9px;
}
.cellular i:nth-child(4) {
height: 11px;
}
.wifi {
position: relative;
width: 18px;
height: 13px;
}
.wifi i {
position: absolute;
left: 50%;
border: 2px solid #11161c;
border-bottom: 0;
border-left-color: transparent;
border-right-color: transparent;
border-radius: 12px 12px 0 0;
transform: translateX(-50%);
}
.wifi i:nth-child(1) {
top: 1px;
width: 16px;
height: 8px;
}
.wifi i:nth-child(2) {
top: 5px;
width: 10px;
height: 5px;
}
.wifi i:nth-child(3) {
top: 10px;
width: 4px;
height: 4px;
border: 0;
border-radius: 50%;
background: #11161c;
}
.battery {
position: relative;
width: 24px;
height: 12px;
border: 2px solid #11161c;
border-radius: 3px;
}
.battery::after {
content: "";
position: absolute;
top: 3px;
right: -4px;
width: 2px;
height: 4px;
border-radius: 0 2px 2px 0;
background: #11161c;
}
.battery i {
display: block;
width: 16px;
height: 100%;
background: #11161c;
}
.title-bar {
display: grid;
grid-template-columns: 52px 1fr 52px;
align-items: center;
z-index: 3;
height: 60px;
padding: 0 14px;
direction: ltr;
}
.title-bar h1 {
margin: 0;
color: #24282e;
font-size: 22px;
font-weight: 900;
line-height: 1;
text-align: center;
}
.back-button {
display: flex;
align-items: center;
justify-content: flex-start;
width: 44px;
height: 44px;
}
.back-button svg {
width: 28px;
height: 28px;
}
.back-button path {
fill: none;
stroke: #24282e;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 3;
}
.language-switcher {
position: relative;
justify-self: end;
}
.language-button {
justify-self: end;
display: flex;
align-items: center;
justify-content: center;
width: 42px;
height: 30px;
border: 1px solid rgba(35, 40, 46, 0.12);
border-radius: 15px;
background: rgba(255, 255, 255, 0.68);
color: #25282e;
font-size: 13px;
font-weight: 900;
box-shadow: 0 4px 12px rgba(43, 118, 107, 0.08);
}
.language-menu {
position: absolute;
z-index: 4;
top: calc(100% + 7px);
right: 0;
display: grid;
gap: 2px;
min-width: 58px;
padding: 5px;
border: 1px solid rgba(35, 40, 46, 0.1);
border-radius: 8px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 10px 26px rgba(36, 46, 60, 0.14);
}
.language-menu button {
min-height: 28px;
border-radius: 6px;
color: #25282e;
font-size: 12px;
font-weight: 900;
}
.language-menu button.is-active {
background: #e6fbf8;
color: #0e8f82;
}
.language-menu[hidden] {
display: none;
}
.content {
display: flex;
flex-direction: column;
gap: 15px;
padding: 8px 14px 90px;
}
.policy-content {
padding-top: 18px;
}
.card {
width: 100%;
border-radius: 8px;
background: var(--card-bg);
box-shadow: var(--shadow);
}
.profile-card {
display: flex;
align-items: center;
min-height: 103px;
padding: 16px 24px;
}
.avatar-shell {
position: relative;
width: 64px;
height: 64px;
flex: 0 0 64px;
}
.avatar-image,
.avatar-fallback {
display: block;
width: 64px;
height: 64px;
border-radius: 50%;
}
.avatar-image {
object-fit: cover;
}
.avatar-image[hidden],
.avatar-fallback[hidden] {
display: none;
}
.profile-copy {
min-width: 0;
margin-left: 20px;
}
.name {
color: #25282e;
font-size: 20px;
font-weight: 900;
line-height: 1.1;
}
.meta,
.agency {
color: var(--muted);
font-size: 13px;
font-weight: 750;
line-height: 1.2;
}
.meta {
margin-top: 6px;
}
.agency {
display: flex;
align-items: center;
max-width: 100%;
margin-top: 6px;
}
.agency span:first-child {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chevron {
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 8px;
color: #8b8e94;
font-size: 25px;
font-weight: 500;
line-height: 0.8;
}
.metrics-card {
display: grid;
grid-template-columns: 1fr 1px 1fr;
min-height: 111px;
padding: 23px 24px 20px;
}
.metric {
min-width: 0;
}
.metric:first-child {
padding-right: 20px;
}
.gift-metric {
padding-left: 25px;
}
.vertical-line {
width: 1px;
background: var(--line);
}
.label {
min-height: 36px;
color: #8b8d92;
font-size: 16px;
font-weight: 750;
line-height: 1.12;
}
.value {
margin-top: 4px;
color: #292d33;
font-size: 25px;
font-weight: 900;
line-height: 1;
}
.gift-value,
.income-value {
display: flex;
align-items: center;
}
.gift-value {
margin-top: 7px;
color: #252a30;
font-size: 24px;
font-weight: 900;
line-height: 1;
}
.gift-value .amount-text {
min-width: 0;
overflow-wrap: anywhere;
}
.dollar-symbol {
display: inline-flex;
align-items: center;
justify-content: center;
width: 25px;
height: 25px;
flex: 0 0 25px;
margin-right: 8px;
border-radius: 50%;
background: linear-gradient(145deg, #ffe179 0%, #ffb62d 52%, #f49b20 100%);
color: #fff;
font-size: 18px;
font-weight: 950;
line-height: 1;
box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.7), 0 1px 2px rgba(195, 117, 12, 0.24);
}
.income-card {
min-height: 143px;
padding: 19px 24px 18px;
}
.income-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.income-title {
display: flex;
align-items: center;
min-width: 0;
color: #24282e;
font-size: 17px;
font-weight: 900;
}
.income-title svg {
width: 17px;
height: 17px;
margin-left: 7px;
}
.income-title circle,
.income-title path {
fill: none;
stroke: #272b31;
stroke-linecap: round;
stroke-width: 2;
}
.cash-link {
display: flex;
align-items: center;
flex: 0 0 auto;
color: var(--mint);
font-size: 13px;
font-weight: 900;
}
.cash-link .chevron {
margin-left: 5px;
color: var(--mint);
font-size: 20px;
}
.income-value {
margin-top: 14px;
max-width: 100%;
color: #23272d;
font-size: 34px;
font-weight: 950;
line-height: 1.05;
}
.income-currency {
flex: 0 0 auto;
margin-right: 6px;
color: var(--gold);
font-size: 28px;
font-weight: 950;
line-height: 1;
}
.income-value .amount-text {
min-width: 0;
overflow-wrap: anywhere;
}
.income-value .chevron {
margin-left: 9px;
color: #b4b6ba;
font-size: 24px;
}
.policy-entry-card {
min-height: 64px;
padding: 0 16px;
}
.policy-entry-link {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 64px;
}
.policy-entry-left {
display: flex;
align-items: center;
min-width: 0;
}
.policy-entry-left svg {
width: 24px;
height: 24px;
flex: 0 0 24px;
margin-right: 18px;
}
.policy-entry-left svg path {
fill: none;
stroke: #55585f;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2.2;
}
.policy-entry-left span {
min-width: 0;
overflow: hidden;
color: #464951;
font-size: 16px;
font-weight: 850;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.policy-entry-link > .chevron {
flex: 0 0 auto;
margin-left: 12px;
color: #8b8d92;
font-size: 24px;
}
.policy-card {
min-height: 184px;
padding: 18px 16px 16px;
}
.policy-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 13px;
}
.policy-head h2 {
min-width: 0;
margin: 0;
overflow: hidden;
color: #24282e;
font-size: 17px;
font-weight: 900;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.policy-region {
flex: 0 1 auto;
overflow: hidden;
color: #8b8d92;
font-size: 12px;
font-weight: 800;
line-height: 1.2;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
}
.policy-status {
display: flex;
align-items: center;
justify-content: center;
min-height: 112px;
border: 1px dashed #e5e8ea;
border-radius: 8px;
color: #8b8d92;
font-size: 14px;
font-weight: 800;
text-align: center;
}
.policy-list {
display: grid;
gap: 10px;
}
.policy-row {
display: grid;
gap: 10px;
padding: 12px;
border: 1px solid #eef0f2;
border-radius: 8px;
background: #fbfcfc;
}
.policy-row-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.policy-level {
color: #24282e;
font-size: 16px;
font-weight: 950;
}
.policy-effective-day {
color: #8b8d92;
font-size: 12px;
font-weight: 850;
}
.policy-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.policy-cell {
min-width: 0;
}
.policy-cell-label {
overflow: hidden;
color: #96989d;
font-size: 11px;
font-weight: 800;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.policy-cell-value {
margin-top: 4px;
overflow-wrap: anywhere;
color: #292d33;
font-size: 14px;
font-weight: 900;
line-height: 1.2;
}
.home-indicator {
position: absolute;
left: 50%;
bottom: 8px;
width: 154px;
height: 5px;
border-radius: 999px;
background: #050506;
transform: translateX(-50%);
}
[dir="rtl"] .content {
direction: rtl;
}
[dir="rtl"] .profile-copy {
margin-right: 20px;
margin-left: 0;
text-align: right;
}
[dir="rtl"] .income-title svg {
margin-right: 7px;
margin-left: 0;
}
[dir="rtl"] .dollar-symbol,
[dir="rtl"] .income-currency {
margin-right: 0;
margin-left: 8px;
}
[dir="rtl"] .chevron {
margin-right: 8px;
margin-left: 0;
transform: rotate(180deg);
}
[dir="rtl"] .cash-link .chevron {
margin-right: 5px;
margin-left: 0;
}
[dir="rtl"] .policy-entry-left svg {
margin-right: 0;
margin-left: 18px;
}
[dir="rtl"] .policy-entry-link > .chevron {
margin-right: 12px;
margin-left: 0;
}
[dir="rtl"] .policy-region {
text-align: left;
}
@media (max-width: 360px) {
.status-bar {
padding-right: 22px;
padding-left: 31px;
}
.content {
padding-right: 12px;
padding-left: 12px;
}
.profile-card,
.metrics-card,
.income-card {
padding-right: 18px;
padding-left: 18px;
}
.profile-copy {
margin-left: 14px;
}
.gift-metric {
padding-left: 18px;
}
.income-value {
font-size: 29px;
}
.policy-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
[dir="rtl"] .profile-copy {
margin-right: 14px;
margin-left: 0;
}
}

View File

@ -0,0 +1,127 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Withdraw/Exchange</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div class="wallet-page" aria-label="Withdraw and exchange" data-i18n-aria="page_label">
<div class="hero-bg" aria-hidden="true"></div>
<header class="status-bar" aria-hidden="true">
<span class="status-time">9:41</span>
<span class="status-icons">
<span class="cellular"><i></i><i></i><i></i><i></i></span>
<span class="wifi"><i></i><i></i><i></i></span>
<span class="battery"><i></i></span>
</span>
</header>
<nav class="title-bar">
<button class="back-button" type="button" aria-label="Back" data-i18n-aria="back">
<svg viewBox="0 0 32 32" aria-hidden="true">
<path d="M20 8 12 16l8 8" />
</svg>
</button>
<h1 data-i18n="title">Withdraw/Exchange</h1>
<div class="language-switcher">
<button class="language-button" type="button" aria-label="Change language" data-i18n-aria="language_button_aria" aria-expanded="false">EN</button>
<div class="language-menu" hidden>
<button type="button" data-lang="en">EN</button>
<button type="button" data-lang="ar">AR</button>
<button type="button" data-lang="tr">TR</button>
<button type="button" data-lang="id">ID</button>
</div>
</div>
</nav>
<main class="content">
<section class="balance-card">
<div class="balance-label" data-i18n="available_income">Available Income</div>
<div class="balance-value">
<span class="balance-currency" aria-hidden="true">$</span>
<span id="balanceAmount">0.00</span>
</div>
</section>
<section class="mode-card">
<div class="mode-tabs" role="tablist" aria-label="Withdraw or exchange">
<button class="mode-tab active" type="button" role="tab" aria-selected="true" data-mode="withdraw" data-i18n="withdraw">Withdraw</button>
<button class="mode-tab" type="button" role="tab" aria-selected="false" data-mode="exchange" data-i18n="exchange">Exchange</button>
</div>
<div class="mode-panel active" data-panel="withdraw">
<label class="field">
<span data-i18n="withdraw_amount">Withdraw amount</span>
<div class="field-control amount-control">
<span>$</span>
<input id="withdrawAmount" type="number" inputmode="decimal" min="0" step="0.01" placeholder="50" aria-label="Withdraw amount" data-i18n-aria="withdraw_amount" />
</div>
</label>
<div class="hint-row">
<span data-i18n="minimum">Minimum 50 USD</span>
<button class="text-action" type="button" data-i18n="all">All</button>
</div>
</div>
<div class="mode-panel" data-panel="exchange">
<label class="field">
<span data-i18n="exchange_amount">Exchange amount</span>
<div class="field-control amount-control">
<span>$</span>
<input type="number" inputmode="decimal" min="0" step="0.01" value="120.00" aria-label="Exchange amount" data-i18n-aria="exchange_amount" />
</div>
</label>
<div class="exchange-preview">
<span data-i18n="you_receive">You receive</span>
<strong>$120.00</strong>
</div>
</div>
</section>
<section class="card-section">
<div class="section-head">
<div>
<div class="section-title" data-i18n="payment_method">Payment method</div>
<div class="section-subtitle" data-i18n="payment_method_subtitle">The currently used bank card will receive this withdrawal.</div>
</div>
<button class="bind-address-button" type="button" hidden data-i18n="manage">Manage</button>
</div>
<div class="empty-address-state">
<div class="empty-icon" aria-hidden="true">
<svg viewBox="0 0 32 32">
<path d="M9 16a5 5 0 0 1 5-5h3" />
<path d="M15 21h-3a5 5 0 0 1 0-10h3" />
<path d="M17 21h3a5 5 0 0 0 0-10h-3" />
<path d="M13 16h6" />
</svg>
</div>
<div>
<div class="empty-title" data-i18n="no_bank_card">No bank card in use</div>
<div class="empty-copy" data-i18n="no_bank_card_copy">Set a bank card as in use before withdrawing.</div>
</div>
</div>
<div class="bound-address-card" hidden>
<div class="network-badge" aria-hidden="true">BANK</div>
<div class="bound-copy">
<div class="bound-network">Bank card</div>
<div class="bound-address">-</div>
<div class="bound-meta">-</div>
</div>
<button class="change-address" type="button" hidden data-i18n="change">Change</button>
</div>
</section>
<button class="submit-button" type="button" disabled data-i18n="withdraw_now">Withdraw now</button>
</main>
<div class="toast" hidden></div>
<div class="home-indicator" aria-hidden="true"></div>
</div>
<script src="./script.js" defer></script>
</body>
</html>

View File

@ -0,0 +1,515 @@
(function () {
const API_BASE_URL = "https://jvapi.haiyihy.com";
const supportedLanguages = ["en", "ar", "tr", "id"];
const languageLabels = {
en: "EN",
ar: "AR",
tr: "TR",
id: "ID"
};
const translations = {
en: {
page_label: "Withdraw and exchange",
back: "Back",
title: "Withdraw/Exchange",
language_button_aria: "Change language",
available_income: "Available Income",
withdraw: "Withdraw",
exchange: "Exchange",
withdraw_amount: "Withdraw amount",
exchange_amount: "Exchange amount",
minimum: "Minimum 50 USD",
all: "All",
you_receive: "You receive",
payment_method: "Payment method",
payment_method_subtitle: "The currently used bank card will receive this withdrawal.",
manage: "Manage",
no_bank_card: "No bank card in use",
no_bank_card_copy: "Set a bank card as in use before withdrawing.",
change: "Change",
withdraw_now: "Withdraw now",
exchange_now: "Exchange now",
submitting: "Submitting...",
payee: "Payee",
card_number: "Card",
missing_token: "Missing token",
load_balance_failed: "Failed to load balance",
load_bank_card_failed: "Failed to load bank card",
missing_bank_card: "No bank card in use",
invalid_amount: "Please enter a valid amount",
amount_too_low: "Minimum withdrawal amount is 50 USD",
amount_exceeds_balance: "Amount exceeds available income",
withdraw_success: "Withdrawal submitted",
withdraw_failed: "Withdrawal failed",
exchange_unavailable: "Exchange is not available on this page"
},
ar: {
page_label: "السحب والتبديل",
back: "رجوع",
title: "سحب/تبديل",
language_button_aria: "تغيير اللغة",
available_income: "الدخل المتاح",
withdraw: "سحب",
exchange: "تبديل",
withdraw_amount: "مبلغ السحب",
exchange_amount: "مبلغ التبديل",
minimum: "الحد الأدنى 50 USD",
all: "الكل",
you_receive: "ستحصل على",
payment_method: "طريقة الدفع",
payment_method_subtitle: "سيتم إرسال السحب إلى البطاقة المستخدمة حاليًا.",
manage: "إدارة",
no_bank_card: "لا توجد بطاقة مستخدمة",
no_bank_card_copy: "عيّن بطاقة بنكية مستخدمة قبل السحب.",
change: "تغيير",
withdraw_now: "اسحب الآن",
exchange_now: "بدل الآن",
submitting: "جار الإرسال...",
payee: "المستفيد",
card_number: "البطاقة",
missing_token: "رمز الدخول مفقود",
load_balance_failed: "فشل تحميل الرصيد",
load_bank_card_failed: "فشل تحميل البطاقة",
missing_bank_card: "لا توجد بطاقة مستخدمة",
invalid_amount: "يرجى إدخال مبلغ صالح",
amount_too_low: "الحد الأدنى للسحب هو 50 USD",
amount_exceeds_balance: "المبلغ يتجاوز الدخل المتاح",
withdraw_success: "تم إرسال طلب السحب",
withdraw_failed: "فشل السحب",
exchange_unavailable: "التبديل غير متاح في هذه الصفحة"
},
tr: {
page_label: "Çekme ve değişim",
back: "Geri",
title: "Çek/Değiştir",
language_button_aria: "Dili değiştir",
available_income: "Kullanılabilir Gelir",
withdraw: "Withdraw",
exchange: "Exchange",
withdraw_amount: "Withdraw amount",
exchange_amount: "Exchange amount",
minimum: "Minimum 50 USD",
all: "All",
you_receive: "You receive",
payment_method: "Payment method",
payment_method_subtitle: "The currently used bank card will receive this withdrawal.",
manage: "Manage",
no_bank_card: "No bank card in use",
no_bank_card_copy: "Set a bank card as in use before withdrawing.",
change: "Change",
withdraw_now: "Withdraw now",
exchange_now: "Exchange now",
submitting: "Submitting...",
payee: "Payee",
card_number: "Card",
missing_token: "Missing token",
load_balance_failed: "Failed to load balance",
load_bank_card_failed: "Failed to load bank card",
missing_bank_card: "No bank card in use",
invalid_amount: "Please enter a valid amount",
amount_too_low: "Minimum withdrawal amount is 50 USD",
amount_exceeds_balance: "Amount exceeds available income",
withdraw_success: "Withdrawal submitted",
withdraw_failed: "Withdrawal failed",
exchange_unavailable: "Exchange is not available on this page"
},
id: {
page_label: "Tarik dan tukar",
back: "Kembali",
title: "Tarik/Tukar",
language_button_aria: "Ubah bahasa",
available_income: "Penghasilan Tersedia",
withdraw: "Tarik",
exchange: "Tukar",
withdraw_amount: "Jumlah penarikan",
exchange_amount: "Jumlah penukaran",
minimum: "Minimum 50 USD",
all: "Semua",
you_receive: "Anda menerima",
payment_method: "Metode pembayaran",
payment_method_subtitle: "Kartu bank yang sedang digunakan akan menerima penarikan ini.",
manage: "Kelola",
no_bank_card: "Tidak ada kartu aktif",
no_bank_card_copy: "Atur kartu bank sebagai aktif sebelum menarik.",
change: "Ubah",
withdraw_now: "Tarik sekarang",
exchange_now: "Tukar sekarang",
submitting: "Mengirim...",
payee: "Penerima",
card_number: "Kartu",
missing_token: "Token tidak ditemukan",
load_balance_failed: "Gagal memuat saldo",
load_bank_card_failed: "Gagal memuat kartu bank",
missing_bank_card: "Tidak ada kartu aktif",
invalid_amount: "Masukkan jumlah yang valid",
amount_too_low: "Minimum penarikan adalah 50 USD",
amount_exceeds_balance: "Jumlah melebihi penghasilan tersedia",
withdraw_success: "Penarikan terkirim",
withdraw_failed: "Penarikan gagal",
exchange_unavailable: "Penukaran tidak tersedia di halaman ini"
}
};
const params = readURLParams();
let currentLang = normalizeLanguage(params.get("lang"));
let currentMode = "withdraw";
let currentMessages = translations[currentLang] || translations.en;
let availableBalance = 0;
let selectedBankCard = null;
let isSubmitting = false;
const amountInput = document.querySelector("#withdrawAmount");
const balanceAmount = document.querySelector("#balanceAmount");
const emptyState = document.querySelector(".empty-address-state");
const boundCard = document.querySelector(".bound-address-card");
const submitButton = document.querySelector(".submit-button");
const toast = document.querySelector(".toast");
function readURLParams() {
const result = new URLSearchParams(window.location.search);
const hashQuery = window.location.hash.includes("?") ? window.location.hash.split("?")[1] : "";
const hashParams = new URLSearchParams(hashQuery);
hashParams.forEach((value, key) => {
if (!result.has(key)) result.set(key, value);
});
return result;
}
function readRawParam(name) {
const queries = [window.location.search.replace(/^\?/, "")];
if (window.location.hash.includes("?")) queries.push(window.location.hash.split("?")[1]);
for (const query of queries) {
for (const part of query.split("&")) {
if (!part) continue;
const equalIndex = part.indexOf("=");
const rawKey = equalIndex >= 0 ? part.slice(0, equalIndex) : part;
const rawValue = equalIndex >= 0 ? part.slice(equalIndex + 1) : "";
let decodedKey = "";
try {
decodedKey = decodeURIComponent(rawKey);
} catch (error) {
continue;
}
if (decodedKey !== name) continue;
try {
return decodeURIComponent(rawValue.replace(/\+/g, "%2B"));
} catch (error) {
return rawValue;
}
}
}
return "";
}
function normalizeLanguage(lang) {
const value = String(lang || "en").toLowerCase();
return supportedLanguages.includes(value) ? value : "en";
}
function apiBaseURL() {
const override = window.HYAPP_API_BASE_URL || params.get("apiBase");
if (override) return String(override).replace(/\/+$/, "");
return API_BASE_URL;
}
function normalizeAuthorization(token) {
const value = String(token || "").trim();
if (!value) return "";
return /^bearer\s+/i.test(value) ? value : `Bearer ${value}`;
}
function authHeadersFromToken(token, hasBody) {
const headers = {
Authorization: normalizeAuthorization(token),
"req-lang": currentLang,
"req-client": "H5",
"req-version": "V2",
"req-zone": Intl.DateTimeFormat().resolvedOptions().timeZone || "Asia/Shanghai",
"req-app-intel": "version=1.0.0;build=1;model=H5;sysVersion=Web;channel=Web",
"req-sys-origin": "origin=ATYOU;originChild=ATYOU"
};
if (hasBody) headers["content-type"] = "application/json";
return headers;
}
async function requestJSON(path, options = {}) {
const token = readRawParam("token");
if (!token) throw new Error(currentMessages.missing_token || translations.en.missing_token);
const response = await fetch(`${apiBaseURL()}${path}`, {
method: options.method || "GET",
cache: "no-store",
headers: authHeadersFromToken(token, options.body !== undefined),
body: options.body === undefined ? undefined : JSON.stringify(options.body)
});
const data = await response.json().catch(() => ({}));
if (!response.ok || data.status === false) {
throw new Error(data.errorMsg || data.message || `Request failed: ${response.status}`);
}
return data;
}
function message(key, fallback) {
return currentMessages[key] || translations.en[key] || fallback || key;
}
function formatMoney(value) {
const number = Number(value);
return Number.isFinite(number) ? number.toFixed(2) : "0.00";
}
function moneyNumber(value) {
const number = Number(value);
return Number.isFinite(number) ? number : 0;
}
function maskCardNumber(value) {
const text = String(value || "").trim();
if (!text) return "-";
if (text.length <= 8) return text;
return `${text.slice(0, 4)} **** ${text.slice(-4)}`;
}
function activeAmount() {
return moneyNumber(amountInput?.value);
}
function applyLanguage(lang) {
currentLang = normalizeLanguage(lang);
currentMessages = translations[currentLang] || translations.en;
document.documentElement.lang = currentLang;
document.documentElement.dir = currentLang === "ar" ? "rtl" : "ltr";
document.querySelectorAll("[data-i18n]").forEach((node) => {
const value = currentMessages[node.dataset.i18n];
if (value) node.textContent = value;
});
document.querySelectorAll("[data-i18n-aria]").forEach((node) => {
const value = currentMessages[node.dataset.i18nAria];
if (value) node.setAttribute("aria-label", value);
});
const languageButton = document.querySelector(".language-button");
if (languageButton) {
languageButton.textContent = languageLabels[currentLang];
languageButton.setAttribute("aria-label", message("language_button_aria", "Change language"));
}
document.querySelectorAll(".language-menu [data-lang]").forEach((button) => {
const isActive = button.dataset.lang === currentLang;
button.classList.toggle("is-active", isActive);
button.setAttribute("aria-pressed", String(isActive));
});
updateSubmitState();
}
function setMode(mode) {
currentMode = mode;
document.querySelectorAll(".mode-tab").forEach((tab) => {
const active = tab.dataset.mode === mode;
tab.classList.toggle("active", active);
tab.setAttribute("aria-selected", String(active));
});
document.querySelectorAll(".mode-panel").forEach((panel) => {
panel.classList.toggle("active", panel.dataset.panel === mode);
});
updateSubmitState();
}
function updateSubmitState() {
if (!submitButton) return;
if (isSubmitting) {
submitButton.textContent = message("submitting", "Submitting...");
submitButton.disabled = true;
return;
}
submitButton.textContent = currentMode === "exchange" ? message("exchange_now", "Exchange now") : message("withdraw_now", "Withdraw now");
const amount = activeAmount();
submitButton.disabled = currentMode !== "withdraw" ||
!selectedBankCard?.id ||
!amount ||
amount < 50 ||
amount > availableBalance;
}
function showToast(keyOrText, fallback) {
if (!toast) return;
toast.textContent = message(keyOrText, fallback || keyOrText);
toast.hidden = false;
window.clearTimeout(showToast.timer);
showToast.timer = window.setTimeout(() => {
toast.hidden = true;
}, 2200);
}
function renderBalance(value) {
availableBalance = moneyNumber(value);
if (balanceAmount) balanceAmount.textContent = formatMoney(availableBalance);
if (amountInput) amountInput.max = formatMoney(availableBalance);
updateSubmitState();
}
function renderBankCard(card) {
selectedBankCard = card || null;
if (!selectedBankCard) {
if (emptyState) emptyState.hidden = false;
if (boundCard) boundCard.hidden = true;
updateSubmitState();
return;
}
const badge = boundCard?.querySelector(".network-badge");
const network = boundCard?.querySelector(".bound-network");
const address = boundCard?.querySelector(".bound-address");
const meta = boundCard?.querySelector(".bound-meta");
if (badge) badge.textContent = selectedBankCard.cardType || "BANK";
if (network) network.textContent = selectedBankCard.cardType || "Bank card";
if (address) address.textContent = `${message("card_number", "Card")}: ${maskCardNumber(selectedBankCard.cardNo)}`;
if (meta) meta.textContent = `${message("payee", "Payee")}: ${selectedBankCard.payee || "-"}`;
if (emptyState) emptyState.hidden = true;
if (boundCard) boundCard.hidden = false;
updateSubmitState();
}
async function fetchBalance() {
try {
const data = await requestJSON("/wallet/bank/balance");
const body = data.body ?? data.data ?? 0;
renderBalance(typeof body === "number" ? body : body.currentSalary ?? body.availableBalance ?? body.balance ?? 0);
} catch (error) {
console.error("Failed to load bank balance:", error);
renderBalance(0);
showToast("load_balance_failed", error.message);
}
}
async function fetchBankCards() {
try {
const data = await requestJSON("/wallet/bank-card/list");
const cards = Array.isArray(data.body) ? data.body : Array.isArray(data.data) ? data.data : [];
renderBankCard(cards.find((item) => item.use));
} catch (error) {
console.error("Failed to load bank card:", error);
renderBankCard(null);
showToast("load_bank_card_failed", error.message);
}
}
function normalizeAmountInput() {
if (!amountInput) return;
const raw = String(amountInput.value || "").replace(/[^\d.]/g, "");
const parts = raw.split(".");
let nextValue = parts.length > 2 ? `${parts[0]}.${parts.slice(1).join("")}` : raw;
const amount = Number(nextValue);
if (Number.isFinite(amount) && amount > availableBalance) nextValue = formatMoney(availableBalance);
amountInput.value = nextValue;
updateSubmitState();
}
function validateWithdraw() {
const amount = activeAmount();
if (!selectedBankCard?.id) return "missing_bank_card";
if (!amount || amount <= 0) return "invalid_amount";
if (amount < 50) return "amount_too_low";
if (amount > availableBalance) return "amount_exceeds_balance";
return "";
}
async function submitWithdraw() {
if (currentMode !== "withdraw") {
showToast("exchange_unavailable");
return;
}
const errorKey = validateWithdraw();
if (errorKey) {
showToast(errorKey);
return;
}
isSubmitting = true;
updateSubmitState();
try {
await requestJSON("/wallet/bank/withdraw", {
method: "POST",
body: {
amount: activeAmount(),
acceptBankCardId: selectedBankCard.id,
salaryType: "HOST_SALARY"
}
});
showToast("withdraw_success");
await fetchBalance();
} catch (error) {
console.error("Failed to submit withdrawal:", error);
showToast(error.message || message("withdraw_failed", "Withdrawal failed"));
} finally {
isSubmitting = false;
updateSubmitState();
}
}
function syncBackFallback() {
document.querySelector(".back-button")?.addEventListener("click", () => {
if (window.history.length > 1) {
window.history.back();
return;
}
const nextParams = new URLSearchParams(params);
const token = readRawParam("token");
if (token) nextParams.set("token", token);
if (currentLang) nextParams.set("lang", currentLang);
const query = nextParams.toString();
window.location.href = `../host-center/index.html${query ? `?${query}` : ""}`;
});
}
function setLanguageMenuOpen(open) {
const languageButton = document.querySelector(".language-button");
const languageMenu = document.querySelector(".language-menu");
if (!languageButton || !languageMenu) return;
languageMenu.hidden = !open;
languageButton.setAttribute("aria-expanded", String(open));
}
document.querySelector(".language-button")?.addEventListener("click", (event) => {
event.stopPropagation();
const languageMenu = document.querySelector(".language-menu");
setLanguageMenuOpen(Boolean(languageMenu?.hidden));
});
document.querySelectorAll(".language-menu [data-lang]").forEach((button) => {
button.addEventListener("click", (event) => {
event.stopPropagation();
setLanguageMenuOpen(false);
applyLanguage(button.dataset.lang);
});
});
document.addEventListener("click", () => {
setLanguageMenuOpen(false);
});
document.querySelectorAll(".mode-tab").forEach((tab) => {
tab.addEventListener("click", () => setMode(tab.dataset.mode));
});
document.querySelector(".text-action")?.addEventListener("click", () => {
if (amountInput) amountInput.value = formatMoney(availableBalance);
updateSubmitState();
});
amountInput?.addEventListener("input", normalizeAmountInput);
submitButton?.addEventListener("click", submitWithdraw);
syncBackFallback();
applyLanguage(currentLang);
setMode("withdraw");
fetchBalance();
fetchBankCards();
})();

View File

@ -0,0 +1,729 @@
:root {
--page-bg: #f6f7f7;
--card-bg: rgba(255, 255, 255, 0.96);
--text: #25282e;
--muted: #8f9196;
--soft: #f1f3f3;
--line: #eef0f2;
--mint: #43e7d8;
--mint-dark: #16b9ac;
--gold: #e3b55b;
--shadow: 0 8px 22px rgba(36, 46, 60, 0.055);
}
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
min-height: 100%;
margin: 0;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
}
body {
display: flex;
justify-content: center;
background: #111;
color: var(--text);
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
button,
input,
select {
font: inherit;
}
button {
border: 0;
padding: 0;
background: transparent;
color: inherit;
}
input,
select {
width: 100%;
border: 0;
outline: 0;
background: transparent;
color: var(--text);
}
.wallet-page {
position: relative;
width: 100%;
max-width: 430px;
min-height: 100vh;
overflow: hidden auto;
background: var(--page-bg);
}
.hero-bg {
position: absolute;
inset: 0 0 auto;
height: 215px;
background:
radial-gradient(circle at 72% 4%, rgba(209, 255, 248, 0.98) 0, rgba(209, 255, 248, 0.88) 23%, rgba(209, 255, 248, 0) 48%),
radial-gradient(circle at 14% 20%, rgba(185, 250, 244, 0.78) 0, rgba(185, 250, 244, 0) 35%),
linear-gradient(151deg, #ccf7ef 0%, #ebfffa 58%, rgba(242, 255, 252, 0.7) 100%);
clip-path: polygon(0 0, 100% 0, 100% 76%, 70% 47%, 40% 58%, 0 78%);
}
.status-bar,
.title-bar,
.content,
.home-indicator,
.toast {
position: relative;
z-index: 1;
}
.status-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 47px;
padding: 18px 29px 0 38px;
color: #11161c;
font-size: 15px;
font-weight: 800;
}
.status-icons {
display: flex;
align-items: center;
gap: 6px;
}
.cellular {
display: flex;
align-items: flex-end;
gap: 2px;
width: 18px;
height: 12px;
}
.cellular i {
width: 3px;
border-radius: 2px;
background: #11161c;
}
.cellular i:nth-child(1) {
height: 4px;
}
.cellular i:nth-child(2) {
height: 6px;
}
.cellular i:nth-child(3) {
height: 9px;
}
.cellular i:nth-child(4) {
height: 11px;
}
.wifi {
position: relative;
width: 18px;
height: 13px;
}
.wifi i {
position: absolute;
left: 50%;
border: 2px solid #11161c;
border-bottom: 0;
border-left-color: transparent;
border-right-color: transparent;
border-radius: 12px 12px 0 0;
transform: translateX(-50%);
}
.wifi i:nth-child(1) {
top: 1px;
width: 16px;
height: 8px;
}
.wifi i:nth-child(2) {
top: 5px;
width: 10px;
height: 5px;
}
.wifi i:nth-child(3) {
top: 10px;
width: 4px;
height: 4px;
border: 0;
border-radius: 50%;
background: #11161c;
}
.battery {
position: relative;
width: 24px;
height: 12px;
border: 2px solid #11161c;
border-radius: 3px;
}
.battery::after {
content: "";
position: absolute;
top: 3px;
right: -4px;
width: 2px;
height: 4px;
border-radius: 0 2px 2px 0;
background: #11161c;
}
.battery i {
display: block;
width: 16px;
height: 100%;
background: #11161c;
}
.title-bar {
display: grid;
grid-template-columns: 52px 1fr 52px;
align-items: center;
height: 60px;
padding: 0 14px;
direction: ltr;
}
.title-bar h1 {
margin: 0;
color: #24282e;
font-size: 21px;
font-weight: 900;
line-height: 1;
text-align: center;
}
.back-button {
display: flex;
align-items: center;
justify-content: flex-start;
width: 44px;
height: 44px;
}
.back-button svg {
width: 28px;
height: 28px;
}
.back-button path {
fill: none;
stroke: #24282e;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 3;
}
.language-switcher {
position: relative;
justify-self: end;
}
.language-button {
justify-self: end;
display: flex;
align-items: center;
justify-content: center;
width: 42px;
height: 30px;
border: 1px solid rgba(35, 40, 46, 0.12);
border-radius: 15px;
background: rgba(255, 255, 255, 0.68);
color: #25282e;
font-size: 13px;
font-weight: 900;
box-shadow: 0 4px 12px rgba(43, 118, 107, 0.08);
}
.language-menu {
position: absolute;
top: calc(100% + 7px);
right: 0;
display: grid;
gap: 2px;
min-width: 58px;
padding: 5px;
border: 1px solid rgba(35, 40, 46, 0.1);
border-radius: 8px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 10px 26px rgba(36, 46, 60, 0.14);
}
.language-menu button {
min-height: 28px;
border-radius: 6px;
color: #25282e;
font-size: 12px;
font-weight: 900;
}
.language-menu button.is-active {
background: #e6fbf8;
color: #0e8f82;
}
.language-menu[hidden] {
display: none;
}
.content {
display: flex;
flex-direction: column;
gap: 14px;
padding: 8px 14px 92px;
}
.balance-card,
.mode-card,
.card-section,
.bind-panel {
width: 100%;
border-radius: 8px;
background: var(--card-bg);
box-shadow: var(--shadow);
}
.balance-card {
padding: 20px 24px 19px;
}
.balance-label {
color: var(--muted);
font-size: 14px;
font-weight: 800;
}
.balance-value {
display: flex;
align-items: center;
margin-top: 10px;
color: #23272d;
font-size: 34px;
font-weight: 950;
line-height: 1.05;
}
.balance-currency {
flex: 0 0 auto;
margin-right: 6px;
color: var(--gold);
font-size: 28px;
font-weight: 950;
line-height: 1;
}
.mode-card {
padding: 16px;
}
.mode-tabs {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
padding: 4px;
border-radius: 8px;
background: #eef1f1;
}
.mode-tab {
min-height: 40px;
border-radius: 7px;
color: #777a80;
font-size: 15px;
font-weight: 900;
}
.mode-tab.active {
background: #fff;
color: var(--text);
box-shadow: 0 5px 14px rgba(38, 51, 62, 0.08);
}
.mode-panel {
display: none;
margin-top: 16px;
}
.mode-panel.active {
display: block;
}
.field {
display: block;
}
.field > span {
display: block;
margin-bottom: 8px;
color: #4a4d53;
font-size: 14px;
font-weight: 850;
}
.field-control,
.field input,
.field select {
min-height: 48px;
border-radius: 8px;
background: #f4f6f6;
}
.field input,
.field select {
padding: 0 14px;
}
.amount-control {
display: flex;
align-items: center;
padding: 0 14px;
}
.amount-control span {
flex: 0 0 auto;
margin-right: 9px;
color: var(--gold);
font-size: 21px;
font-weight: 950;
}
.amount-control input {
min-height: 0;
padding: 0;
background: transparent;
font-size: 23px;
font-weight: 900;
}
.hint-row,
.exchange-preview {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10px;
color: var(--muted);
font-size: 13px;
font-weight: 800;
}
.text-action {
color: var(--mint-dark);
font-weight: 950;
}
.exchange-preview strong {
color: #24282e;
}
.card-section,
.bind-panel {
padding: 18px 16px;
}
.section-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.section-title {
color: #24282e;
font-size: 17px;
font-weight: 900;
}
.section-subtitle {
margin-top: 4px;
color: var(--muted);
font-size: 13px;
font-weight: 750;
}
.bind-address-button,
.change-address {
flex: 0 0 auto;
min-height: 34px;
padding: 0 13px;
border-radius: 17px;
background: rgba(67, 231, 216, 0.14);
color: var(--mint-dark);
font-size: 13px;
font-weight: 950;
}
.empty-address-state {
display: flex;
align-items: center;
gap: 13px;
min-height: 76px;
margin-top: 16px;
padding: 13px;
border: 1px dashed #d9dddd;
border-radius: 8px;
background: #fbfcfc;
}
.empty-icon {
display: flex;
align-items: center;
justify-content: center;
width: 42px;
height: 42px;
flex: 0 0 42px;
border-radius: 50%;
background: #edf8f7;
}
.empty-icon svg {
width: 24px;
height: 24px;
}
.empty-icon path {
fill: none;
stroke: var(--mint-dark);
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2.2;
}
.empty-title {
color: #292d33;
font-size: 15px;
font-weight: 900;
}
.empty-copy {
margin-top: 3px;
color: var(--muted);
font-size: 12px;
font-weight: 750;
line-height: 1.25;
}
.bound-address-card {
display: flex;
align-items: center;
gap: 14px;
min-height: 88px;
margin-top: 16px;
padding: 14px;
border-radius: 8px;
background: linear-gradient(135deg, #1e252d 0%, #354858 100%);
color: #fff;
}
.network-badge {
display: flex;
align-items: center;
justify-content: center;
width: 56px;
height: 32px;
flex: 0 0 56px;
border-radius: 7px;
background: linear-gradient(135deg, #ffe58b 0%, #c8942e 100%);
color: #1f252c;
font-size: 12px;
font-weight: 950;
}
.bound-copy {
flex: 1 1 auto;
min-width: 0;
}
.bound-network {
font-size: 15px;
font-weight: 900;
}
.bound-address {
margin-top: 5px;
color: rgba(255, 255, 255, 0.82);
font-size: 14px;
font-weight: 850;
letter-spacing: 0;
}
.bound-meta {
margin-top: 4px;
color: rgba(255, 255, 255, 0.62);
font-size: 12px;
font-weight: 750;
}
.change-address {
background: rgba(255, 255, 255, 0.18);
color: #fff;
}
.bind-panel {
display: block;
}
.address-form {
display: flex;
flex-direction: column;
gap: 13px;
margin-top: 14px;
}
.network-field {
display: flex;
align-items: center;
gap: 9px;
min-height: 48px;
padding: 0 14px;
border-radius: 8px;
background: #f4f6f6;
}
.network-field strong {
color: #25282e;
font-size: 15px;
font-weight: 950;
}
.network-field span {
padding: 4px 8px;
border-radius: 999px;
background: rgba(67, 231, 216, 0.16);
color: var(--mint-dark);
font-size: 12px;
font-weight: 950;
}
.network-note {
margin-top: -2px;
color: var(--muted);
font-size: 12px;
font-weight: 750;
line-height: 1.35;
}
.form-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 2px;
}
.secondary-button,
.primary-button,
.submit-button {
min-height: 48px;
border-radius: 24px;
font-size: 16px;
font-weight: 950;
}
.secondary-button {
background: #eef1f1;
color: #55585f;
}
.primary-button,
.submit-button {
background: linear-gradient(135deg, #67f4e8 0%, #27d3c4 100%);
color: #11302d;
}
.submit-button {
width: 100%;
box-shadow: 0 8px 20px rgba(29, 206, 193, 0.22);
}
.submit-button:disabled {
background: #d7dada;
color: #8b8e94;
box-shadow: none;
}
.toast {
position: fixed;
left: 50%;
bottom: 34px;
transform: translateX(-50%);
padding: 10px 14px;
border-radius: 999px;
background: rgba(24, 29, 34, 0.92);
color: #fff;
font-size: 13px;
font-weight: 850;
}
.home-indicator {
position: fixed;
left: 50%;
bottom: 8px;
width: 154px;
height: 5px;
border-radius: 999px;
background: #050506;
transform: translateX(-50%);
}
[hidden] {
display: none !important;
}
[dir="rtl"] .content {
direction: rtl;
}
[dir="rtl"] .balance-currency,
[dir="rtl"] .amount-control span {
margin-right: 0;
margin-left: 9px;
}
[dir="rtl"] .toast,
[dir="rtl"] .home-indicator {
direction: ltr;
}
@media (max-width: 360px) {
.status-bar {
padding-right: 22px;
padding-left: 31px;
}
.content {
padding-right: 12px;
padding-left: 12px;
}
.balance-card {
padding-right: 18px;
padding-left: 18px;
}
.balance-value {
font-size: 29px;
}
.title-bar h1 {
font-size: 19px;
}
}