优化host center
This commit is contained in:
parent
29eec35eb0
commit
2507eae749
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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`,必传
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
90
h5/hyapp/join-agency/index.html
Normal file
90
h5/hyapp/join-agency/index.html
Normal 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>
|
||||
529
h5/hyapp/join-agency/script.js
Normal file
529
h5/hyapp/join-agency/script.js
Normal 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);
|
||||
})();
|
||||
209
h5/hyapp/join-agency/style.css
Normal file
209
h5/hyapp/join-agency/style.css
Normal 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%;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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) => ({
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'"
|
||||
})[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();
|
||||
})();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user