443 lines
15 KiB
JavaScript
443 lines
15 KiB
JavaScript
const API_BASE_URL = "https://jvapi.haiyihy.com";
|
||
const icons = {
|
||
back: "../host-center/icons/back.svg",
|
||
arrow: "../host-center/icons/arrow.svg",
|
||
coin: "./icons/coin.svg"
|
||
};
|
||
|
||
let commonHeaders = {
|
||
authorization: "Bearer undefined",
|
||
"req-app-intel": "version=1.0.0;build=1;model=SM-G9550;sysVersion=9;channel=Google",
|
||
"req-client": "Android",
|
||
"req-imei": "8fa54d728ab449e04f9292329ed44bda",
|
||
"req-lang": "id-ID",
|
||
"req-sys-origin": "origin=ATYOU;originChild=ATYOU",
|
||
"req-version": "V2",
|
||
"req-zone": "Asia/Shanghai"
|
||
};
|
||
|
||
const coinOptions = [
|
||
{ id: 1, amount: 10500, price: 1 },
|
||
{ id: 2, amount: 52500, price: 5 },
|
||
{ id: 3, amount: 105000, price: 10 },
|
||
{ id: 4, amount: 525000, price: 50 },
|
||
{ id: 5, amount: 1050000, price: 100 },
|
||
{ id: 6, amount: 2100000, price: 200 }
|
||
];
|
||
|
||
const i18n = {
|
||
en: {
|
||
exchange_gold_coins: "Exchange Coins",
|
||
information: "Information",
|
||
details: "Details",
|
||
available_salaries: "Available salaries",
|
||
loading: "Loading",
|
||
exchange: "Exchange",
|
||
tips: "Tips",
|
||
confirm_exchange: "Confirm exchange?",
|
||
cancel: "Cancel",
|
||
confirm: "Confirm",
|
||
failed_to_load_balance: "Failed to load balance.",
|
||
exchange_not_available: "Exchange is not available.",
|
||
request_not_acceptable: "Request not acceptable.",
|
||
failed_to_load_user_info: "Failed to load user info.",
|
||
please_select_coin_amount: "Please select coin amount.",
|
||
insufficient_balance: "Insufficient balance. Balance: {balance}, price: {price}.",
|
||
exchange_success: "Exchange success: ${price}.",
|
||
exchange_failed: "Exchange failed.",
|
||
exchange_error_occurred: "Exchange error occurred."
|
||
},
|
||
ar: {
|
||
exchange_gold_coins: "استبدال العملات",
|
||
information: "معلومات",
|
||
details: "التفاصيل",
|
||
available_salaries: "الرواتب المتاحة",
|
||
loading: "جار التحميل",
|
||
exchange: "استبدال",
|
||
tips: "نصائح",
|
||
confirm_exchange: "تأكيد الاستبدال؟",
|
||
cancel: "إلغاء",
|
||
confirm: "تأكيد",
|
||
failed_to_load_balance: "فشل تحميل الرصيد.",
|
||
exchange_not_available: "الاستبدال غير متاح.",
|
||
request_not_acceptable: "الطلب غير مقبول.",
|
||
failed_to_load_user_info: "فشل تحميل معلومات المستخدم.",
|
||
please_select_coin_amount: "يرجى اختيار كمية العملات.",
|
||
insufficient_balance: "الرصيد غير كاف. الرصيد: {balance}، السعر: {price}.",
|
||
exchange_success: "تم الاستبدال بنجاح: ${price}.",
|
||
exchange_failed: "فشل الاستبدال.",
|
||
exchange_error_occurred: "حدث خطأ في الاستبدال."
|
||
},
|
||
tr: {
|
||
exchange_gold_coins: "Coin Değiştir",
|
||
information: "Bilgi",
|
||
details: "Detaylar",
|
||
available_salaries: "Kullanılabilir maaşlar",
|
||
loading: "Yükleniyor",
|
||
exchange: "Değiştir",
|
||
tips: "İpuçları",
|
||
confirm_exchange: "Değişimi onaylıyor musunuz?",
|
||
cancel: "İptal",
|
||
confirm: "Onayla",
|
||
failed_to_load_balance: "Bakiye yüklenemedi.",
|
||
exchange_not_available: "Değişim kullanılamıyor.",
|
||
request_not_acceptable: "İstek kabul edilemez.",
|
||
failed_to_load_user_info: "Kullanıcı bilgisi yüklenemedi.",
|
||
please_select_coin_amount: "Lütfen coin miktarı seçin.",
|
||
insufficient_balance: "Yetersiz bakiye. Bakiye: {balance}, fiyat: {price}.",
|
||
exchange_success: "Değişim başarılı: ${price}.",
|
||
exchange_failed: "Değişim başarısız.",
|
||
exchange_error_occurred: "Değişim hatası oluştu."
|
||
},
|
||
bn: {
|
||
exchange_gold_coins: "কয়েন এক্সচেঞ্জ",
|
||
information: "তথ্য",
|
||
details: "বিস্তারিত",
|
||
available_salaries: "উপলভ্য বেতন",
|
||
loading: "লোড হচ্ছে",
|
||
exchange: "এক্সচেঞ্জ",
|
||
tips: "টিপস",
|
||
confirm_exchange: "এক্সচেঞ্জ নিশ্চিত করবেন?",
|
||
cancel: "বাতিল",
|
||
confirm: "নিশ্চিত",
|
||
failed_to_load_balance: "ব্যালেন্স লোড ব্যর্থ।",
|
||
exchange_not_available: "এক্সচেঞ্জ উপলভ্য নয়।",
|
||
request_not_acceptable: "রিকোয়েস্ট গ্রহণযোগ্য নয়।",
|
||
failed_to_load_user_info: "ইউজার তথ্য লোড ব্যর্থ।",
|
||
please_select_coin_amount: "কয়েন পরিমাণ নির্বাচন করুন।",
|
||
insufficient_balance: "ব্যালেন্স অপর্যাপ্ত। ব্যালেন্স: {balance}, মূল্য: {price}।",
|
||
exchange_success: "এক্সচেঞ্জ সফল: ${price}।",
|
||
exchange_failed: "এক্সচেঞ্জ ব্যর্থ।",
|
||
exchange_error_occurred: "এক্সচেঞ্জ ত্রুটি ঘটেছে।"
|
||
}
|
||
};
|
||
|
||
const state = {
|
||
lang: resolveLanguage(),
|
||
balance: "0",
|
||
selectedId: null,
|
||
isBalanceLoading: false,
|
||
isUserLoading: false,
|
||
role: ""
|
||
};
|
||
|
||
const $ = (selector) => document.querySelector(selector);
|
||
const $$ = (selector) => Array.from(document.querySelectorAll(selector));
|
||
|
||
function translate(key, params = {}) {
|
||
let text = (i18n[state.lang] && i18n[state.lang][key]) || i18n.en[key] || key;
|
||
Object.entries(params).forEach(([name, value]) => {
|
||
text = text.replace(`{${name}}`, value);
|
||
});
|
||
return text;
|
||
}
|
||
|
||
function resolveLanguage() {
|
||
const params = new URLSearchParams(window.location.search);
|
||
const lang = params.get("lang") || navigator.language || "en";
|
||
if (lang.startsWith("ar")) return "ar";
|
||
if (lang.startsWith("tr")) return "tr";
|
||
if (lang.startsWith("bn")) return "bn";
|
||
return "en";
|
||
}
|
||
|
||
function splitHeaderPairs(value) {
|
||
const result = {};
|
||
if (!value) return result;
|
||
value.split(";").forEach((item) => {
|
||
const index = item.indexOf("=");
|
||
if (index > -1) result[item.slice(0, index)] = item.slice(index + 1);
|
||
});
|
||
return result;
|
||
}
|
||
|
||
function parseAccessOrigin(raw) {
|
||
const data = JSON.parse(raw);
|
||
const appIntel = splitHeaderPairs(data["Req-App-Intel"]);
|
||
const sysOrigin = splitHeaderPairs(data["Req-Sys-Origin"]);
|
||
return {
|
||
reqLang: data["Req-Lang"],
|
||
authorization: data.Authorization,
|
||
buildVersion: appIntel.build,
|
||
appVersion: appIntel.version,
|
||
appChannel: appIntel.channel,
|
||
reqImei: appIntel["Req-Imei"],
|
||
sysOrigin: sysOrigin.origin,
|
||
sysOriginChild: sysOrigin.child
|
||
};
|
||
}
|
||
|
||
function setHeadersFromApp(headers) {
|
||
const next = {};
|
||
if (headers.authorization) next.authorization = headers.authorization;
|
||
if (headers.reqLang) next["req-lang"] = headers.reqLang;
|
||
if (headers.appVersion || headers.buildVersion || headers.appChannel) {
|
||
const intel = [];
|
||
if (headers.appVersion) intel.push(`version=${headers.appVersion}`);
|
||
if (headers.buildVersion) intel.push(`build=${headers.buildVersion}`);
|
||
if (headers.appChannel) intel.push(`channel=${headers.appChannel}`);
|
||
next["req-app-intel"] = intel.join(";");
|
||
}
|
||
if (headers.reqImei) next["req-imei"] = headers.reqImei;
|
||
if (headers.sysOrigin) {
|
||
const origin = [`origin=${headers.sysOrigin}`];
|
||
if (headers.sysOriginChild) origin.push(`child=${headers.sysOriginChild}`);
|
||
next["req-sys-origin"] = origin.join(";");
|
||
}
|
||
commonHeaders = { ...commonHeaders, ...next };
|
||
}
|
||
|
||
function connectToApp() {
|
||
return new Promise((resolve) => {
|
||
window.renderData = resolve;
|
||
window.getIosAccessOriginParam = resolve;
|
||
|
||
if (!(window.app || window.webkit || window.FlutterPageControl)) {
|
||
setHeadersFromApp({
|
||
authorization: "",
|
||
reqLang: state.lang,
|
||
appVersion: "1.0.0",
|
||
buildVersion: "1",
|
||
appChannel: "Web",
|
||
reqImei: "H5-STATIC",
|
||
sysOrigin: "LIKEI",
|
||
sysOriginChild: "LIKEI"
|
||
});
|
||
resolve(null);
|
||
return;
|
||
}
|
||
|
||
let attempt = 0;
|
||
const poll = () => {
|
||
attempt += 1;
|
||
if (window.app && typeof window.app.getAccessOrigin === "function") {
|
||
resolve(window.app.getAccessOrigin());
|
||
return;
|
||
}
|
||
if (attempt < 50) {
|
||
window.setTimeout(poll, 100);
|
||
return;
|
||
}
|
||
if (window.FlutterApp && typeof window.FlutterApp.postMessage === "function") {
|
||
window.FlutterApp.postMessage("requestAccessOrigin");
|
||
}
|
||
resolve(null);
|
||
};
|
||
poll();
|
||
}).then((raw) => {
|
||
if (raw) setHeadersFromApp(parseAccessOrigin(raw));
|
||
});
|
||
}
|
||
|
||
async function apiGet(path) {
|
||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||
method: "GET",
|
||
headers: commonHeaders
|
||
});
|
||
return parseApiResponse(response);
|
||
}
|
||
|
||
async function apiPost(path, body) {
|
||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||
method: "POST",
|
||
headers: {
|
||
...commonHeaders,
|
||
"content-type": "application/json"
|
||
},
|
||
body: JSON.stringify(body || {})
|
||
});
|
||
return parseApiResponse(response);
|
||
}
|
||
|
||
async function parseApiResponse(response) {
|
||
const data = await response.json().catch(() => ({}));
|
||
if (!response.ok || data.status === false) {
|
||
const error = new Error(data.errorMsg || data.message || `HTTP Error: ${response.status}`);
|
||
error.status = response.status;
|
||
error.response = data;
|
||
error.errorCode = data.errorCode;
|
||
throw error;
|
||
}
|
||
return data;
|
||
}
|
||
|
||
function navigateTo(route) {
|
||
const url = new URL(route, window.location.origin);
|
||
const lang = new URLSearchParams(window.location.search).get("lang");
|
||
if (lang && !url.searchParams.has("lang")) url.searchParams.set("lang", lang);
|
||
window.location.href = `${url.pathname}${url.search}${url.hash}`;
|
||
}
|
||
|
||
function closePage() {
|
||
if (window.history.length > 1) {
|
||
window.history.back();
|
||
return;
|
||
}
|
||
if (window.app && typeof window.app.closePage === "function") {
|
||
window.app.closePage();
|
||
return;
|
||
}
|
||
if (window.FlutterPageControl && typeof window.FlutterPageControl.postMessage === "function") {
|
||
window.FlutterPageControl.postMessage("close_page");
|
||
}
|
||
}
|
||
|
||
function showToast(message) {
|
||
const toast = $("#toast");
|
||
toast.textContent = message || "Request failed";
|
||
toast.hidden = false;
|
||
window.clearTimeout(showToast.timer);
|
||
showToast.timer = window.setTimeout(() => {
|
||
toast.hidden = true;
|
||
}, 2500);
|
||
}
|
||
|
||
function showModal() {
|
||
$("#confirmModal").hidden = false;
|
||
}
|
||
|
||
function hideModal() {
|
||
$("#confirmModal").hidden = true;
|
||
}
|
||
|
||
async function fetchBalance() {
|
||
state.isBalanceLoading = true;
|
||
renderBalance();
|
||
try {
|
||
const result = await apiGet("/wallet/bank/balance");
|
||
state.balance = result.status && typeof result.body === "number" ? result.body.toFixed(2) : "0.00";
|
||
} catch (error) {
|
||
console.error("Failed to fetch bank balance:", error);
|
||
state.balance = "0.00";
|
||
showToast(translate("failed_to_load_balance"));
|
||
} finally {
|
||
state.isBalanceLoading = false;
|
||
renderBalance();
|
||
}
|
||
}
|
||
|
||
async function checkExchangeAvailable() {
|
||
state.isUserLoading = true;
|
||
try {
|
||
const result = await apiGet("/wallet/bank/check/exchange-gold");
|
||
if (result.status && result.body) {
|
||
state.role = result.body.role || "";
|
||
console.debug("用户角色:", state.role);
|
||
return;
|
||
}
|
||
state.role = "";
|
||
closePage();
|
||
} catch (error) {
|
||
if (error.errorCode === 1083) {
|
||
showToast(translate("exchange_not_available"));
|
||
} else if (error.message && error.message.includes("HTTP Error: 406")) {
|
||
showToast(translate("request_not_acceptable"));
|
||
} else {
|
||
showToast(translate("failed_to_load_user_info"));
|
||
}
|
||
closePage();
|
||
state.role = "";
|
||
} finally {
|
||
state.isUserLoading = false;
|
||
}
|
||
}
|
||
|
||
async function confirmExchange() {
|
||
hideModal();
|
||
if (!state.selectedId) {
|
||
showToast(translate("please_select_coin_amount"));
|
||
return;
|
||
}
|
||
|
||
const option = coinOptions.find((item) => item.id === state.selectedId);
|
||
const price = option.price;
|
||
const balance = parseFloat(state.balance);
|
||
if (Number.isNaN(balance) || balance < price) {
|
||
showToast(translate("insufficient_balance", { balance: balance.toFixed(2), price }));
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await apiPost("/wallet/bank/exchange/gold", {
|
||
coinId: option.id,
|
||
amount: option.price,
|
||
price: option.price
|
||
});
|
||
if (result.status === true || result.errorCode === 0) {
|
||
showToast(translate("exchange_success", { price: option.price }));
|
||
await fetchBalance();
|
||
state.selectedId = null;
|
||
renderCoins();
|
||
} else {
|
||
showToast(result.message || translate("exchange_failed"));
|
||
}
|
||
} catch (error) {
|
||
console.error("Exchange error:", error);
|
||
showToast(translate("exchange_error_occurred"));
|
||
}
|
||
}
|
||
|
||
function renderI18n() {
|
||
document.documentElement.lang = state.lang;
|
||
document.documentElement.dir = state.lang === "ar" ? "rtl" : "ltr";
|
||
$(".lang-button").textContent = state.lang.toUpperCase();
|
||
$$("[data-i18n]").forEach((node) => {
|
||
node.textContent = translate(node.dataset.i18n);
|
||
});
|
||
}
|
||
|
||
function renderIcons() {
|
||
$("#backIcon").src = icons.back;
|
||
$("#detailsArrowIcon").src = icons.arrow;
|
||
}
|
||
|
||
function renderBalance() {
|
||
$("#balanceLoading").hidden = !state.isBalanceLoading;
|
||
$("#balanceValue").hidden = state.isBalanceLoading;
|
||
$("#balanceValue").textContent = state.balance;
|
||
}
|
||
|
||
function renderCoins() {
|
||
const grid = $("#coinGrid");
|
||
grid.innerHTML = "";
|
||
coinOptions.forEach((option) => {
|
||
const item = document.createElement("button");
|
||
item.type = "button";
|
||
item.className = `coin-item${state.selectedId === option.id ? " selected" : ""}`;
|
||
item.innerHTML = `<img src="${icons.coin}" alt=""><div class="coin-price">$${option.price}</div>`;
|
||
item.addEventListener("click", () => {
|
||
state.selectedId = state.selectedId === option.id ? null : option.id;
|
||
renderCoins();
|
||
});
|
||
grid.appendChild(item);
|
||
});
|
||
$("#exchangeButton").disabled = !state.selectedId;
|
||
}
|
||
|
||
function bindEvents() {
|
||
$("[data-action='back']").addEventListener("click", closePage);
|
||
$("[data-action='language']").addEventListener("click", () => navigateTo("/language"));
|
||
$$("[data-route]").forEach((node) => node.addEventListener("click", () => navigateTo(node.dataset.route)));
|
||
$("#exchangeButton").addEventListener("click", () => {
|
||
if (state.selectedId) showModal();
|
||
});
|
||
$("#confirmModal").addEventListener("click", hideModal);
|
||
$(".modal-card").addEventListener("click", (event) => event.stopPropagation());
|
||
$("[data-action='cancel']").addEventListener("click", hideModal);
|
||
$("[data-action='confirm']").addEventListener("click", confirmExchange);
|
||
}
|
||
|
||
async function init() {
|
||
renderI18n();
|
||
renderIcons();
|
||
renderBalance();
|
||
renderCoins();
|
||
bindEvents();
|
||
await connectToApp();
|
||
checkExchangeAvailable();
|
||
fetchBalance();
|
||
console.log("document.documentElement.getAttribute('dir') === 'rtl'", document.documentElement.getAttribute("dir") === "rtl");
|
||
}
|
||
|
||
init();
|