2026-04-27 23:27:06 +08:00

516 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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