优化host center

This commit is contained in:
170-carry 2026-04-28 11:02:31 +08:00
parent 29eec35eb0
commit 2507eae749
12 changed files with 1813 additions and 227 deletions

View File

@ -6,7 +6,7 @@
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
/>
<title>Invite Gift</title>
<title>Yumi Chat Party - Invite Friends to Get Golds</title>
<style>
:root {
--page-width: 420px;

View File

@ -192,7 +192,7 @@ function formatSignedReward(value) {
function render() {
els.title.innerHTML = `Your friend [${escapeHtml(state.friendName)}]<br />has invited you to join`;
els.avatar.src = trimValue(state.inviterAvatar) || DEFAULT_AVATAR;
els.giftRewardAmount.textContent = formatSignedReward(state.giftReward);
// els.giftRewardAmount.textContent = formatSignedReward(state.giftReward);
}
function applyLandingData(body) {

View File

@ -41,22 +41,6 @@
<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>

View File

@ -190,6 +190,43 @@
- 显式查询参数:无。
- 返回中使用 `body.teamProfile || body` 作为团队信息。
### GET `/team/release/policy`
- 用途:获取团队政策。
- 查询参数:
- `id`: 团队 ID必传。线上 Java 接口传 `teamId` 会返回 `REQUEST_PARAMETER_ERROR: id required.`
- 团队 ID 来源:
- 优先使用 `/team/entry` 返回的 `body.teamProfile.id`
- `/team/team-own/profile` 返回的 `body.id` 是团长/用户资料 ID不是团队政策接口需要的团队 ID直接传给 `/team/release/policy?id=...` 会返回 `TEAM_NOT_FOUND`
- 页面 URL 可以继续带 `teamId` 作为本地跳转参数,但真正请求 Java 接口时必须映射为 `id=<teamProfile.id>`
- 返回使用:
- `body.policy[]`
- `policy[].level`
- `policy[].onlineTime`
- `policy[].target`
- `policy[].memberSalary`
- `policy[].ownSalary`
- `policy[].totalSalary`
### GET `/team/search-account`
- 用途:加入代理页搜索代理账号。
- 查询参数:
- `account`: 用户输入的代理 ID/账号,必传
- 原 Vue `ApplyView` 使用:
- 请求后判断 `body.teamProfile`
- `body.teamProfile.id` 作为加入申请的 `teamId`
### GET `/team/wait-apply/profile`
- 用途:获取当前用户待处理的加入代理申请。
- 显式查询参数:无。
- 原 Vue `ApplyView` 使用:
- 有返回 `body` 时显示一条 pending 申请记录
- 展示 `body.ownUserProfile.userAvatar`
- 展示 `body.ownUserProfile.userNickname`
- 展示 `body.ownUserProfile.ownSpecialId.account || body.ownUserProfile.account`
### GET `/wallet/bank/balance`
- 用途:获取工资/银行卡余额。
@ -226,6 +263,25 @@
- 返回使用:
- `body[]`
- `item.use`
- `item.cardType`
- `item.cardNo`
- `item.payee`
### POST `/wallet/bank-card/add`
- 用途:新增收款方式。
- Body
- `userId`: 当前用户 ID
- `cardType`: `USDT | BANK`
- `cardName`: 收款方式名称USDT 时原 Vue 固定为 `USDT`
- `cardNo`: 卡号或 USDT 地址
- `payee`: 收款人
### POST `/wallet/bank-card/use`
- 用途:设置当前使用的收款方式。
- Body
- `id`: 收款方式 ID
### GET `/wallet/withdraw-info/list`
@ -242,6 +298,39 @@
- 返回使用:
- `body` boolean
### GET `/wallet/bank/check/exchange-gold`
- 用途:检查当前用户是否允许兑换金币。
- 显式查询参数:无。
- 原 Vue `ExchangeGoldCoins` 用它做兑换入口权限检查。
### POST `/wallet/bank/exchange/gold`
- 用途:把当前可用余额兑换金币。
- Body原 Vue 主播兑换页):
- `coinId`: 金币档位 ID
- `amount`: 美元金额
- `price`: 美元金额
- Body原静态逻辑页
- `amount`: 美元金额
- 兑换比例未发现后端接口返回;原 Vue 金币档位是前端常量。
### GET `/wallet/bank/search/user-profile`
- 用途:转账前搜索收款用户。
- 查询参数:
- `account`: 用户输入的账号/ID
- `type`: 原 Vue 传 `AGENT`
- `searchType`: 转账场景传 `TRANSFER`
### POST `/wallet/bank/transferGold`
- 用途:把当前可用余额转给其它用户。
- Body
- `amount`: 美元金额
- `acceptUserId`: 收款用户 ID
- `acceptMethod`: 原 Vue 主播转账传 `BANK_TRANSFER`
### GET `/app/h5/identity`
- 用途:获取当前 H5 身份。
@ -264,3 +353,12 @@
- Host Center 退出团队使用:
- `reason: "QUIT"`
- `teamId: <当前团队 id>`
- ApplyView 加入代理使用:
- `reason: "JOIN"`
- `teamId: <GET /team/search-account 返回的 body.teamProfile.id>`
### POST `/team/user/join-apply-cancel`
- 用途:撤销待处理的加入代理申请。
- Body
- `id`: 申请记录 `messageId`,必传

View File

@ -17,6 +17,7 @@
policy_loading: "Loading policy...",
policy_empty: "No policy data",
policy_missing_token: "Missing token",
policy_missing_team: "Missing team ID",
policy_load_failed: "Failed to load policy",
policy_level: "Level",
policy_online: "Online (Hour)",
@ -34,6 +35,7 @@
policy_loading: "جار تحميل السياسة...",
policy_empty: "لا توجد بيانات سياسة",
policy_missing_token: "رمز الدخول مفقود",
policy_missing_team: "معرّف الفريق مفقود",
policy_load_failed: "فشل تحميل السياسة",
policy_level: "المستوى",
policy_online: "الاتصال (ساعة)",
@ -51,6 +53,7 @@
policy_loading: "Politika yükleniyor...",
policy_empty: "Politika verisi yok",
policy_missing_token: "Token eksik",
policy_missing_team: "Takım ID eksik",
policy_load_failed: "Politika yüklenemedi",
policy_level: "Seviye",
policy_online: "Online (Saat)",
@ -68,6 +71,7 @@
policy_loading: "Memuat kebijakan...",
policy_empty: "Tidak ada data kebijakan",
policy_missing_token: "Token tidak ditemukan",
policy_missing_team: "ID tim tidak ditemukan",
policy_load_failed: "Gagal memuat kebijakan",
policy_level: "Level",
policy_online: "Online (Jam)",
@ -163,6 +167,24 @@
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 renderPolicyStatus(key, fallback) {
policyStatusKey = key;
const status = document.querySelector("#policyStatus");
@ -243,6 +265,53 @@
return row;
}
function pickTeamId(entry) {
return entry?.teamProfile?.id || entry?.teamId || entry?.id || "";
}
async function fetchTeamEntryId(token) {
try {
const url = new URL("/team/entry", `${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}`);
}
return pickTeamId(data.body || data.data || {});
} catch (error) {
console.error("Failed to resolve team ID:", error);
return "";
}
}
async function resolveTeamId(token, forceTeamEntry = false) {
const directTeamId = params.get("teamId") || params.get("team_id") || params.get("TEAM_ID");
if (directTeamId && !forceTeamEntry) return directTeamId;
return fetchTeamEntryId(token);
}
async function requestTeamPolicy(token, teamId) {
const url = new URL("/team/release/policy", `${apiBaseURL()}/`);
url.searchParams.set("id", teamId);
const response = await fetch(url.toString(), {
cache: "no-store",
headers: authHeadersFromToken(token)
});
const data = await response.json().catch(() => ({}));
if (!response.ok || data.status === false) {
const error = new Error(data.errorMsg || data.message || `Request failed: ${response.status}`);
error.response = data;
throw error;
}
return data.body || data.data || {};
}
function renderAnchorPolicy(policy) {
const rows = Array.isArray(policy?.policy) ? policy.policy : [];
const list = document.querySelector("#policyList");
@ -274,17 +343,20 @@
}
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}`);
const teamId = await resolveTeamId(token);
if (!teamId) {
renderPolicyStatus("policy_missing_team");
return;
}
try {
currentPolicy = await requestTeamPolicy(token, teamId);
} catch (error) {
const fallbackTeamId = await resolveTeamId(token, true);
if (!fallbackTeamId || fallbackTeamId === teamId) throw error;
currentPolicy = await requestTeamPolicy(token, fallbackTeamId);
}
currentPolicy = data.body || data.data || {};
renderAnchorPolicy(currentPolicy);
} catch (error) {
console.error("Failed to load anchor policy:", error);

View File

@ -123,6 +123,9 @@
let currentLang = normalizeLanguage(params.get("lang"));
let currentMessages = fallbackMessages[currentLang] || fallbackMessages.en;
let currentProfile = null;
let currentAgencyProfile = null;
let currentTeamId = "";
let currentIdentity = null;
let currentSalary = "0.00";
let currentStreamingDuration = "0";
let nextTargetValue = "-";
@ -224,13 +227,6 @@
};
}
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";
@ -245,6 +241,89 @@
return profile?.ownSpecialId?.account || profile?.account || profile?.id || "1234567890";
}
function agencyPrefix(messages) {
const template = messages.agency || fallbackMessages.en.agency;
const marker = "Bilibili(101013)";
return template.includes(marker) ? template.replace(marker, "").trim() : "Agency:";
}
function pickAgencyName(profile) {
return profile?.userNickname || profile?.name || profile?.teamName || "-";
}
function pickAgencyAccount(profile) {
return profile?.ownSpecialId?.account || profile?.account || profile?.id || "";
}
function formatAgencyText(profile, messages = currentMessages) {
const prefix = agencyPrefix(messages);
if (!profile) return `${prefix} -`;
const name = pickAgencyName(profile);
const account = pickAgencyAccount(profile);
return account ? `${prefix} ${name}(${account})` : `${prefix} ${name}`;
}
function renderAgencyProfile(profile, messages = currentMessages) {
const agencyButton = document.querySelector(".agency");
const agencyTextNode = document.querySelector(".agency span:first-child");
if (!agencyButton || !agencyTextNode) return;
agencyTextNode.textContent = formatAgencyText(profile, messages);
agencyButton.dataset.teamId = String(currentTeamId || pickAgencyTeamId(profile) || "");
agencyButton.dataset.account = String(profile?.account || "");
agencyButton.dataset.specialAccount = String(profile?.ownSpecialId?.account || "");
syncPolicyLink();
}
function pickAgencyTeamId(profile) {
return profile?.teamId || profile?.teamProfile?.id || profile?.id || "";
}
function pickTeamEntryId(entry) {
return entry?.teamProfile?.id || entry?.teamId || entry?.id || "";
}
function removeAvatarFallback() {
document.querySelector("#profileAvatarFallback")?.remove();
}
function ensureAvatarFallback(displayName) {
const avatarShell = document.querySelector(".avatar-shell");
if (!avatarShell) return null;
let avatarFallback = document.querySelector("#profileAvatarFallback");
if (avatarFallback) {
avatarFallback.hidden = false;
avatarFallback.setAttribute("aria-label", `${displayName} avatar`);
return avatarFallback;
}
const template = document.createElement("template");
template.innerHTML = `
<svg class="avatar-fallback" id="profileAvatarFallback" viewBox="0 0 72 72" role="img">
<defs>
<linearGradient id="avatarBg" x1="12" y1="8" x2="60" y2="64">
<stop stop-color="#5AF6E4"></stop>
<stop offset="1" stop-color="#D9FFF5"></stop>
</linearGradient>
</defs>
<circle cx="36" cy="36" r="35" fill="url(#avatarBg)"></circle>
<path d="M22 30 20 18l10 7M50 30l2-12-10 7" fill="#101820"></path>
<path d="M20 31c3-9 28-9 32 0 4 10-3 21-16 21S16 41 20 31Z" fill="#F7FBF7"></path>
<circle cx="29" cy="36" r="4" fill="#101820"></circle>
<circle cx="46" cy="36" r="4" fill="#101820"></circle>
<circle cx="25" cy="41" r="5" fill="#F0B64A" opacity=".9"></circle>
<path d="M35 42h3l-1.5 2Z" fill="#101820"></path>
<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"></path>
</svg>
`;
avatarFallback = template.content.firstElementChild;
avatarFallback.setAttribute("aria-label", `${displayName} avatar`);
avatarShell.appendChild(avatarFallback);
return avatarFallback;
}
function renderProfile(profile, messages = currentMessages) {
if (!profile) return;
@ -252,7 +331,6 @@
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);
@ -265,29 +343,59 @@
if (nameNode) nameNode.textContent = displayName;
if (uidNode) uidNode.textContent = `${profileUidPrefix(messages)} ${displayAccount}`.trim();
if (!avatarImage || !avatarFallback) return;
if (!avatarImage) 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.onerror = null;
avatarImage.removeAttribute("src");
ensureAvatarFallback(displayName);
return;
}
avatarImage.hidden = false;
avatarFallback.hidden = true;
removeAvatarFallback();
avatarImage.onerror = () => {
avatarImage.hidden = true;
avatarFallback.hidden = false;
avatarImage.removeAttribute("src");
ensureAvatarFallback(displayName);
};
avatarImage.src = avatar;
}
function hasOwn(object, key) {
return Boolean(object && Object.prototype.hasOwnProperty.call(object, key));
}
function isTruthyIdentityValue(value) {
if (typeof value === "boolean") return value;
if (typeof value === "number") return value === 1;
if (typeof value === "string") return ["true", "1", "yes"].includes(value.toLowerCase());
return false;
}
function shouldOpenJoinAgency(identity) {
if (!hasOwn(identity, "anchor")) return false;
return !isTruthyIdentityValue(identity.anchor);
}
function buildPageURL(path) {
const url = new URL(path, window.location.href);
const nextParams = new URLSearchParams(params);
const token = readRawParam("token");
if (token) nextParams.set("token", token);
if (currentLang) nextParams.set("lang", currentLang);
url.search = nextParams.toString();
return url.toString();
}
function openJoinAgencyPage() {
window.location.replace(buildPageURL("../join-agency/index.html"));
}
function formatCurrentSalary(value) {
const numericValue = Number(value);
if (Number.isFinite(numericValue)) return numericValue.toFixed(2);
@ -407,6 +515,92 @@
}
}
async function fetchIdentity() {
const token = readRawParam("token");
if (!token) {
console.warn("Missing token for /app/h5/identity");
return null;
}
try {
const url = new URL("/app/h5/identity", `${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}`);
}
currentIdentity = data.body || data.data || {};
return currentIdentity;
} catch (error) {
console.error("Failed to load H5 identity:", error);
return null;
}
}
async function fetchAgencyProfile() {
const token = readRawParam("token");
if (!token) {
console.warn("Missing token for /team/team-own/profile");
renderAgencyProfile(null);
return null;
}
try {
const url = new URL("/team/team-own/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}`);
}
currentAgencyProfile = data.body || data.data || null;
renderAgencyProfile(currentAgencyProfile);
return currentAgencyProfile;
} catch (error) {
console.error("Failed to load agency profile:", error);
renderAgencyProfile(null);
return null;
}
}
async function fetchTeamEntry() {
const token = readRawParam("token");
if (!token) {
console.warn("Missing token for /team/entry");
return "";
}
try {
const url = new URL("/team/entry", `${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}`);
}
currentTeamId = pickTeamEntryId(data.body || data.data || {});
renderAgencyProfile(currentAgencyProfile);
syncPolicyLink();
return currentTeamId;
} catch (error) {
console.error("Failed to load team entry:", error);
return "";
}
}
async function fetchBankBalance() {
const token = readRawParam("token");
if (!token) {
@ -497,15 +691,26 @@
}
async function fetchAnchorPolicyForTarget() {
const token = readRawParam("token");
if (!token) {
console.warn("Missing token for /team/release/policy");
return null;
}
try {
const url = new URL("/app/h5/host-center/policy", `${apiBaseURL()}/`);
if (!appendTokenParam(url)) {
console.warn("Missing token for /app/h5/host-center/policy");
const teamId = currentTeamId || await fetchTeamEntry();
if (!teamId) {
console.warn("Missing teamId for /team/release/policy");
return null;
}
if (params.get("policyType")) url.searchParams.set("policyType", params.get("policyType"));
const response = await fetch(url.toString(), { cache: "no-store" });
const url = new URL("/team/release/policy", `${apiBaseURL()}/`);
url.searchParams.set("id", teamId);
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}`);
@ -513,7 +718,7 @@
return data.body || data.data || null;
} catch (error) {
console.error("Failed to load anchor policy for next target:", error);
console.error("Failed to load team policy for next target:", error);
return null;
}
}
@ -552,8 +757,10 @@
const nextParams = new URLSearchParams(params);
const token = readRawParam("token");
const teamId = currentTeamId;
if (token) nextParams.set("token", token);
if (currentLang) nextParams.set("lang", currentLang);
if (teamId) nextParams.set("teamId", teamId);
const query = nextParams.toString();
link.setAttribute("href", `./policy.html${query ? `?${query}` : ""}`);
@ -595,6 +802,7 @@
syncLocalizedLinks();
syncPolicyLink();
renderProfile(currentProfile, messages);
renderAgencyProfile(currentAgencyProfile, messages);
}
document.querySelector(".language-button")?.addEventListener("click", (event) => {
@ -619,7 +827,21 @@
if (window.history.length > 1) window.history.back();
});
applyLanguage(currentLang).then(() => {
document.querySelector(".agency")?.addEventListener("click", () => {
if (currentIdentity && shouldOpenJoinAgency(currentIdentity)) openJoinAgencyPage();
});
ensureAvatarFallback("Oscar");
applyLanguage(currentLang).then(async () => {
const identity = await fetchIdentity();
if (shouldOpenJoinAgency(identity)) {
openJoinAgencyPage();
return;
}
fetchAgencyProfile();
fetchTeamEntry();
fetchUserProfile().then((profile) => {
const memberId = profile?.id || profile?.account;
if (memberId) {

View File

@ -0,0 +1,90 @@
<!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>Join Agency</title>
<link rel="stylesheet" href="../host-center/style.css" />
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div class="host-center join-agency-page" aria-label="Join Agency" 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">Join Agency</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 join-content">
<section class="card join-search-card">
<label class="field-label" for="agencyAccount" data-i18n="agency_id">Agency ID</label>
<form class="search-form" id="searchForm">
<input id="agencyAccount" name="agencyAccount" type="text" inputmode="numeric" autocomplete="off" placeholder="Enter agency ID" data-i18n-placeholder="enter_agent_id" />
<button class="search-button" id="searchButton" type="submit" data-i18n="search">Search</button>
</form>
</section>
<div class="status-message" id="statusMessage" role="status" hidden></div>
<section class="card agency-result-card" id="resultCard" hidden>
<div class="section-title" data-i18n="agency">Agency</div>
<div class="agency-profile-row">
<div class="agency-avatar">
<img id="agencyAvatar" alt="" hidden />
<span id="agencyAvatarFallback">A</span>
</div>
<div class="agency-copy">
<div class="agency-name" id="agencyName">-</div>
<div class="agency-account" id="agencyAccountText">UID: -</div>
</div>
</div>
<button class="primary-action" id="applyButton" type="button" data-i18n="apply">Apply</button>
</section>
<section class="card pending-card" id="pendingCard" hidden>
<div class="pending-head">
<div class="section-title" data-i18n="application_records">Application Records</div>
<span class="pending-status" data-i18n="pending">Pending</span>
</div>
<div class="agency-profile-row">
<div class="agency-avatar">
<img id="pendingAvatar" alt="" hidden />
<span id="pendingAvatarFallback">A</span>
</div>
<div class="agency-copy">
<div class="agency-name" id="pendingName">-</div>
<div class="agency-account" id="pendingAccount">UID: -</div>
</div>
</div>
<button class="secondary-action" id="cancelButton" type="button" data-i18n="cancel">Cancel</button>
</section>
</main>
<div class="home-indicator" aria-hidden="true"></div>
</div>
<script src="./script.js" defer></script>
</body>
</html>

View File

@ -0,0 +1,529 @@
(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: "Join Agency",
page_label: "Join Agency",
back: "Back",
title: "Join Agency",
language_button_aria: "Change language",
agency_id: "Agency ID",
enter_agent_id: "Enter agency ID",
search: "Search",
searching: "Searching...",
apply: "Apply",
applying: "Applying...",
agency: "Agency",
uid_prefix: "UID:",
account_not_found: "Account not found or not an agency",
missing_token: "Missing token",
found: "Agency found",
application_submitted: "Application submitted",
application_failed: "Application failed",
application_records: "Application Records",
pending: "Pending",
cancel: "Cancel",
cancelling: "Cancelling...",
cancellation_failed: "Cancellation failed",
application_cancelled: "Application cancelled",
network_error: "Network error"
},
ar: {
document_title: "الانضمام إلى وكالة",
page_label: "الانضمام إلى وكالة",
back: "رجوع",
title: "الانضمام إلى وكالة",
language_button_aria: "تغيير اللغة",
agency_id: "معرّف الوكالة",
enter_agent_id: "أدخل معرّف الوكالة",
search: "بحث",
searching: "جار البحث...",
apply: "تقديم",
applying: "جار التقديم...",
agency: "الوكالة",
uid_prefix: "UID:",
account_not_found: "لم يتم العثور على وكالة",
missing_token: "رمز الدخول مفقود",
found: "تم العثور على الوكالة",
application_submitted: "تم إرسال الطلب",
application_failed: "فشل الطلب",
application_records: "سجلات الطلب",
pending: "قيد الانتظار",
cancel: "إلغاء",
cancelling: "جار الإلغاء...",
cancellation_failed: "فشل الإلغاء",
application_cancelled: "تم إلغاء الطلب",
network_error: "خطأ في الشبكة"
},
tr: {
document_title: "Ajansa Katıl",
page_label: "Ajansa Katıl",
back: "Geri",
title: "Ajansa Katıl",
language_button_aria: "Dili değiştir",
agency_id: "Ajans ID",
enter_agent_id: "Ajans ID gir",
search: "Ara",
searching: "Aranıyor...",
apply: "Başvur",
applying: "Başvuruluyor...",
agency: "Ajans",
uid_prefix: "UID:",
account_not_found: "Hesap bulunamadı veya ajans değil",
missing_token: "Token eksik",
found: "Ajans bulundu",
application_submitted: "Başvuru gönderildi",
application_failed: "Başvuru başarısız",
application_records: "Başvuru Kayıtları",
pending: "Beklemede",
cancel: "İptal",
cancelling: "İptal ediliyor...",
cancellation_failed: "İptal başarısız",
application_cancelled: "Başvuru iptal edildi",
network_error: "Ağ hatası"
},
id: {
document_title: "Gabung Agensi",
page_label: "Gabung Agensi",
back: "Kembali",
title: "Gabung Agensi",
language_button_aria: "Ubah bahasa",
agency_id: "ID Agensi",
enter_agent_id: "Masukkan ID agensi",
search: "Cari",
searching: "Mencari...",
apply: "Ajukan",
applying: "Mengajukan...",
agency: "Agensi",
uid_prefix: "UID:",
account_not_found: "Akun tidak ditemukan atau bukan agensi",
missing_token: "Token tidak ditemukan",
found: "Agensi ditemukan",
application_submitted: "Pengajuan dikirim",
application_failed: "Pengajuan gagal",
application_records: "Riwayat Pengajuan",
pending: "Menunggu",
cancel: "Batal",
cancelling: "Membatalkan...",
cancellation_failed: "Gagal membatalkan",
application_cancelled: "Pengajuan dibatalkan",
network_error: "Kesalahan jaringan"
}
};
const params = readURLParams();
let currentLang = normalizeLanguage(params.get("lang"));
let currentMessages = fallbackMessages[currentLang] || fallbackMessages.en;
let foundAgency = null;
let pendingRecord = null;
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 t(key) {
return currentMessages[key] || fallbackMessages.en[key] || key;
}
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"
};
}
async function apiRequest(path, options = {}) {
const token = readRawParam("token");
if (!token) throw new Error(t("missing_token"));
const method = options.method || "GET";
const headers = authHeadersFromToken(token);
const request = {
method,
cache: "no-store",
headers
};
if (options.body) {
headers["Content-Type"] = "application/json";
request.body = JSON.stringify(options.body);
}
const response = await fetch(new URL(path, `${apiBaseURL()}/`).toString(), request);
const data = await response.json().catch(() => ({}));
if (!response.ok || data.status === false) {
const error = new Error(data.errorMsg || data.message || `Request failed: ${response.status}`);
error.response = data;
throw error;
}
return data.body ?? data.data ?? null;
}
async function loadMessages(lang) {
return fallbackMessages[normalizeLanguage(lang)] || fallbackMessages.en;
}
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);
});
document.querySelectorAll("[data-i18n-placeholder]").forEach((node) => {
const value = messages[node.dataset.i18nPlaceholder];
if (value) node.setAttribute("placeholder", value);
});
}
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 || "Join Agency";
renderMessages(messages);
renderLanguageControls(messages);
renderFoundAgency(foundAgency);
renderPendingRecord(pendingRecord);
}
function showMessage(message, type = "") {
const node = document.querySelector("#statusMessage");
if (!node) return;
node.textContent = message || "";
node.hidden = !message;
node.classList.toggle("is-error", type === "error");
node.classList.toggle("is-success", type === "success");
}
function pickProfile(body) {
return body?.teamProfile || body?.ownUserProfile || body?.userProfile || body || null;
}
function pickName(profile) {
return profile?.userNickname || profile?.name || profile?.teamName || "-";
}
function pickAccount(profile) {
return profile?.ownSpecialId?.account || profile?.account || profile?.id || "-";
}
function initials(name) {
const value = String(name || "A").trim();
return value ? value.slice(0, 1).toUpperCase() : "A";
}
function renderAvatar(profile, imageSelector, fallbackSelector) {
const image = document.querySelector(imageSelector);
const fallback = document.querySelector(fallbackSelector);
if (!image || !fallback) return;
const name = pickName(profile);
const avatar = profile?.userAvatar || profile?.avatar || "";
fallback.textContent = initials(name);
image.alt = name;
if (!avatar) {
image.hidden = true;
image.removeAttribute("src");
fallback.hidden = false;
return;
}
fallback.hidden = true;
image.hidden = false;
image.onerror = () => {
image.hidden = true;
image.removeAttribute("src");
fallback.hidden = false;
};
image.src = avatar;
}
function syncApplyAvailability() {
const button = document.querySelector("#applyButton");
if (button) button.hidden = Boolean(pendingRecord);
}
function renderFoundAgency(profile) {
const card = document.querySelector("#resultCard");
if (!card) return;
if (!profile) {
card.hidden = true;
syncApplyAvailability();
return;
}
card.hidden = false;
document.querySelector("#agencyName").textContent = pickName(profile);
document.querySelector("#agencyAccountText").textContent = `${t("uid_prefix")} ${pickAccount(profile)}`.trim();
renderAvatar(profile, "#agencyAvatar", "#agencyAvatarFallback");
syncApplyAvailability();
}
function renderPendingRecord(record) {
const card = document.querySelector("#pendingCard");
if (!card) return;
if (!record) {
card.hidden = true;
syncApplyAvailability();
return;
}
const profile = pickProfile(record);
card.hidden = false;
document.querySelector("#pendingName").textContent = pickName(profile);
document.querySelector("#pendingAccount").textContent = `${t("uid_prefix")} ${pickAccount(profile)}`.trim();
renderAvatar(profile, "#pendingAvatar", "#pendingAvatarFallback");
syncApplyAvailability();
}
function errorMessage(error, fallbackKey) {
return error?.response?.errorMsg || error?.response?.message || error?.message || t(fallbackKey);
}
async function searchAgency() {
const input = document.querySelector("#agencyAccount");
const button = document.querySelector("#searchButton");
const account = input?.value.trim() || "";
if (!account) {
showMessage(t("enter_agent_id"), "error");
return;
}
button.disabled = true;
button.textContent = t("searching");
showMessage("");
try {
const body = await apiRequest(`/team/search-account?account=${encodeURIComponent(account)}`);
const profile = body?.teamProfile || null;
if (!profile?.id) {
foundAgency = null;
renderFoundAgency(null);
showMessage(t("account_not_found"), "error");
return;
}
foundAgency = profile;
renderFoundAgency(foundAgency);
showMessage(t("found"), "success");
} catch (error) {
foundAgency = null;
renderFoundAgency(null);
showMessage(errorMessage(error, "network_error"), "error");
} finally {
button.disabled = false;
button.textContent = t("search");
}
}
async function fetchPendingRecord() {
try {
pendingRecord = await apiRequest("/team/wait-apply/profile");
renderPendingRecord(pendingRecord);
} catch (error) {
pendingRecord = null;
renderPendingRecord(null);
if (error.message === t("missing_token")) showMessage(error.message, "error");
}
}
async function applyToAgency() {
if (pendingRecord) return;
if (!foundAgency?.id) return;
const button = document.querySelector("#applyButton");
button.disabled = true;
button.textContent = t("applying");
showMessage("");
try {
await apiRequest("/team/user/apply", {
method: "POST",
body: {
teamId: foundAgency.id,
reason: "JOIN"
}
});
foundAgency = null;
renderFoundAgency(null);
showMessage(t("application_submitted"), "success");
await fetchPendingRecord();
} catch (error) {
showMessage(errorMessage(error, "application_failed"), "error");
} finally {
button.disabled = false;
button.textContent = t("apply");
}
}
async function cancelApplication() {
const id = pendingRecord?.messageId || pendingRecord?.id;
if (!id) return;
const button = document.querySelector("#cancelButton");
button.disabled = true;
button.textContent = t("cancelling");
showMessage("");
try {
await apiRequest("/team/user/join-apply-cancel", {
method: "POST",
body: { id }
});
pendingRecord = null;
renderPendingRecord(null);
showMessage(t("application_cancelled"), "success");
} catch (error) {
showMessage(errorMessage(error, "cancellation_failed"), "error");
} finally {
button.disabled = false;
button.textContent = t("cancel");
}
}
function buildPageURL(path) {
const url = new URL(path, window.location.href);
const nextParams = new URLSearchParams(params);
const token = readRawParam("token");
if (token) nextParams.set("token", token);
if (currentLang) nextParams.set("lang", currentLang);
url.search = nextParams.toString();
return url.toString();
}
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();
return;
}
window.location.href = buildPageURL("../host-center/index.html");
});
document.querySelector("#searchForm")?.addEventListener("submit", (event) => {
event.preventDefault();
searchAgency();
});
document.querySelector("#applyButton")?.addEventListener("click", applyToAgency);
document.querySelector("#cancelButton")?.addEventListener("click", cancelApplication);
applyLanguage(currentLang).then(fetchPendingRecord);
})();

View File

@ -0,0 +1,209 @@
.join-agency-page {
min-height: 100vh;
}
.join-content {
gap: 12px;
padding-top: 14px;
}
.join-search-card,
.agency-result-card,
.pending-card {
padding: 18px 16px;
}
.field-label,
.section-title {
display: block;
margin-bottom: 12px;
color: #24282e;
font-size: 16px;
font-weight: 900;
line-height: 1.2;
}
.search-form {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 10px;
align-items: center;
}
.search-form input {
width: 100%;
height: 44px;
min-width: 0;
border: 1px solid #eef0f2;
border-radius: 8px;
outline: none;
background: #f8fafa;
color: #25282e;
font: inherit;
font-size: 15px;
font-weight: 750;
padding: 0 12px;
}
.search-form input::placeholder {
color: #b3b5ba;
}
.search-button,
.primary-action,
.secondary-action {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
border-radius: 8px;
font-size: 15px;
font-weight: 900;
}
.search-button {
min-width: 86px;
padding: 0 16px;
background: #31d9cb;
color: #fff;
}
.search-button:disabled,
.primary-action:disabled,
.secondary-action:disabled {
opacity: 0.58;
}
.status-message {
min-height: 38px;
border-radius: 8px;
padding: 10px 12px;
background: rgba(255, 255, 255, 0.96);
color: #8b8d92;
font-size: 13px;
font-weight: 800;
line-height: 1.3;
box-shadow: var(--shadow);
}
.status-message.is-error {
color: #d64f4f;
}
.status-message.is-success {
color: #0d988d;
}
.agency-profile-row {
display: flex;
align-items: center;
gap: 14px;
min-width: 0;
}
.agency-avatar {
display: flex;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
flex: 0 0 56px;
overflow: hidden;
border-radius: 50%;
background: linear-gradient(145deg, #d7fff9, #45e7d8);
color: #0c5f58;
font-size: 21px;
font-weight: 950;
}
.agency-avatar img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.agency-avatar img[hidden],
.agency-avatar span[hidden] {
display: none;
}
.agency-copy {
min-width: 0;
flex: 1;
}
.agency-name {
overflow: hidden;
color: #25282e;
font-size: 18px;
font-weight: 900;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.agency-account {
margin-top: 6px;
overflow: hidden;
color: #8b8d92;
font-size: 13px;
font-weight: 800;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.primary-action,
.secondary-action {
width: 100%;
margin-top: 16px;
}
.primary-action {
background: linear-gradient(135deg, #35dfd2 0%, #18bfb4 100%);
color: #fff;
}
.secondary-action {
border: 1px solid #e9edf0;
background: #fff;
color: #4b4e55;
}
.pending-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.pending-head .section-title {
margin-bottom: 12px;
}
.pending-status {
flex: 0 0 auto;
margin-bottom: 12px;
color: #f2a329;
font-size: 13px;
font-weight: 900;
}
html[dir="rtl"] .agency-copy {
text-align: right;
}
html[dir="rtl"] .search-form {
direction: rtl;
}
@media (max-width: 360px) {
.search-form {
grid-template-columns: 1fr;
}
.search-button {
width: 100%;
}
}

View File

@ -47,9 +47,10 @@
</section>
<section class="mode-card">
<div class="mode-tabs" role="tablist" aria-label="Withdraw or exchange">
<div class="mode-tabs" role="tablist" aria-label="Withdraw, exchange, or transfer">
<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>
<button class="mode-tab" type="button" role="tab" aria-selected="false" data-mode="transfer" data-i18n="transfer">Transfer</button>
</div>
<div class="mode-panel active" data-panel="withdraw">
@ -57,7 +58,7 @@
<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" />
<input id="withdrawAmount" type="number" inputmode="decimal" min="0" step="0.01" aria-label="Withdraw amount" data-i18n-aria="withdraw_amount" />
</div>
</label>
<div class="hint-row">
@ -71,23 +72,40 @@
<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" />
<input id="exchangeAmount" type="number" inputmode="decimal" min="0" step="0.01" 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>
<span data-i18n="exchange_rate">Exchange rate</span>
<strong id="exchangeRateText">1$ = 80000 golds</strong>
</div>
</div>
<div class="mode-panel" data-panel="transfer">
<label class="field">
<span data-i18n="transfer_to">Transfer to</span>
<div class="transfer-search">
<input id="transferAccount" type="text" inputmode="numeric" autocomplete="off" aria-label="Transfer to" data-i18n-aria="transfer_to" />
<button class="search-payee-button" id="searchPayeeButton" type="button" data-i18n="search">Search</button>
</div>
</label>
<div class="payee-result" id="payeeResult" hidden></div>
<label class="field transfer-amount-field">
<span data-i18n="transfer_amount">Transfer amount</span>
<div class="field-control amount-control">
<span>$</span>
<input id="transferAmount" type="number" inputmode="decimal" min="0" step="0.01" aria-label="Transfer amount" data-i18n-aria="transfer_amount" />
</div>
</label>
</div>
</section>
<section class="card-section">
<section class="card-section payment-section" data-payment-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 class="section-title" data-i18n="payment_method">USDT TRC20 address</div>
<div class="section-subtitle" data-i18n="payment_method_subtitle">Withdrawals are sent to your TRC20 USDT address.</div>
</div>
<button class="bind-address-button" type="button" hidden data-i18n="manage">Manage</button>
</div>
<div class="empty-address-state">
@ -100,19 +118,26 @@
</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 class="empty-title" data-i18n="no_payment_info">No USDT address</div>
<div class="empty-copy" data-i18n="no_payment_info_copy">Fill in a TRC20 USDT address before withdrawing.</div>
</div>
</div>
<div class="address-form" id="addressForm" hidden>
<label class="field">
<span data-i18n="trc20_address">TRC20 address</span>
<input id="trc20Address" type="text" autocomplete="off" />
</label>
<button class="primary-button" id="saveAddressButton" type="button" data-i18n="save_address">Save address</button>
</div>
<div class="bound-address-card" hidden>
<div class="network-badge" aria-hidden="true">BANK</div>
<div class="network-badge" aria-hidden="true">TRC20</div>
<div class="bound-copy">
<div class="bound-network">Bank card</div>
<div class="bound-network">USDT TRC20</div>
<div class="bound-address">-</div>
<div class="bound-meta">-</div>
<div class="bound-meta" data-i18n="usdt_only">USDT only</div>
</div>
<button class="change-address" type="button" hidden data-i18n="change">Change</button>
</div>
</section>

View File

@ -1,5 +1,6 @@
(function () {
const API_BASE_URL = "https://jvapi.haiyihy.com";
const EXCHANGE_RATE = 80000;
const supportedLanguages = ["en", "ar", "tr", "id"];
const languageLabels = {
en: "EN",
@ -17,32 +18,47 @@
available_income: "Available Income",
withdraw: "Withdraw",
exchange: "Exchange",
transfer: "Transfer",
withdraw_amount: "Withdraw amount",
exchange_amount: "Exchange amount",
transfer_amount: "Transfer amount",
transfer_to: "Transfer to",
search: "Search",
searching: "Searching...",
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",
exchange_rate: "Exchange rate",
payment_method: "USDT TRC20 address",
payment_method_subtitle: "Withdrawals are sent to your TRC20 USDT address.",
no_payment_info: "No USDT address",
no_payment_info_copy: "Fill in a TRC20 USDT address before withdrawing.",
trc20_address: "TRC20 address",
save_address: "Save address",
saving: "Saving...",
usdt_only: "USDT only",
withdraw_now: "Withdraw now",
exchange_now: "Exchange now",
transfer_now: "Transfer now",
submitting: "Submitting...",
payee: "Payee",
card_number: "Card",
uid_prefix: "UID:",
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",
load_payment_failed: "Failed to load payment info",
missing_payment_info: "Please add a TRC20 USDT address",
invalid_address: "Please enter a valid TRC20 USDT address",
invalid_amount: "Please enter a valid amount",
amount_too_low: "Minimum withdrawal amount is 50 USD",
amount_exceeds_balance: "Amount exceeds available income",
payee_not_found: "User not found",
missing_payee: "Please search and select a user",
address_saved: "Address saved",
withdraw_success: "Withdrawal submitted",
withdraw_failed: "Withdrawal failed",
exchange_unavailable: "Exchange is not available on this page"
exchange_success: "Exchange success",
exchange_failed: "Exchange failed",
transfer_success: "Transfer success",
transfer_failed: "Transfer failed"
},
ar: {
page_label: "السحب والتبديل",
@ -52,32 +68,47 @@
available_income: "الدخل المتاح",
withdraw: "سحب",
exchange: "تبديل",
transfer: "تحويل",
withdraw_amount: "مبلغ السحب",
exchange_amount: "مبلغ التبديل",
transfer_amount: "مبلغ التحويل",
transfer_to: "تحويل إلى",
search: "بحث",
searching: "جار البحث...",
minimum: "الحد الأدنى 50 USD",
all: "الكل",
you_receive: "ستحصل على",
payment_method: "طريقة الدفع",
payment_method_subtitle: "سيتم إرسال السحب إلى البطاقة المستخدمة حاليًا.",
manage: "إدارة",
no_bank_card: "لا توجد بطاقة مستخدمة",
no_bank_card_copy: "عيّن بطاقة بنكية مستخدمة قبل السحب.",
change: "تغيير",
exchange_rate: "سعر التبديل",
payment_method: "عنوان USDT TRC20",
payment_method_subtitle: "يتم إرسال السحب إلى عنوان USDT TRC20.",
no_payment_info: "لا يوجد عنوان USDT",
no_payment_info_copy: "أدخل عنوان USDT TRC20 قبل السحب.",
trc20_address: "عنوان TRC20",
save_address: "حفظ العنوان",
saving: "جار الحفظ...",
usdt_only: "USDT فقط",
withdraw_now: "اسحب الآن",
exchange_now: "بدل الآن",
transfer_now: "حوّل الآن",
submitting: "جار الإرسال...",
payee: "المستفيد",
card_number: "البطاقة",
uid_prefix: "UID:",
missing_token: "رمز الدخول مفقود",
load_balance_failed: "فشل تحميل الرصيد",
load_bank_card_failed: "فشل تحميل البطاقة",
missing_bank_card: "لا توجد بطاقة مستخدمة",
load_payment_failed: "فشل تحميل معلومات الدفع",
missing_payment_info: "يرجى إضافة عنوان USDT TRC20",
invalid_address: "يرجى إدخال عنوان USDT TRC20 صالح",
invalid_amount: "يرجى إدخال مبلغ صالح",
amount_too_low: "الحد الأدنى للسحب هو 50 USD",
amount_exceeds_balance: "المبلغ يتجاوز الدخل المتاح",
payee_not_found: "لم يتم العثور على المستخدم",
missing_payee: "يرجى البحث واختيار مستخدم",
address_saved: "تم حفظ العنوان",
withdraw_success: "تم إرسال طلب السحب",
withdraw_failed: "فشل السحب",
exchange_unavailable: "التبديل غير متاح في هذه الصفحة"
exchange_success: "تم التبديل",
exchange_failed: "فشل التبديل",
transfer_success: "تم التحويل",
transfer_failed: "فشل التحويل"
},
tr: {
page_label: "Çekme ve değişim",
@ -87,32 +118,47 @@
available_income: "Kullanılabilir Gelir",
withdraw: "Withdraw",
exchange: "Exchange",
transfer: "Transfer",
withdraw_amount: "Withdraw amount",
exchange_amount: "Exchange amount",
transfer_amount: "Transfer amount",
transfer_to: "Transfer to",
search: "Search",
searching: "Searching...",
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",
exchange_rate: "Exchange rate",
payment_method: "USDT TRC20 address",
payment_method_subtitle: "Withdrawals are sent to your TRC20 USDT address.",
no_payment_info: "No USDT address",
no_payment_info_copy: "Fill in a TRC20 USDT address before withdrawing.",
trc20_address: "TRC20 address",
save_address: "Save address",
saving: "Saving...",
usdt_only: "USDT only",
withdraw_now: "Withdraw now",
exchange_now: "Exchange now",
transfer_now: "Transfer now",
submitting: "Submitting...",
payee: "Payee",
card_number: "Card",
uid_prefix: "UID:",
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",
load_payment_failed: "Failed to load payment info",
missing_payment_info: "Please add a TRC20 USDT address",
invalid_address: "Please enter a valid TRC20 USDT address",
invalid_amount: "Please enter a valid amount",
amount_too_low: "Minimum withdrawal amount is 50 USD",
amount_exceeds_balance: "Amount exceeds available income",
payee_not_found: "User not found",
missing_payee: "Please search and select a user",
address_saved: "Address saved",
withdraw_success: "Withdrawal submitted",
withdraw_failed: "Withdrawal failed",
exchange_unavailable: "Exchange is not available on this page"
exchange_success: "Exchange success",
exchange_failed: "Exchange failed",
transfer_success: "Transfer success",
transfer_failed: "Transfer failed"
},
id: {
page_label: "Tarik dan tukar",
@ -122,49 +168,80 @@
available_income: "Penghasilan Tersedia",
withdraw: "Tarik",
exchange: "Tukar",
transfer: "Transfer",
withdraw_amount: "Jumlah penarikan",
exchange_amount: "Jumlah penukaran",
transfer_amount: "Jumlah transfer",
transfer_to: "Transfer ke",
search: "Cari",
searching: "Mencari...",
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",
exchange_rate: "Kurs penukaran",
payment_method: "Alamat USDT TRC20",
payment_method_subtitle: "Penarikan dikirim ke alamat USDT TRC20 Anda.",
no_payment_info: "Tidak ada alamat USDT",
no_payment_info_copy: "Isi alamat USDT TRC20 sebelum menarik.",
trc20_address: "Alamat TRC20",
save_address: "Simpan alamat",
saving: "Menyimpan...",
usdt_only: "Hanya USDT",
withdraw_now: "Tarik sekarang",
exchange_now: "Tukar sekarang",
transfer_now: "Transfer sekarang",
submitting: "Mengirim...",
payee: "Penerima",
card_number: "Kartu",
uid_prefix: "UID:",
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",
load_payment_failed: "Gagal memuat informasi pembayaran",
missing_payment_info: "Tambahkan alamat USDT TRC20",
invalid_address: "Masukkan alamat USDT TRC20 yang valid",
invalid_amount: "Masukkan jumlah yang valid",
amount_too_low: "Minimum penarikan adalah 50 USD",
amount_exceeds_balance: "Jumlah melebihi penghasilan tersedia",
payee_not_found: "Pengguna tidak ditemukan",
missing_payee: "Cari dan pilih pengguna",
address_saved: "Alamat disimpan",
withdraw_success: "Penarikan terkirim",
withdraw_failed: "Penarikan gagal",
exchange_unavailable: "Penukaran tidak tersedia di halaman ini"
exchange_success: "Penukaran berhasil",
exchange_failed: "Penukaran gagal",
transfer_success: "Transfer berhasil",
transfer_failed: "Transfer gagal"
}
};
const params = readURLParams();
let currentLang = normalizeLanguage(params.get("lang"));
let currentMode = "withdraw";
let currentMode = normalizeMode(params.get("mode")) || "withdraw";
let currentMessages = translations[currentLang] || translations.en;
let availableBalance = 0;
let selectedBankCard = null;
let selectedPaymentInfo = null;
let selectedPayee = null;
let currentProfile = null;
let isSubmitting = false;
let isSavingAddress = false;
let isSearchingPayee = false;
const amountInput = document.querySelector("#withdrawAmount");
const inputs = {
withdraw: document.querySelector("#withdrawAmount"),
exchange: document.querySelector("#exchangeAmount"),
transfer: document.querySelector("#transferAmount")
};
const balanceAmount = document.querySelector("#balanceAmount");
const emptyState = document.querySelector(".empty-address-state");
const addressForm = document.querySelector("#addressForm");
const trc20AddressInput = document.querySelector("#trc20Address");
const saveAddressButton = document.querySelector("#saveAddressButton");
const boundCard = document.querySelector(".bound-address-card");
const paymentSection = document.querySelector("[data-payment-section]");
const submitButton = document.querySelector(".submit-button");
const toast = document.querySelector(".toast");
const payeeResult = document.querySelector("#payeeResult");
const transferAccountInput = document.querySelector("#transferAccount");
const searchPayeeButton = document.querySelector("#searchPayeeButton");
const exchangeRateText = document.querySelector("#exchangeRateText");
function readURLParams() {
const result = new URLSearchParams(window.location.search);
@ -208,6 +285,10 @@
return supportedLanguages.includes(value) ? value : "en";
}
function normalizeMode(mode) {
return ["withdraw", "exchange", "transfer"].includes(mode) ? mode : "";
}
function apiBaseURL() {
const override = window.HYAPP_API_BASE_URL || params.get("apiBase");
if (override) return String(override).replace(/\/+$/, "");
@ -236,7 +317,7 @@
async function requestJSON(path, options = {}) {
const token = readRawParam("token");
if (!token) throw new Error(currentMessages.missing_token || translations.en.missing_token);
if (!token) throw new Error(message("missing_token", "Missing token"));
const response = await fetch(`${apiBaseURL()}${path}`, {
method: options.method || "GET",
@ -246,7 +327,9 @@
});
const data = await response.json().catch(() => ({}));
if (!response.ok || data.status === false) {
throw new Error(data.errorMsg || data.message || `Request failed: ${response.status}`);
const error = new Error(data.errorMsg || data.message || `Request failed: ${response.status}`);
error.response = data;
throw error;
}
return data;
}
@ -265,15 +348,26 @@
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 activeInput() {
return inputs[currentMode] || null;
}
function activeAmount() {
return moneyNumber(amountInput?.value);
return moneyNumber(activeInput()?.value);
}
function formatExchangeRate() {
return `1$ = ${EXCHANGE_RATE} golds`;
}
function isValidTrc20Address(value) {
return /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(String(value || "").trim());
}
function maskAddress(value) {
const text = String(value || "").trim();
if (text.length <= 12) return text || "-";
return `${text.slice(0, 6)}...${text.slice(-6)}`;
}
function applyLanguage(lang) {
@ -301,19 +395,23 @@
button.classList.toggle("is-active", isActive);
button.setAttribute("aria-pressed", String(isActive));
});
if (exchangeRateText) exchangeRateText.textContent = formatExchangeRate();
renderPayee();
updateSubmitState();
}
function setMode(mode) {
currentMode = mode;
const nextMode = normalizeMode(mode) || "withdraw";
currentMode = nextMode;
document.querySelectorAll(".mode-tab").forEach((tab) => {
const active = tab.dataset.mode === mode;
const active = tab.dataset.mode === nextMode;
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);
panel.classList.toggle("active", panel.dataset.panel === nextMode);
});
if (paymentSection) paymentSection.hidden = nextMode !== "withdraw";
updateSubmitState();
}
@ -325,13 +423,24 @@
return;
}
submitButton.textContent = currentMode === "exchange" ? message("exchange_now", "Exchange now") : message("withdraw_now", "Withdraw now");
const submitLabels = {
withdraw: message("withdraw_now", "Withdraw now"),
exchange: message("exchange_now", "Exchange now"),
transfer: message("transfer_now", "Transfer now")
};
submitButton.textContent = submitLabels[currentMode];
const amount = activeAmount();
submitButton.disabled = currentMode !== "withdraw" ||
!selectedBankCard?.id ||
!amount ||
amount < 50 ||
amount > availableBalance;
const exceedsBalance = amount > availableBalance;
const missingWithdrawInfo = currentMode === "withdraw" && !selectedPaymentInfo?.id;
const missingPayee = currentMode === "transfer" && !selectedPayee?.id;
submitButton.disabled = !amount ||
amount <= 0 ||
exceedsBalance ||
missingWithdrawInfo ||
missingPayee ||
(currentMode === "withdraw" && amount < 50);
}
function showToast(keyOrText, fallback) {
@ -347,15 +456,18 @@
function renderBalance(value) {
availableBalance = moneyNumber(value);
if (balanceAmount) balanceAmount.textContent = formatMoney(availableBalance);
if (amountInput) amountInput.max = formatMoney(availableBalance);
Object.values(inputs).forEach((input) => {
if (input) input.max = formatMoney(availableBalance);
});
updateSubmitState();
}
function renderBankCard(card) {
selectedBankCard = card || null;
function renderPaymentInfo(paymentInfo) {
selectedPaymentInfo = paymentInfo || null;
if (!selectedBankCard) {
if (!selectedPaymentInfo) {
if (emptyState) emptyState.hidden = false;
if (addressForm) addressForm.hidden = false;
if (boundCard) boundCard.hidden = true;
updateSubmitState();
return;
@ -365,16 +477,51 @@
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 (badge) badge.textContent = "TRC20";
if (network) network.textContent = "USDT TRC20";
if (address) address.textContent = maskAddress(selectedPaymentInfo.cardNo);
if (meta) meta.textContent = message("usdt_only", "USDT only");
if (emptyState) emptyState.hidden = true;
if (addressForm) addressForm.hidden = true;
if (boundCard) boundCard.hidden = false;
updateSubmitState();
}
function renderPayee() {
if (!payeeResult) return;
if (!selectedPayee) {
payeeResult.hidden = true;
payeeResult.innerHTML = "";
updateSubmitState();
return;
}
const name = selectedPayee.userNickname || selectedPayee.name || "-";
const account = selectedPayee.ownSpecialId?.account || selectedPayee.account || selectedPayee.id || "-";
const avatar = selectedPayee.userAvatar || selectedPayee.avatar || "";
payeeResult.hidden = false;
payeeResult.innerHTML = `
<div class="payee-avatar">${avatar ? `<img src="${escapeHTML(avatar)}" alt="">` : escapeHTML(name.slice(0, 1) || "U")}</div>
<div class="payee-copy">
<div class="payee-name">${escapeHTML(name)}</div>
<div class="payee-id">${escapeHTML(message("uid_prefix", "UID:"))} ${escapeHTML(account)}</div>
</div>
`;
updateSubmitState();
}
function escapeHTML(value) {
return String(value || "").replace(/[&<>"']/g, (char) => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;"
})[char]);
}
async function fetchBalance() {
try {
const data = await requestJSON("/wallet/bank/balance");
@ -387,66 +534,215 @@
}
}
async function fetchBankCards() {
async function fetchUserProfile() {
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));
const data = await requestJSON("/team/member/profile");
currentProfile = data.body ?? data.data ?? null;
} catch (error) {
console.error("Failed to load bank card:", error);
renderBankCard(null);
showToast("load_bank_card_failed", error.message);
console.error("Failed to load user profile:", error);
currentProfile = null;
}
}
function normalizeAmountInput() {
if (!amountInput) return;
const raw = String(amountInput.value || "").replace(/[^\d.]/g, "");
async function fetchPaymentInfo() {
try {
const data = await requestJSON("/wallet/bank-card/list");
const cards = Array.isArray(data.body) ? data.body : Array.isArray(data.data) ? data.data : [];
const usdtCards = cards.filter((item) => item.cardType === "USDT");
renderPaymentInfo(usdtCards.find((item) => item.use) || usdtCards[0] || null);
} catch (error) {
console.error("Failed to load payment info:", error);
renderPaymentInfo(null);
showToast("load_payment_failed", error.message);
}
}
function normalizeAmountInput(event) {
const input = event?.target || activeInput();
if (!input) return;
const raw = String(input.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;
input.value = nextValue;
updateSubmitState();
}
function validateWithdraw() {
function validateAmount(requireMinimum) {
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 (requireMinimum && amount < 50) return "amount_too_low";
if (amount > availableBalance) return "amount_exceeds_balance";
return "";
}
async function submitWithdraw() {
if (currentMode !== "withdraw") {
showToast("exchange_unavailable");
async function saveAddress() {
const address = String(trc20AddressInput?.value || "").trim();
if (!isValidTrc20Address(address)) {
showToast("invalid_address");
return;
}
if (!currentProfile) await fetchUserProfile();
isSavingAddress = true;
if (saveAddressButton) {
saveAddressButton.disabled = true;
saveAddressButton.textContent = message("saving", "Saving...");
}
try {
const data = await requestJSON("/wallet/bank-card/add", {
method: "POST",
body: {
userId: currentProfile?.id || currentProfile?.userId || "",
cardType: "USDT",
cardName: "USDT",
cardNo: address,
payee: "USDT-TRC20"
}
});
const created = data.body || data.data || null;
if (created?.id) {
renderPaymentInfo({ ...created, cardType: "USDT", cardNo: address });
await requestJSON("/wallet/bank-card/use", {
method: "POST",
body: { id: created.id }
}).catch((error) => console.warn("Failed to set USDT address in use:", error));
}
await fetchPaymentInfo();
showToast("address_saved");
} catch (error) {
console.error("Failed to save TRC20 address:", error);
showToast(error.message || message("load_payment_failed", "Failed to load payment info"));
} finally {
isSavingAddress = false;
if (saveAddressButton) {
saveAddressButton.disabled = false;
saveAddressButton.textContent = message("save_address", "Save address");
}
}
}
async function searchPayee() {
const account = String(transferAccountInput?.value || "").trim();
if (!account) {
showToast("missing_payee");
return;
}
selectedPayee = null;
renderPayee();
isSearchingPayee = true;
if (searchPayeeButton) {
searchPayeeButton.disabled = true;
searchPayeeButton.textContent = message("searching", "Searching...");
}
try {
const path = `/wallet/bank/search/user-profile?account=${encodeURIComponent(account)}&type=AGENT&searchType=TRANSFER`;
const data = await requestJSON(path);
const body = data.body ?? data.data ?? null;
if (!body?.id) {
showToast("payee_not_found");
return;
}
selectedPayee = body;
renderPayee();
} catch (error) {
console.error("Failed to search payee:", error);
showToast(error.message || message("payee_not_found", "User not found"));
} finally {
isSearchingPayee = false;
if (searchPayeeButton) {
searchPayeeButton.disabled = false;
searchPayeeButton.textContent = message("search", "Search");
}
}
}
function validateWithdraw() {
if (!selectedPaymentInfo?.id) return "missing_payment_info";
return validateAmount(true);
}
function validateTransfer() {
if (!selectedPayee?.id) return "missing_payee";
return validateAmount(false);
}
async function submitWithdraw() {
const errorKey = validateWithdraw();
if (errorKey) {
showToast(errorKey);
return;
}
await requestJSON("/wallet/bank/withdraw", {
method: "POST",
body: {
amount: activeAmount(),
acceptBankCardId: selectedPaymentInfo.id,
salaryType: "HOST_SALARY"
}
});
showToast("withdraw_success");
}
async function submitExchange() {
const errorKey = validateAmount(false);
if (errorKey) {
showToast(errorKey);
return;
}
await requestJSON("/wallet/bank/exchange/gold", {
method: "POST",
body: {
amount: activeAmount()
}
});
showToast("exchange_success");
}
async function submitTransfer() {
const errorKey = validateTransfer();
if (errorKey) {
showToast(errorKey);
return;
}
await requestJSON("/wallet/bank/transferGold", {
method: "POST",
body: {
amount: activeAmount(),
acceptUserId: selectedPayee.id,
acceptMethod: "BANK_TRANSFER"
}
});
showToast("transfer_success");
}
async function submitCurrentMode() {
if (isSubmitting) return;
isSubmitting = true;
updateSubmitState();
try {
await requestJSON("/wallet/bank/withdraw", {
method: "POST",
body: {
amount: activeAmount(),
acceptBankCardId: selectedBankCard.id,
salaryType: "HOST_SALARY"
}
});
showToast("withdraw_success");
if (currentMode === "withdraw") await submitWithdraw();
if (currentMode === "exchange") await submitExchange();
if (currentMode === "transfer") await submitTransfer();
const input = activeInput();
if (input) input.value = "";
await fetchBalance();
} catch (error) {
console.error("Failed to submit withdrawal:", error);
showToast(error.message || message("withdraw_failed", "Withdrawal failed"));
console.error(`Failed to submit ${currentMode}:`, error);
const fallback = {
withdraw: "withdraw_failed",
exchange: "exchange_failed",
transfer: "transfer_failed"
}[currentMode];
showToast(error.message || message(fallback, "Submit failed"));
} finally {
isSubmitting = false;
updateSubmitState();
@ -500,16 +796,26 @@
});
document.querySelector(".text-action")?.addEventListener("click", () => {
if (amountInput) amountInput.value = formatMoney(availableBalance);
if (inputs.withdraw) inputs.withdraw.value = formatMoney(availableBalance);
updateSubmitState();
});
amountInput?.addEventListener("input", normalizeAmountInput);
submitButton?.addEventListener("click", submitWithdraw);
Object.values(inputs).forEach((input) => input?.addEventListener("input", normalizeAmountInput));
transferAccountInput?.addEventListener("input", () => {
selectedPayee = null;
renderPayee();
});
transferAccountInput?.addEventListener("keydown", (event) => {
if (event.key === "Enter") searchPayee();
});
searchPayeeButton?.addEventListener("click", searchPayee);
saveAddressButton?.addEventListener("click", saveAddress);
submitButton?.addEventListener("click", submitCurrentMode);
syncBackFallback();
applyLanguage(currentLang);
setMode("withdraw");
setMode(currentMode);
fetchUserProfile();
fetchBalance();
fetchBankCards();
fetchPaymentInfo();
})();

View File

@ -338,7 +338,7 @@ select {
.mode-tabs {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
padding: 4px;
border-radius: 8px;
@ -349,7 +349,7 @@ select {
min-height: 40px;
border-radius: 7px;
color: #777a80;
font-size: 15px;
font-size: 14px;
font-weight: 900;
}
@ -435,6 +435,96 @@ select {
color: #24282e;
}
.transfer-search {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 10px;
align-items: center;
}
.transfer-search input {
min-height: 48px;
padding: 0 14px;
border-radius: 8px;
background: #f4f6f6;
}
.search-payee-button {
min-width: 82px;
min-height: 48px;
padding: 0 14px;
border-radius: 8px;
background: var(--mint-dark);
color: #fff;
font-size: 14px;
font-weight: 950;
}
.search-payee-button:disabled {
opacity: 0.58;
}
.payee-result {
display: flex;
align-items: center;
gap: 12px;
min-height: 68px;
margin-top: 12px;
padding: 12px;
border-radius: 8px;
background: #f9fbfb;
border: 1px solid #edf0f0;
}
.payee-avatar {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
flex: 0 0 44px;
overflow: hidden;
border-radius: 50%;
background: #ddfbf8;
color: #0d8077;
font-weight: 950;
}
.payee-avatar img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.payee-copy {
min-width: 0;
flex: 1;
}
.payee-name {
overflow: hidden;
color: #25282e;
font-size: 15px;
font-weight: 900;
text-overflow: ellipsis;
white-space: nowrap;
}
.payee-id {
margin-top: 4px;
overflow: hidden;
color: var(--muted);
font-size: 12px;
font-weight: 800;
text-overflow: ellipsis;
white-space: nowrap;
}
.transfer-amount-field {
margin-top: 14px;
}
.card-section,
.bind-panel {
padding: 18px 16px;
@ -460,18 +550,6 @@ select {
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;
@ -564,6 +642,7 @@ select {
font-size: 14px;
font-weight: 850;
letter-spacing: 0;
overflow-wrap: anywhere;
}
.bound-meta {
@ -573,11 +652,6 @@ select {
font-weight: 750;
}
.change-address {
background: rgba(255, 255, 255, 0.18);
color: #fff;
}
.bind-panel {
display: block;
}
@ -585,51 +659,16 @@ select {
.address-form {
display: flex;
flex-direction: column;
gap: 13px;
margin-top: 14px;
}
.network-field {
display: flex;
align-items: center;
gap: 9px;
.address-form input {
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;
@ -638,17 +677,21 @@ select {
font-weight: 950;
}
.secondary-button {
background: #eef1f1;
color: #55585f;
}
.primary-button,
.submit-button {
background: linear-gradient(135deg, #67f4e8 0%, #27d3c4 100%);
color: #11302d;
}
.primary-button {
width: 100%;
margin-top: 14px;
}
.primary-button:disabled {
opacity: 0.58;
}
.submit-button {
width: 100%;
box-shadow: 0 8px 20px rgba(29, 206, 193, 0.22);
@ -726,4 +769,12 @@ select {
.title-bar h1 {
font-size: 19px;
}
.mode-tab {
font-size: 13px;
}
.transfer-search {
grid-template-columns: 1fr;
}
}