chore: update h5 center subpages

This commit is contained in:
170-carry 2026-04-24 21:13:53 +08:00
parent 6cdd3bdfbc
commit b6c3d8b0b3
60 changed files with 6107 additions and 1155 deletions

View File

@ -1,13 +1,29 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <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"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>KYC</title> <title>KYC</title>
<link rel="stylesheet" href="../logic-pages/app.css"> <link rel="stylesheet" href="../bd-static/shared.css">
</head> </head>
<body> <body>
<script>window.PAGE_CONFIG = {"title":"KYC","mode":"bankCards"};</script> <main class="page">
<script src="../logic-pages/app.js"></script> <section class="content">
<div class="subtle" data-i18n="contact_number">Contact number</div>
<div class="input-box"><input class="field" id="contactNumber" maxlength="50"></div>
<div class="subtle" data-i18n="other_description">Other description</div>
<div class="input-box" style="position:relative">
<textarea class="field" id="otherDescription" rows="5" maxlength="100"></textarea>
<span class="subtle" id="wordCount" style="position:absolute;right:8px;bottom:4px">0/100</span>
</div>
<div class="subtle" data-i18n="passport_id_card_title">Passport/ID card</div>
<div class="row" id="imageList" style="justify-content:flex-start;flex-wrap:wrap"></div>
<div id="statusLine"></div>
<div style="display:flex;justify-content:center">
<button class="primary-btn" type="button" id="submitButton" disabled data-i18n="submit">Submit</button>
</div>
</section>
</main>
<script type="module" src="./kyc.js"></script>
</body> </body>
</html> </html>

109
h5/KYC/kyc.js Normal file
View File

@ -0,0 +1,109 @@
import { applyLang, bindHeader, connectToApp, get, post, safeJsonArray, t, toast } from "../bd-static/shared.js";
let existing = false;
let data = { id: "", contactNumber: "", category: "CARD", otherDescription: "", passportFrontUrl: "", passportBackUrl: "meaningless" };
let images = [];
let lastImg = localStorage.getItem("lastImgUrl") || "";
bindHeader("kyc");
applyLang();
document.getElementById("contactNumber").placeholder = t("enter_contact_number");
document.getElementById("otherDescription").placeholder = t("enter_other_description");
document.getElementById("contactNumber").addEventListener("input", sync);
document.getElementById("otherDescription").addEventListener("input", sync);
document.getElementById("submitButton").addEventListener("click", submit);
connectToApp().then(load);
async function load() {
try {
const response = await get("/wallet/withdraw-info/list");
if ((response.body || []).length > 0) {
existing = true;
data = { ...response.body[0] };
images = safeJsonArray(data.passportFrontUrl);
}
render();
} catch (error) {
toast(error.response?.errorMsg || error.message);
}
}
function sync() {
data.contactNumber = document.getElementById("contactNumber").value.trim();
data.otherDescription = document.getElementById("otherDescription").value;
document.getElementById("wordCount").textContent = `${data.otherDescription.length}/100`;
document.getElementById("submitButton").disabled = !(data.contactNumber && images.length > 0 && data.contactNumber.length <= 50);
}
function render() {
document.getElementById("contactNumber").value = data.contactNumber || "";
document.getElementById("otherDescription").value = data.otherDescription || "";
const status = document.getElementById("statusLine");
status.innerHTML = existing ? `<span class="subtle">${t("current_status")}</span> <span class="badge ${data.status === "PASS" ? "pass" : data.status === "NOT_PASS" ? "fail" : "wait"}">${statusText(data.status)}</span>` : "";
const list = document.getElementById("imageList");
list.innerHTML = "";
images.forEach((url, index) => {
const wrap = document.createElement("div");
wrap.style.position = "relative";
wrap.innerHTML = `<img src="${url}" alt="" style="display:block;width:15vw;aspect-ratio:1/1;object-fit:cover;border-radius:5vw"><button type="button" class="danger-btn" style="position:absolute;right:-6px;top:-6px;min-height:20px;padding:0 6px">×</button>`;
wrap.querySelector("button").addEventListener("click", () => {
images.splice(index, 1);
render();
});
list.appendChild(wrap);
});
if (images.length < 3) {
const add = document.createElement("button");
add.type = "button";
add.className = "card";
add.style.cssText = "width:15vw;aspect-ratio:1/1;border-radius:5vw;font-size:2em;color:#32a96b";
add.textContent = "+";
add.addEventListener("click", pickImage);
list.appendChild(add);
}
sync();
}
function statusText(status) {
return { PASS: t("review_passed"), NOT_PASS: t("review_failed"), PENDING: t("awaiting_review") }[status] || status || "";
}
function pickImage() {
window.renderData = receiveImage;
window.getIosAccessOriginParam = receiveImage;
if (window.FlutterPageControl) {
window.FlutterPageControl.postMessage("uploadImgFile");
} else if (window.app && typeof window.app.getImagePath === "function") {
receiveImage(window.app.getImagePath());
} else {
toast("uploadImgFile");
}
}
function receiveImage(raw) {
try {
if (!raw || raw === "\"\"") return;
const parsed = JSON.parse(raw);
const path = parsed.path || parsed.url || parsed;
if (!path || path === lastImg) return;
lastImg = path;
localStorage.setItem("lastImgUrl", path);
images.push(path);
render();
} catch (error) {
toast(error.message);
}
}
async function submit() {
sync();
if (document.getElementById("submitButton").disabled) return;
data.passportFrontUrl = JSON.stringify(images);
try {
await post(existing ? "/wallet/withdraw-info/update" : "/wallet/withdraw-info/add", data);
toast(t("application_submitted"));
window.setTimeout(() => window.history.back(), 300);
} catch (error) {
toast(error.response?.errorMsg || error.message);
}
}

View File

@ -571,7 +571,7 @@
</div> </div>
<script> <script>
const DEFAULT_AVATAR = "../../assets/defaultAvatar-CdxWBK1k-1776148661448.png"; const DEFAULT_AVATAR = "../../assets/defaultAvatar-CdxWBK1k.png";
const FALLBACK_IMAGE = "../../assets/coin-lz3Tp0nX-1776148661447.png"; const FALLBACK_IMAGE = "../../assets/coin-lz3Tp0nX-1776148661447.png";
const DEFAULT_WEEK_STAR_API_BASE = "https://jvapi.haiyihy.com/"; const DEFAULT_WEEK_STAR_API_BASE = "https://jvapi.haiyihy.com/";
const TOP_RANK_ICONS = { const TOP_RANK_ICONS = {

View File

@ -126,6 +126,7 @@ const rolePages = {
"/team-bill" "/team-bill"
], ],
bdLeader: [ bdLeader: [
"/agency-center",
"/bd-leader-center", "/bd-leader-center",
"/invite-bd-leader", "/invite-bd-leader",
"/invite-bd", "/invite-bd",

View File

@ -1,13 +1,24 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <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"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Agency List</title> <title>Agency List</title>
<link rel="stylesheet" href="../logic-pages/app.css"> <link rel="stylesheet" href="../bd-static/shared.css">
</head> </head>
<body> <body>
<script>window.PAGE_CONFIG = {"title":"Agency List","mode":"teamHistory","listEndpoint":"/team/bd/member-bill/list?type=BD","historyEndpoint":"/team/bd/history","moreEndpoint":"/team/bd/history/more?billBelong=","countLabel":"Agency number","totalLabel":"Team salary","memberSide1":"Salary","memberSide2":"Host"};</script> <main class="page">
<script src="../logic-pages/app.js"></script> <section class="content">
<article class="card card-pad">
<div class="row">
<div class="section-title" id="billTitle">-</div>
<button class="soft-btn" type="button" id="historyButton" data-i18n="revenue_details">Revenue details</button>
</div>
<div class="row subtle" id="summaryLine"></div>
<div id="memberList"></div>
</article>
</section>
</main>
<script type="module" src="./team-list.js"></script>
</body> </body>
</html> </html>

144
h5/agency-list/team-list.js Normal file
View File

@ -0,0 +1,144 @@
import { applyLang, avatarError, bindHeader, connectToApp, createMask, get, money, statusClass, statusText, t, toast } from "../bd-static/shared.js";
const config = {
title: "agency_list_link",
billType: "BD",
historyTitle: "revenue_details",
listSummary: (data) => `${t("team_recharge")}: ${data.totalRecharge || 0} ${t("team_salary")}: $${data.totalSalary || 0}`,
itemSide: (item) => [`${t("salary")}: $${item.teamSalaryAmount || 0}`, `${t("recharge")}: $${item.teamRechargeAmount || 0}`, `${t("host")}: ${item.teamMemberCount || 0}`],
historyEndpoint: "/team/bd/history",
moreEndpoint: (billBelong) => `/team/bd/history/more?billBelong=${encodeURIComponent(billBelong)}`,
countLabel: "agency_number"
};
init(config);
async function init(settings) {
bindHeader(settings.title);
applyLang();
document.getElementById("historyButton").addEventListener("click", () => showHistory(settings));
try {
await connectToApp();
const response = await get(`/team/bd/member-bill/list?type=${settings.billType}`);
renderList(response.body || {}, settings);
} catch (error) {
toast(error.response?.errorMsg || error.message);
}
}
function renderList(data, settings) {
document.getElementById("billTitle").textContent = data.billTitle || "";
document.getElementById("summaryLine").textContent = settings.listSummary(data);
const list = document.getElementById("memberList");
list.innerHTML = "";
(data.memberBillList || []).forEach((item) => {
const card = document.createElement("article");
card.className = "member-card";
card.innerHTML = `
<img class="avatar" src="${item.agentAvatar || ""}" alt="">
<div class="member-main">
<div class="member-name">${item.agentName || "-"}</div>
<div class="member-meta">${t("user_id_prefix")}${item.agentAccount || "-"}</div>
</div>
<div class="member-side">${settings.itemSide(item).map((line) => `<div>${line}</div>`).join("")}</div>
`;
avatarError(card.querySelector("img"));
list.appendChild(card);
});
}
async function showHistory(settings) {
try {
const historyResponse = await get(settings.historyEndpoint);
const listResponse = await get(`/team/bd/member-bill/list?type=${settings.billType}`);
const history = historyResponse.body || {};
const bill = listResponse.body || {};
if (bill.bdHistoryCO) {
history.incomeDetails = history.incomeDetails || [];
history.incomeDetails.unshift(bill.bdHistoryCO);
}
const sheet = document.createElement("section");
sheet.className = "sheet";
sheet.innerHTML = `
<div class="sheet-title">${t(settings.historyTitle)}</div>
<div class="card card-pad">
<div>${t("total_income")} $${money(history.totalIncome || 0)}</div>
<div>${t("previous_period_income")}: $${money(history.previousPeriodIncome || 0)}</div>
</div>
<div class="section-title">${t("income_details_label")}</div>
<div id="historyItems"></div>
`;
createMask(sheet);
const holder = sheet.querySelector("#historyItems");
(history.incomeDetails || []).forEach((item) => holder.appendChild(historyItem(item, settings)));
} catch (error) {
toast(error.response?.errorMsg || error.message);
}
}
function historyItem(item, settings) {
const card = document.createElement("article");
card.className = `salary-item ${statusClass(item.statusText)}`;
const details = item.statusText === "In Progress"
? [
`<div>${item.billTitle || ""}</div>`,
`<div class="row"><span>${t("team_salary")}: $${item.teamSalaryAmount || 0}</span><span>${t("bd_income_salary")}: $${item.bdIncomeSalary || 0} (${item.bdRatioSalary || 0}%)</span></div>`,
`<div class="row"><span>${t("team_top_up")}: $${item.teamRechargeAmount || 0}</span><span>${t("bd_income_top_up")}: $${item.bdIncomeRecharge || 0} (${item.bdRatioRecharge || 0}%)</span></div>`,
`<div>${t(settings.countLabel)}: ${item.agencyNumber || item.bdNumber || 0}</div>`
]
: [
`<div>${item.billTitle || ""}</div>`,
item.hitPolicyType === "TEAM_SALARY" ? `<div>${t("team_salary")}: $${item.teamSalaryAmount || 0}</div><div>${t("bd_income_salary")}: $${item.settlementSalary || 0} (${item.commissionRate || 0}%)</div>` : "",
item.hitPolicyType === "TEAM_RECHARGE" ? `<div>${t("team_top_up")}: $${item.teamRechargeAmount || 0}</div><div>${t("bd_income_top_up")}: $${item.settlementSalary || 0} (${item.commissionRate || 0}%)</div>` : "",
`<div>${t("num_teams") || t(settings.countLabel)} ${item.agencyNumber || item.bdNumber || 0}</div>`
];
card.innerHTML = `<div class="status-tag">${statusText(item.statusText)}</div>${details.join("")}`;
if (item.billBelong) {
const more = document.createElement("button");
more.className = "more-btn";
more.type = "button";
more.textContent = t("more");
more.addEventListener("click", () => showMore(item.billBelong, settings));
card.appendChild(more);
}
return card;
}
async function showMore(billBelong, settings) {
try {
const response = await get(settings.moreEndpoint(billBelong));
const data = response.body || {};
const sheet = document.createElement("section");
sheet.className = "sheet";
sheet.innerHTML = `
<div class="sheet-title">${data.period || ""}</div>
<div class="row card card-pad">
<span>${t("team_top_up")}: $${data.teamTotalRecharge || data.teamRecharge || 0}</span>
<span>${t("total_team_salary")}: $${data.teamTotalSalary || data.teamSalary || 0}</span>
</div>
<div id="moreMembers"></div>
`;
createMask(sheet);
const list = sheet.querySelector("#moreMembers");
(data.memberDetails || []).forEach((item) => {
const card = document.createElement("article");
card.className = "member-card";
card.innerHTML = `
<img class="avatar" src="${item.userAvatar || item.bdUserAvatar || ""}" alt="">
<div class="member-main">
<div class="member-name">${item.userName || item.bdUserName || "-"}</div>
<div class="member-meta">${t("user_id_prefix")}${item.account || "-"}</div>
</div>
<div class="member-side">
<div>${t("salary")}: $${item.salary || 0}</div>
<div>${t("recharge")}: $${item.recharge || 0}</div>
<div>${t("host")}: ${item.hostCount || item.agencyCount || 0}</div>
</div>
`;
avatarError(card.querySelector("img"));
list.appendChild(card);
});
} catch (error) {
toast(error.response?.errorMsg || error.message);
}
}

View File

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

View File

@ -0,0 +1,381 @@
import { applyLang, bindHeader, connectToApp, createMask, get, money, navigateTo, post, safeJsonArray, t, toast } from "../bd-static/shared.js";
const query = new URLSearchParams(window.location.search);
const state = {
identity: readIdentity(),
activeAction: query.get("activeAction") || "Exchange",
salaryTab: 0,
balances: { ADMIN_SALARY: {}, BD_SALARY: {}, BD_LEADER_SALARY: {}, TOTAL: {} },
payee: readPayee(),
kyc: null,
bankCard: null,
transferId: null,
exchangeId: null,
cashAmount: null
};
const transfers = [
{ 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 exchanges = transfers.map((item) => ({ id: item.id, coins: item.amount, cash: item.price }));
const actions = [
{ name: "Exchange", lang: "exchange" },
{ name: "Transfer", lang: "transfer" },
{ name: "Cash out", lang: "cash_out" }
];
bindHeader("available_salaries");
applyLang();
document.getElementById("detailsButton").addEventListener("click", () => navigateTo("/income-details"));
async function init() {
try {
await connectToApp();
await Promise.all([loadBalances(), loadKyc(), loadCards()]);
} catch (error) {
toast(error.response?.errorMsg || error.message || t("failed_to_load_balance"));
} finally {
render();
}
}
function readIdentity() {
try {
const data = JSON.parse(localStorage.getItem("identity") || "{}");
return data.identity || data.primaryRole || localStorage.getItem("userIdentity") || "BD";
} catch {
return "BD";
}
}
function readPayee() {
for (const storage of [sessionStorage, localStorage]) {
try {
const value = storage.getItem("payee");
if (value) return JSON.parse(value);
} catch {}
}
return { id: null, account: "", name: "", avatar: "", type: "", isHost: false, isAgency: false, hasSalary: false };
}
async function loadBalances() {
if (state.identity === "ADMIN") {
state.balances.ADMIN_SALARY = await balance("ADMIN_SALARY");
} else if (state.identity === "BD_LEADER") {
const [total, leader, bd] = await Promise.all([get("/wallet/salary-account/balance/total"), balance("BD_LEADER_SALARY"), balance("BD_SALARY")]);
state.balances.TOTAL = total.body || {};
state.balances.BD_LEADER_SALARY = leader;
state.balances.BD_SALARY = bd;
} else {
state.balances.BD_SALARY = await balance("BD_SALARY");
}
}
async function balance(type) {
const response = await get(`/wallet/salary-account/balance?salaryType=${type}`);
return response.status && response.body ? response.body : {};
}
async function loadKyc() {
const response = await get("/wallet/withdraw-info/list");
const body = response.status && Array.isArray(response.body) ? response.body : [];
state.kyc = body.length > 0 ? { ...body[0], previewUrls: safeJsonArray(body[0].passportFrontUrl) } : null;
}
async function loadCards() {
const response = await get("/wallet/bank-card/list");
const body = response.status && Array.isArray(response.body) ? response.body : [];
state.bankCard = body.find((item) => item.use) || null;
}
function salaryType() {
if (state.identity === "ADMIN") return "ADMIN_SALARY";
if (state.identity === "BD_LEADER") return state.salaryTab === 0 ? "BD_SALARY" : "BD_LEADER_SALARY";
return "BD_SALARY";
}
function visibleBalance() {
if (state.identity === "BD_LEADER") return state.balances.TOTAL.availableBalance || 0;
return state.balances[salaryType()]?.availableBalance || 0;
}
function operatingBalance() {
return state.balances[salaryType()]?.availableBalance || 0;
}
function render() {
document.getElementById("totalBalance").textContent = `$${money(visibleBalance())}`;
renderIdentitySwitch();
renderActions();
renderPanel();
}
function renderIdentitySwitch() {
const holder = document.getElementById("identitySwitch");
if (state.identity !== "BD_LEADER") {
holder.hidden = true;
return;
}
holder.hidden = false;
const rows = [
{ label: t("bd"), detail: t("my_bd_salary"), salary: state.balances.BD_SALARY.availableBalance || 0 },
{ label: t("bd_leader"), detail: t("my_bd_leader_salary"), salary: state.balances.BD_LEADER_SALARY.availableBalance || 0 }
];
holder.innerHTML = `
<div class="row" style="gap:8px;margin-bottom:10px">
${rows.map((row, index) => `<button class="action-tab ${state.salaryTab === index ? "active" : ""}" data-salary="${index}" type="button">${row.label}</button>`).join("")}
</div>
<div><b>${rows[state.salaryTab].detail}</b></div>
<div style="font-size:1.35em;font-weight:700">$${money(rows[state.salaryTab].salary)}</div>
`;
holder.querySelectorAll("[data-salary]").forEach((button) => {
button.addEventListener("click", () => {
state.salaryTab = Number(button.dataset.salary);
state.transferId = null;
state.exchangeId = null;
render();
});
});
}
function renderActions() {
const nav = document.getElementById("actionTabs");
nav.innerHTML = actions.map((item) => `<button class="action-tab ${state.activeAction === item.name ? "active" : ""}" data-action-name="${item.name}" type="button">${t(item.lang)}</button>`).join("");
nav.querySelectorAll("[data-action-name]").forEach((button) => {
button.addEventListener("click", () => {
state.activeAction = button.dataset.actionName;
state.transferId = null;
state.exchangeId = null;
render();
});
});
}
function renderPanel() {
if (state.activeAction === "Transfer") renderTransfer();
else if (state.activeAction === "Cash out") renderCashOut();
else renderExchange();
}
function renderTransfer() {
const panel = document.getElementById("actionPanel");
panel.innerHTML = `
<div class="amount-grid">
${transfers.map((item) => `<button class="amount-card ${state.transferId === item.id ? "active" : ""}" data-transfer="${item.id}" type="button">$${item.price}<br><span class="subtle">${item.amount}</span></button>`).join("")}
</div>
<button class="menu-link" id="payeeButton" type="button">
${state.payee?.id ? payeeHtml(state.payee) : `<span>${t("please_select_payee")}</span>`}
<img class="arrow" src="../bd-static/icons/arrow.svg" alt="">
</button>
<div style="display:flex;justify-content:center"><button class="primary-btn" id="transferButton" type="button">${t("transfer")}</button></div>
`;
panel.querySelectorAll("[data-transfer]").forEach((button) => button.addEventListener("click", () => {
const id = Number(button.dataset.transfer);
state.transferId = state.transferId === id ? null : id;
renderTransfer();
}));
document.getElementById("payeeButton").addEventListener("click", () => navigateTo("/search-payee?from=/available-income&activeAction=Transfer"));
document.getElementById("transferButton").addEventListener("click", submitTransfer);
}
function payeeHtml(payee) {
return `
<span class="payee">
<img class="avatar" src="${payee.avatar || ""}" alt="">
<span class="member-main">
<span class="member-name">${payee.name || "-"}</span>
<span class="member-meta">${t("user_id_prefix")}${payee.account || "-"}</span>
</span>
</span>
`;
}
function renderExchange() {
const panel = document.getElementById("actionPanel");
panel.innerHTML = `
<div class="amount-grid">
${exchanges.map((item) => `<button class="amount-card ${state.exchangeId === item.id ? "active" : ""}" data-exchange="${item.id}" type="button">$${item.cash}<br><span class="subtle">${item.coins}</span></button>`).join("")}
</div>
<div style="display:flex;justify-content:center;margin-top:12px"><button class="primary-btn" id="exchangeButton" type="button">${t("exchange")}</button></div>
`;
panel.querySelectorAll("[data-exchange]").forEach((button) => button.addEventListener("click", () => {
const id = Number(button.dataset.exchange);
state.exchangeId = state.exchangeId === id ? null : id;
renderExchange();
}));
document.getElementById("exchangeButton").addEventListener("click", confirmExchange);
}
function renderCashOut() {
const panel = document.getElementById("actionPanel");
const status = state.kyc?.status;
panel.innerHTML = `
<div style="display:flex;align-items:flex-end;margin-bottom:10px">
<b>${t("kyc")}</b>${state.kyc ? `: ${statusLabel(status)}` : ""}
</div>
<article class="card card-pad">
${state.kyc ? kycHtml(state.kyc) : `<button class="menu-link" id="completeKyc" type="button"><span>${t("complete_information")}</span><img class="arrow" src="../bd-static/icons/arrow.svg" alt=""></button>`}
</article>
<div style="display:flex;justify-content:center;margin-top:12px">
<button class="primary-btn" id="cashButton" type="button" ${status === "PASS" ? "" : "disabled"}>${t("cash_out")}</button>
</div>
`;
const completeKyc = document.getElementById("completeKyc");
if (completeKyc) completeKyc.addEventListener("click", () => navigateTo("/bank-cards?from=/available-income&activeAction=Cash out"));
document.getElementById("cashButton").addEventListener("click", () => {
if (state.kyc && state.kyc.status === "PASS") openCashSheet();
else if (!state.kyc) openImproveDialog();
});
}
function statusLabel(status) {
if (status === "PASS") return `<span class="badge pass">${t("review_passed")}</span>`;
if (status === "NOT_PASS") return `<span class="badge fail">${t("review_failed")}</span>`;
if (status === "PENDING") return `<span class="badge wait">${t("awaiting_review")}</span>`;
return "";
}
function kycHtml(item) {
return `
<div style="display:flex;flex-direction:column;gap:8px">
<div><b>${t("passport_id_card_title")}:</b></div>
<div style="display:flex;gap:8px;flex-wrap:wrap">${(item.previewUrls || []).map((src) => `<img src="${src}" alt="" style="width:15vw;aspect-ratio:1/1;object-fit:cover;border-radius:10px">`).join("")}</div>
<div><b>${t("contact_number")}:</b></div><div class="subtle">${item.contactNumber || ""}</div>
<div><b>${t("other_description")}:</b></div><div class="subtle">${item.otherDescription || ""}</div>
</div>
`;
}
async function submitTransfer() {
const item = transfers.find((entry) => entry.id === state.transferId);
if (!item) return toast(t("please_select_amount"));
if (!state.payee?.id) return toast(t("please_select_payee"));
try {
const response = await post("/wallet/salary-account/transferGold", { amount: item.price, acceptUserId: state.payee.id, salaryType: salaryType() });
if (response.status) {
toast(t("transfer_dollar_success"));
state.transferId = null;
await loadBalances();
render();
}
} catch (error) {
toast(error.response?.errorMsg || t("transfer_error"));
}
}
function confirmExchange() {
if (!state.exchangeId) return toast(t("please_select_coin_amount"));
const panel = document.createElement("section");
panel.className = "card card-pad";
panel.style.cssText = "width:80%;margin:32vh auto 0;text-align:center";
panel.innerHTML = `
<div style="font-size:1.3em;font-weight:700">${t("tips")}</div>
<div class="subtle" style="margin:8px 0 16px">${t("confirm_exchange")}</div>
<div class="row">
<button class="soft-btn" id="cancelExchange" type="button">${t("cancel")}</button>
<button class="primary-btn" id="confirmExchange" type="button">${t("confirm")}</button>
</div>
`;
const mask = createMask(panel);
panel.querySelector("#cancelExchange").addEventListener("click", () => mask.remove());
panel.querySelector("#confirmExchange").addEventListener("click", () => submitExchange(mask));
}
async function submitExchange(mask) {
const item = exchanges.find((entry) => entry.id === state.exchangeId);
try {
const response = await post("/wallet/salary-account/exchange/gold", { amount: item.cash, salaryType: salaryType() });
if (response.status === true || response.errorCode === 0) {
mask.remove();
toast(t("exchange_success"));
state.exchangeId = null;
await loadBalances();
render();
}
} catch (error) {
mask.remove();
toast(error.response?.errorMsg || t("exchange_error_occurred"));
}
}
function openImproveDialog() {
const panel = document.createElement("section");
panel.className = "card card-pad";
panel.style.cssText = "width:80%;margin:30vh auto 0;text-align:center";
panel.innerHTML = `
<div style="font-size:1.3em;font-weight:700">${t("prompt")}</div>
<div style="font-weight:700;margin-top:8px">${t("fill_info_tips")}</div>
<div class="subtle">${t("card_issue_tips")}</div>
<button class="primary-btn" id="improveButton" type="button" style="margin-top:12px">${t("to_improve")}</button>
`;
createMask(panel);
panel.querySelector("#improveButton").addEventListener("click", () => navigateTo("/bank-cards?from=/available-income&activeAction=Cash out"));
}
function openCashSheet() {
const panel = document.createElement("section");
panel.className = "sheet";
panel.innerHTML = `
<div class="sheet-title">${t("cash_out")}</div>
<div class="card card-pad">
<div><b>${t("withdrawal_method")}:</b> ${state.bankCard?.cardType || ""}</div>
${state.bankCard ? `<div><b>${t("card_number")}:</b> ${state.bankCard.cardNo || ""}</div>` : ""}
<button class="menu-link" id="changeCard" type="button">
<span>${state.bankCard ? t("change_withdrawal_method") : t("please_select_bank_card")}</span>
<img class="arrow" src="../bd-static/icons/arrow.svg" alt="">
</button>
</div>
<div class="input-box" style="margin-top:12px"><input class="field" id="cashInput" inputmode="decimal" dir="ltr" placeholder="${t("enter_cash_out_amount")}"></div>
<div class="subtle" style="margin-top:8px">${t("cash_out_minimum")}</div>
<div style="display:flex;justify-content:center"><button class="primary-btn" id="submitCash" type="button" style="margin-top:12px;width:80%">${t("cash_out")}</button></div>
`;
const mask = createMask(panel);
const input = panel.querySelector("#cashInput");
input.addEventListener("input", () => {
let value = input.value.replace(/[^\d.]/g, "");
const parts = value.split(".");
if (parts.length > 2) value = `${parts[0]}.${parts.slice(1).join("")}`;
if (Number(value) > operatingBalance()) value = String(operatingBalance());
input.value = value;
state.cashAmount = Number(value) || null;
});
panel.querySelector("#changeCard").addEventListener("click", () => navigateTo("/bank-cards?from=/available-income&activeAction=Cash out"));
panel.querySelector("#submitCash").addEventListener("click", () => submitCash(mask));
}
async function submitCash(mask) {
if (!(state.kyc?.status === "PASS" && state.bankCard?.id && state.cashAmount >= 50)) {
if (!state.bankCard?.id && state.cashAmount >= 50) {
mask.remove();
openImproveDialog();
}
return;
}
try {
const response = await post("/wallet/bank/withdraw", { amount: state.cashAmount, acceptBankCardId: state.bankCard.id, salaryType: salaryType() });
if (response.status) {
mask.remove();
await loadBalances();
openSuccessDialog();
render();
}
} catch (error) {
mask.remove();
toast(t("something_went_wrong"));
}
}
function openSuccessDialog() {
const panel = document.createElement("section");
panel.className = "card card-pad";
panel.style.cssText = "width:80%;margin:34vh auto 0;text-align:center";
panel.innerHTML = `<div style="font-size:1.3em;font-weight:700">${t("tips")}</div><div style="font-weight:700;margin-top:8px">${t("cash_withdrawal_tips")}</div>`;
createMask(panel);
}
init();

View File

@ -1,13 +1,35 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <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"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Available Income</title> <title>Available Salaries</title>
<link rel="stylesheet" href="../logic-pages/app.css"> <link rel="stylesheet" href="../bd-static/shared.css">
<style>
.tabs-actions { display:flex; justify-content:space-around; align-items:center; margin:12px 0; }
.action-tab { min-width:30%; padding:8px 10px; border-radius:999px; color:#32463a; font-weight:700; }
.action-tab.active { background:#bfe8cf; color:#165b35; }
.amount-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; }
.amount-card { min-height:64px; border:1px solid #d7eadf; border-radius:12px; background:#fff; color:#131111; font-weight:700; }
.amount-card.active { border-color:#32a96b; background:#e5f6eb; color:#165b35; }
.payee { display:flex; align-items:center; gap:10px; width:100%; }
</style>
</head> </head>
<body> <body>
<script>window.PAGE_CONFIG = {"title":"Available Income","mode":"walletAction"};</script> <main class="page">
<script src="../logic-pages/app.js"></script> <section class="content">
<div class="row">
<h2 class="section-title" data-i18n="available_salaries">Available salaries</h2>
<button class="subtle" type="button" id="detailsButton" data-i18n="details">Details</button>
</div>
<article class="card card-pad">
<strong id="totalBalance" style="font-size:1.2em">$0</strong>
</article>
<article class="card card-pad" id="identitySwitch" hidden></article>
<nav class="tabs-actions" id="actionTabs"></nav>
<section class="card card-pad" id="actionPanel"></section>
</section>
</main>
<script type="module" src="./available-income.js"></script>
</body> </body>
</html> </html>

104
h5/bank-card/bank-card.js Normal file
View File

@ -0,0 +1,104 @@
import { applyLang, bindHeader, connectToApp, createMask, get, getUserId, post, t, toast } from "../bd-static/shared.js";
let ways = [];
let cards = [];
let form = { userId: getUserId(), cardType: "", cardName: "", cardNo: "", payee: "" };
bindHeader("bank_card");
applyLang();
document.getElementById("addButton").addEventListener("click", openAddSheet);
connectToApp().then(loadData);
async function loadData() {
try {
const [wayResponse, cardResponse] = await Promise.all([get("/wallet/bank/withdrawal_ways"), get("/wallet/bank-card/list")]);
ways = wayResponse.body || [];
cards = cardResponse.body || [];
render();
} catch (error) {
toast(error.response?.errorMsg || error.message);
}
}
function render() {
const current = cards.find((item) => item.use);
document.getElementById("currentCard").innerHTML = current
? `<div>${t("payee")}: ${current.payee || "-"}</div><div class="subtle">${t("card_number")}: ${current.cardNo || "-"}</div>`
: `<div class="subtle">${t("no_bank_cards_in_use")}</div>`;
const list = document.getElementById("cardList");
list.innerHTML = "";
if (!cards.length) {
list.innerHTML = `<article class="card card-pad"><div class="subtle">${t("no_bank_cards_added")}</div></article>`;
return;
}
cards.forEach((item) => {
const card = document.createElement("article");
card.className = "card card-pad";
card.style.marginBottom = "10px";
card.innerHTML = `
<div>${t("payee")}: ${item.payee || "-"}</div>
<div class="subtle">${t("card_number")}: ${item.cardNo || "-"}</div>
<div class="row" style="margin-top:10px">
<button class="danger-btn" type="button" data-delete>${t("delete")}</button>
${item.use ? `<button class="soft-btn" type="button" disabled>${t("using")}</button>` : `<button class="primary-btn" type="button" data-use>${t("use")}</button>`}
</div>
`;
card.querySelector("[data-delete]").addEventListener("click", () => deleteCard(item.id));
const use = card.querySelector("[data-use]");
if (use) use.addEventListener("click", () => useCard(item.id));
list.appendChild(card);
});
}
function openAddSheet() {
form = { userId: getUserId(), cardType: "", cardName: "", cardNo: "", payee: "" };
const sheet = document.createElement("section");
sheet.className = "sheet";
sheet.innerHTML = `
<div class="sheet-title">${t("add_new_payment_method")}</div>
<div class="subtle">${t("withdrawal_method")}:</div>
<div class="input-box"><select class="field" id="cardType"><option value="">${t("choose_withdrawal_method")}</option>${ways.map((way) => `<option value="${way}">${way}</option>`).join("")}</select></div>
<div id="cardNameWrap" hidden><div class="subtle">${t("card_name")}:</div><div class="input-box"><input class="field" id="cardName" placeholder="${t("enter_card_name")}"></div></div>
<div class="subtle">${t("card_number")}:</div><div class="input-box"><input class="field" id="cardNo" placeholder="${t("enter_card_number")}"></div>
<div class="subtle">${t("payee")}:</div><div class="input-box"><input class="field" id="payee" placeholder="${t("enter_payee")}"></div>
<div style="display:flex;justify-content:center;margin-top:10px"><button class="primary-btn" id="confirmAdd" type="button">${t("confirm")}</button></div>
`;
createMask(sheet);
const type = sheet.querySelector("#cardType");
type.addEventListener("change", () => {
form.cardType = type.value;
form.cardName = type.value === "USDT" ? "USDT" : "";
sheet.querySelector("#cardNameWrap").hidden = type.value !== "BANK";
});
sheet.querySelector("#confirmAdd").addEventListener("click", async () => {
form.cardName = type.value === "BANK" ? sheet.querySelector("#cardName").value.trim() : form.cardName;
form.cardNo = sheet.querySelector("#cardNo").value.trim();
form.payee = sheet.querySelector("#payee").value.trim();
if (!form.cardType || !form.cardName || !form.cardNo || !form.payee) return;
try {
await post("/wallet/bank-card/add", form);
document.getElementById("modalMask").remove();
await loadData();
} catch (error) {
toast(error.response?.errorMsg || error.message);
}
});
}
async function deleteCard(id) {
try {
await post("/wallet/bank-card/delete", { id });
await loadData();
} catch (error) {
toast(error.response?.errorMsg || error.message);
}
}
async function useCard(id) {
try {
await post("/wallet/bank-card/use", { id });
await loadData();
} catch (error) {
toast(error.response?.errorMsg || error.message);
}
}

View File

@ -1,13 +1,24 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <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"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Bank Card</title> <title>Bank Card</title>
<link rel="stylesheet" href="../logic-pages/app.css"> <link rel="stylesheet" href="../bd-static/shared.css">
</head> </head>
<body> <body>
<script>window.PAGE_CONFIG = {"title":"Bank Card","mode":"bankCards"};</script> <main class="page">
<script src="../logic-pages/app.js"></script> <section class="content">
<div class="section-title" data-i18n="currently_in_use">Currently in use</div>
<article class="card card-pad" id="currentCard"></article>
<div class="section-title" data-i18n="all_bank_cards">All bank cards</div>
<div id="cardList"></div>
</section>
<div style="height:64px"></div>
<div style="position:fixed;left:0;right:0;bottom:10px;display:flex;justify-content:center">
<button class="primary-btn" type="button" id="addButton" data-i18n="add_new_payment_method">Add new payment method</button>
</div>
</main>
<script type="module" src="./bank-card.js"></script>
</body> </body>
</html> </html>

102
h5/bank-cards/bank-cards.js Normal file
View File

@ -0,0 +1,102 @@
import { applyLang, bindHeader, connectToApp, get, navigateTo, safeJsonArray, t, toast } from "../bd-static/shared.js";
const state = { card: null, kyc: null };
const bankCard = document.getElementById("bankCard");
const moreCards = document.getElementById("moreCards");
const kycCard = document.getElementById("kycCard");
const kycStatus = document.getElementById("kycStatus");
const kycMore = document.getElementById("kycMore");
const kycColon = document.getElementById("kycColon");
const query = new URLSearchParams(window.location.search);
bindHeader("bank_cards_link");
applyLang();
moreCards.addEventListener("click", () => navigateTo("/bank-card"));
kycMore.addEventListener("click", () => navigateTo("/KYC"));
async function init() {
try {
await connectToApp();
await Promise.all([loadCards(), loadKyc()]);
} catch (error) {
toast(error.response?.errorMsg || error.message);
} finally {
render();
}
}
async function loadCards() {
const response = await get("/wallet/bank-card/list");
const body = response.status && Array.isArray(response.body) ? response.body : [];
state.card = body.find((item) => item.use) || null;
}
async function loadKyc() {
const response = await get("/wallet/withdraw-info/list");
const body = response.status && Array.isArray(response.body) ? response.body : [];
state.kyc = body.length > 0 ? { ...body[0], previewUrls: safeJsonArray(body[0].passportFrontUrl) } : null;
}
function render() {
moreCards.hidden = !state.card;
bankCard.innerHTML = state.card
? `
<div style="display:flex;flex-direction:column;gap:8px">
<div><b>${t("category")}</b></div>
<div class="subtle">${state.card.cardType || ""}</div>
<div><b>${t("card_number_label")}</b></div>
<div class="subtle">${state.card.cardNo || ""}</div>
<div><b>${t("payee")}:</b></div>
<div class="subtle">${state.card.payee || ""}</div>
</div>
`
: `
<button class="menu-link" type="button" id="addCard">
<span>${t("add_payment_method")}</span>
<img class="arrow" src="../bd-static/icons/arrow.svg" alt="">
</button>
`;
const addCard = document.getElementById("addCard");
if (addCard) addCard.addEventListener("click", () => navigateTo("/bank-card"));
kycColon.hidden = !state.kyc;
kycMore.hidden = !state.kyc;
kycStatus.innerHTML = statusHtml(state.kyc?.status);
kycCard.innerHTML = state.kyc ? kycDetails(state.kyc) : `
<button class="menu-link" type="button" id="completeKyc">
<span>${t("complete_information")}</span>
<img class="arrow" src="../bd-static/icons/arrow.svg" alt="">
</button>
`;
const completeKyc = document.getElementById("completeKyc");
if (completeKyc) completeKyc.addEventListener("click", () => navigateTo("/KYC"));
const from = query.get("from");
const activeAction = query.get("activeAction");
if (from || activeAction) {
sessionStorage.setItem("bankCardsBackQuery", JSON.stringify({ from, activeAction }));
}
}
function statusHtml(status) {
if (status === "PASS") return `<span class="badge pass">${t("review_passed")}</span>`;
if (status === "NOT_PASS") return `<span class="badge fail">${t("review_failed")}</span>`;
if (status === "PENDING") return `<span class="badge wait">${t("awaiting_review")}</span>`;
return "";
}
function kycDetails(item) {
const images = (item.previewUrls || []).map((src) => `<img src="${src}" alt="" style="width:15vw;aspect-ratio:1/1;object-fit:cover;border-radius:10px">`).join("");
return `
<div style="display:flex;flex-direction:column;gap:8px">
<div><b>${t("passport_id_card_title")}:</b></div>
<div style="display:flex;gap:8px;flex-wrap:wrap">${images}</div>
<div><b>${t("contact_number")}:</b></div>
<div class="subtle">${item.contactNumber || ""}</div>
<div><b>${t("other_description")}:</b></div>
<div class="subtle">${item.otherDescription || ""}</div>
</div>
`;
}
init();

View File

@ -1,13 +1,34 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <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"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Bank Cards</title> <title>Bank Cards</title>
<link rel="stylesheet" href="../logic-pages/app.css"> <link rel="stylesheet" href="../bd-static/shared.css">
</head> </head>
<body> <body>
<script>window.PAGE_CONFIG = {"title":"Bank Cards","mode":"bankCards"};</script> <main class="page">
<script src="../logic-pages/app.js"></script> <section class="content">
<section>
<div class="row" style="margin-bottom:8px">
<h2 class="section-title" data-i18n="bank_card">Bank Card</h2>
<button class="subtle" type="button" id="moreCards" hidden data-i18n="more">More</button>
</div>
<article class="card card-pad" id="bankCard"></article>
</section>
<section>
<div class="row" style="margin-bottom:8px">
<div style="display:flex;align-items:center;gap:4px">
<h2 class="section-title" data-i18n="kyc">KYC</h2>
<span id="kycColon" hidden>:</span>
<span id="kycStatus"></span>
</div>
<button type="button" id="kycMore" hidden><img class="arrow" src="../bd-static/icons/arrow.svg" alt=""></button>
</div>
<article class="card card-pad" id="kycCard"></article>
</section>
</section>
</main>
<script type="module" src="./bank-cards.js"></script>
</body> </body>
</html> </html>

View File

@ -1,13 +1,25 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <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"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>BD Item Distribution</title> <title>BD Item Distribution</title>
<link rel="stylesheet" href="../logic-pages/app.css"> <link rel="stylesheet" href="../bd-static/shared.css">
<style>
.tabs { position: relative; display: flex; }
.tab { flex: 1; padding: 10px 0; color: rgba(0,0,0,.45); font-size: 1.3em; font-weight: 700; text-align: center; }
.tab.active { color: #4b8f67; }
.tab-line { position: absolute; bottom: 0; width: 15px; height: 3px; border-radius: 3px; background: #77bd8d; transition: left .25s ease; }
.goods { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; padding: 15px; }
.goods-card { display: flex; flex-direction: column; align-items: center; padding: 10px; border: 1px solid #d7eadf; border-radius: 10px; background: #fff; box-shadow: 0 2px 8px rgba(0,0,0,.08); }
.goods-card img { width: 60%; aspect-ratio: 1/1; object-fit: contain; }
</style>
</head> </head>
<body> <body>
<script>window.PAGE_CONFIG = {"title":"BD Item Distribution","mode":"itemDistribution","admin":false};</script> <main class="page">
<script src="../logic-pages/app.js"></script> <div class="tabs" id="tabs"></div>
<div class="goods" id="goods"></div>
</main>
<script type="module" src="./item-distribution.js"></script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,128 @@
import { applyLang, bindHeader, connectToApp, createMask, get, post, t, toast } from "../bd-static/shared.js";
const state = {
tabs: [],
active: 0,
goods: [],
selected: null,
locked: true
};
const tabs = document.getElementById("tabs");
const goods = document.getElementById("goods");
const typeMap = { frames: "AVATAR_FRAME", vehicles: "RIDE", vip: "NOBLE_VIP" };
bindHeader("item_distribution");
applyLang();
async function init() {
try {
await connectToApp();
await loadPermissions();
renderTabs();
if (state.tabs.length) await loadGoods();
} catch (error) {
toast(error.response?.errorMsg || error.message);
}
}
async function loadPermissions() {
const response = await get("/sys/bd/list-permission");
const body = response.status && Array.isArray(response.body) ? response.body : [];
const next = [];
if (body.includes("AVATAR_FRAME")) next.push({ id: 1, name: "frames" });
if (body.includes("RIDE")) next.push({ id: 2, name: "vehicles" });
if (body.some((item) => item.startsWith("NOBLE_VIP"))) next.push({ id: 3, name: "vip" });
state.tabs = next;
state.active = 0;
}
async function loadGoods() {
const tab = state.tabs[state.active];
if (!tab) {
state.goods = [];
renderGoods();
return;
}
const response = await get(`/sys/bd/props?propsType=${typeMap[tab.name]}&currencyType=GOLD`);
state.goods = response.status && Array.isArray(response.body) ? response.body : [];
renderGoods();
}
function renderTabs() {
tabs.innerHTML = "";
state.tabs.forEach((tab, index) => {
const button = document.createElement("button");
button.type = "button";
button.className = `tab${state.active === index ? " active" : ""}`;
button.textContent = t(tab.name);
button.addEventListener("click", async () => {
if (state.active === index) return;
state.active = index;
renderTabs();
await loadGoods();
});
tabs.appendChild(button);
});
if (state.tabs.length) {
const line = document.createElement("div");
line.className = "tab-line";
line.style.left = `calc(${(state.active + 0.5) * (100 / state.tabs.length)}% - 7.5px)`;
tabs.appendChild(line);
}
}
function renderGoods() {
goods.innerHTML = "";
state.goods.forEach((item) => {
const card = document.createElement("button");
card.type = "button";
card.className = "goods-card";
card.innerHTML = `
<img src="${item.cover || ""}" alt="">
<span class="primary-btn" style="min-height:0;margin-top:10px;padding:5px 14px">${t("send")}</span>
`;
card.addEventListener("click", () => openSend(item));
goods.appendChild(card);
});
}
function openSend(item) {
state.selected = item;
state.locked = true;
const panel = document.createElement("section");
panel.className = "card card-pad";
panel.style.cssText = "width:90%;margin:20vh auto 0;display:flex;flex-direction:column;align-items:center;gap:12px";
panel.innerHTML = `
<img src="${item.cover || ""}" alt="" style="width:60%;aspect-ratio:1/1;object-fit:contain">
<div><span class="subtle">${t("validity")}:</span> <b>3${t("day")}</b></div>
<div class="row" style="width:100%">
<input class="field input-box" id="giftUserId" type="text" placeholder="${t("enter_user_id_placeholder")}">
<button class="primary-btn" id="confirmGift" type="button">${t("confirm")}</button>
</div>
`;
const mask = createMask(panel);
const input = panel.querySelector("#giftUserId");
panel.querySelector("#confirmGift").addEventListener("click", () => sendGift(input.value.trim(), mask));
}
async function sendGift(account, mask) {
if (!state.locked) return;
state.locked = false;
try {
const user = await get(`/user/user-profile/open-search?account=${encodeURIComponent(account)}`);
if (user.status && user.body) {
const response = await post("/sys/bd/send", { propsId: state.selected.id, acceptUserId: user.body.id });
if (response.errorCode === 0 && response.status) {
mask.remove();
toast(t("gift_delivery_successful"));
state.selected = null;
}
}
} catch (error) {
toast(error.response?.errorMsg || error.message);
state.locked = true;
}
}
init();

129
h5/bd-list/bd-list.js Normal file
View File

@ -0,0 +1,129 @@
import { applyLang, avatarError, bindHeader, connectToApp, createMask, get, money, statusClass, statusText, t, toast } from "../bd-static/shared.js";
bindHeader("bd_list_link");
applyLang();
document.getElementById("historyButton").addEventListener("click", showHistory);
init();
async function init() {
try {
await connectToApp();
const response = await get("/team/bd/member-bill/list?type=BDLEADER");
renderList(response.body || {});
} catch (error) {
toast(error.response?.errorMsg || error.message);
}
}
function renderList(data) {
document.getElementById("billTitle").textContent = data.billTitle || "";
document.getElementById("summaryLine").textContent = `${t("number_of_bd")}: ${data.memberCount || 0} ${t("team_salary")}: $${data.totalSalary || 0}`;
const list = document.getElementById("memberList");
list.innerHTML = "";
(data.memberBillList || []).forEach((item) => {
const card = document.createElement("article");
card.className = "member-card";
card.innerHTML = `
<img class="avatar" src="${item.agentAvatar || ""}" alt="">
<div class="member-main">
<div class="member-name">${item.agentName || "-"}</div>
<div class="member-meta">${t("user_id_prefix")}${item.agentAccount || "-"}</div>
</div>
<div class="member-side">
<div>${t("team_salary")}: $${item.teamSalaryAmount || 0}</div>
<div>${t("agencies")}: ${item.teamMemberCount || 0}</div>
</div>
`;
avatarError(card.querySelector("img"));
list.appendChild(card);
});
}
async function showHistory() {
try {
const historyResponse = await get("/team/bd/leader/history");
const listResponse = await get("/team/bd/member-bill/list?type=BDLEADER");
const history = historyResponse.body || {};
const bill = listResponse.body || {};
if (bill.bdHistoryCO) {
history.incomeDetails = history.incomeDetails || [];
history.incomeDetails.unshift(bill.bdHistoryCO);
}
const sheet = document.createElement("section");
sheet.className = "sheet";
sheet.innerHTML = `
<div class="sheet-title">${t("bd_leader_history")}</div>
<div class="card card-pad">
<div>${t("total_income")} $${money(history.totalIncome || 0)}</div>
<div>${t("previous_period_income")}: $${money(history.previousPeriodIncome || 0)}</div>
</div>
<div class="section-title">${t("income_details_label")}</div>
<div id="historyItems"></div>
`;
createMask(sheet);
const holder = sheet.querySelector("#historyItems");
(history.incomeDetails || []).forEach((item) => holder.appendChild(historyItem(item)));
} catch (error) {
toast(error.response?.errorMsg || error.message);
}
}
function historyItem(item) {
const card = document.createElement("article");
card.className = `salary-item ${statusClass(item.statusText)}`;
card.innerHTML = `
<div class="status-tag">${statusText(item.statusText)}</div>
<div>${item.billTitle || ""}</div>
<div class="row">
<span>${t("bd_number")}: ${item.bdNumber || 0}</span>
<span>${t("team_salary")}: $${item.teamSalaryAmount || 0}</span>
</div>
`;
if (item.statusText === "Completed") {
const more = document.createElement("button");
more.className = "more-btn";
more.type = "button";
more.textContent = t("more");
more.addEventListener("click", () => showMore(item.billBelong));
card.appendChild(more);
}
return card;
}
async function showMore(billBelong) {
try {
const response = await get(`/team/bd/leader/history/more?billBelong=${encodeURIComponent(billBelong)}`);
const data = response.body || {};
const sheet = document.createElement("section");
sheet.className = "sheet";
sheet.innerHTML = `
<div class="sheet-title">${data.period || ""}</div>
<div class="row card card-pad">
<span>${t("team_salary")}: $${data.teamSalary || 0}</span>
<span>${t("bd_number")}: ${data.bdNumber || 0}</span>
</div>
<div id="moreMembers"></div>
`;
createMask(sheet);
const list = sheet.querySelector("#moreMembers");
(data.memberDetails || []).forEach((item) => {
const card = document.createElement("article");
card.className = "member-card";
card.innerHTML = `
<img class="avatar" src="${item.bdUserAvatar || ""}" alt="">
<div class="member-main">
<div class="member-name">${item.bdUserName || "-"}</div>
<div class="member-meta">${t("user_id_prefix")}${item.account || "-"}</div>
</div>
<div class="member-side">
<div>${t("salary")}: $${item.salary || 0}</div>
<div>${t("agencies")}: ${item.agencyCount || 0}</div>
</div>
`;
avatarError(card.querySelector("img"));
list.appendChild(card);
});
} catch (error) {
toast(error.response?.errorMsg || error.message);
}
}

View File

@ -1,13 +1,24 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <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"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>BD List</title> <title>BD List</title>
<link rel="stylesheet" href="../logic-pages/app.css"> <link rel="stylesheet" href="../bd-static/shared.css">
</head> </head>
<body> <body>
<script>window.PAGE_CONFIG = {"title":"BD List","mode":"teamHistory","listEndpoint":"/team/bd/member-bill/list?type=BDLEADER","historyEndpoint":"/team/bd/leader/history","moreEndpoint":"/team/bd/leader/history/more?billBelong=","countLabel":"BD number","totalLabel":"Team salary","memberSide1":"Team salary","memberSide2":"Agencies"};</script> <main class="page">
<script src="../logic-pages/app.js"></script> <section class="content">
<article class="card card-pad">
<div class="row">
<div class="section-title" id="billTitle">-</div>
<button class="soft-btn" type="button" id="historyButton" data-i18n="bd_leader_history">BD Leader history</button>
</div>
<div class="row subtle" id="summaryLine"></div>
<div id="memberList"></div>
</article>
</section>
</main>
<script type="module" src="./bd-list.js"></script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="m9 5 7 7-7 7" stroke="#62a87c" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 190 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M15 5 8 12l7 7" stroke="#244b37" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 192 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<circle cx="11" cy="11" r="6.5" stroke="#5b7f68" stroke-width="2"/>
<path d="m16 16 4 4" stroke="#5b7f68" stroke-width="2.2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 234 B

388
h5/bd-static/shared.css Normal file
View File

@ -0,0 +1,388 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: "Baloo 2", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.4;
}
html,
body {
width: 100%;
min-height: 100%;
background: #f4fbf6;
color: #1f2937;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
}
button,
input,
textarea {
font: inherit;
}
button {
border: 0;
background: transparent;
color: inherit;
}
.page {
min-height: 100vh;
background: #f4fbf6;
}
.header {
position: sticky;
top: 0;
z-index: 20;
display: grid;
grid-template-columns: 44px 1fr 58px;
align-items: center;
min-height: calc(48px + env(safe-area-inset-top));
padding: env(safe-area-inset-top) 10px 0;
background: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.header h1 {
min-width: 0;
text-align: center;
font-size: 1.35em;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.icon-btn,
.lang-btn {
min-width: 36px;
height: 36px;
display: flex;
justify-content: center;
align-items: center;
font-weight: 700;
}
.icon-btn {
padding: 0;
}
.icon-btn img {
width: 24px;
height: 24px;
}
.lang-btn {
justify-self: end;
padding: 0 10px;
border-radius: 999px;
background: #e9f7ee;
color: #276344;
font-size: 0.86em;
}
.content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
}
.card {
border: 1px solid #d7eadf;
border-radius: 16px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.card-pad {
padding: 16px;
}
.section-title {
font-size: 1.35em;
font-weight: 700;
}
.subtle {
color: rgba(0, 0, 0, 0.45);
font-weight: 600;
}
.row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
}
.divider {
border-bottom: 1px solid #e6e6e6;
}
.member-card {
display: flex;
align-items: center;
gap: 10px;
padding: 12px;
border-radius: 12px;
background: #fff8df;
}
.member-card + .member-card {
margin-top: 10px;
}
.avatar {
width: 3.5em;
height: 3.5em;
flex: 0 0 auto;
border-radius: 50%;
object-fit: cover;
background: #e7efe9;
}
.member-main {
flex: 1;
min-width: 0;
}
.member-name {
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.member-meta,
.member-side {
color: rgba(0, 0, 0, 0.48);
font-size: 0.86em;
font-weight: 600;
}
.member-side {
flex: 0 0 auto;
text-align: right;
color: #1f2937;
}
.menu-link {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 14px 0;
text-align: left;
font-weight: 700;
}
.arrow {
width: 22px;
height: 22px;
flex: 0 0 auto;
}
.field {
width: 100%;
border: 0;
outline: 0;
background: transparent;
color: #1f2937;
font-weight: 600;
}
.input-box {
padding: 10px 12px;
border: 1px solid #d7eadf;
border-radius: 8px;
background: #fff;
}
.primary-btn,
.danger-btn,
.soft-btn {
min-height: 40px;
border-radius: 999px;
padding: 8px 16px;
color: #fff;
font-weight: 700;
text-align: center;
}
.primary-btn {
background: #32a96b;
}
.danger-btn {
background: #ef4444;
}
.soft-btn {
background: #9ca3af;
}
.primary-btn:disabled,
.soft-btn:disabled {
background: #cfd8d2;
}
.badge {
display: inline-flex;
padding: 3px 8px;
border-radius: 999px;
color: #fff;
background: #3d73ff;
font-size: 0.8em;
font-weight: 700;
}
.badge.pass,
.badge.success {
background: #36b56b;
}
.badge.fail,
.badge.pending {
background: #ef4444;
}
.badge.wait {
background: #d59a20;
}
.mask {
position: fixed;
inset: 0;
z-index: 50;
background: rgba(0, 0, 0, 0.35);
}
.sheet {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 51;
max-height: 82vh;
overflow: auto;
padding: 16px;
border-radius: 16px 16px 0 0;
background: #fffbe3;
}
.sheet-title {
margin-bottom: 10px;
color: #333;
font-size: 1.15em;
font-weight: 700;
text-align: center;
}
.salary-item {
position: relative;
margin-top: 8px;
padding: 32px 10px 30px;
border-radius: 8px;
background: #ebf1fd;
font-size: 0.9em;
}
.salary-item.pending {
background: #feebef;
}
.salary-item.completed {
background: #e8faed;
}
.salary-item.out {
background: #e9e9e9;
}
.status-tag {
position: absolute;
top: 0;
left: 0;
min-width: 7.5em;
padding: 2px 8px;
border-radius: 8px 0 8px 0;
background: #3d73ff;
color: #fff;
font-weight: 700;
text-align: center;
}
.salary-item.pending .status-tag {
background: #ff3d40;
}
.salary-item.completed .status-tag {
background: #32b968;
}
.salary-item.out .status-tag {
background: #9ca3af;
}
.more-btn {
position: absolute;
right: 10px;
bottom: 7px;
color: #4f46e5;
font-weight: 700;
}
.toast {
position: fixed;
left: 50%;
bottom: calc(28px + env(safe-area-inset-bottom));
z-index: 80;
max-width: 86vw;
transform: translateX(-50%);
padding: 10px 14px;
border-radius: 8px;
color: #fff;
background: rgba(0, 0, 0, 0.75);
font-size: 13px;
font-weight: 500;
text-align: center;
}
[hidden] {
display: none !important;
}
[dir="rtl"] .arrow,
[dir="rtl"] .flip-img {
transform: scaleX(-1);
}
[dir="rtl"] .member-side {
text-align: left;
}
@media screen and (max-width: 360px) {
.page {
font-size: 10px;
}
}
@media screen and (min-width: 360px) {
.page {
font-size: 12px;
}
}
@media screen and (min-width: 768px) {
.page {
font-size: 24px;
}
}

384
h5/bd-static/shared.js Normal file
View File

@ -0,0 +1,384 @@
export const API_BASE_URL = "https://jvapi.haiyihy.com";
const text = {
en: {
bd_leader_center: "BD Leader Center",
available_salaries: "Available salaries",
bd: "BD",
bd_leader: "BD Leader",
my_bd_salary: "My BD salary",
my_bd_leader_salary: "My BD leader salary",
exchange: "Exchange",
transfer: "Transfer",
cash_out: "Cash out",
details: "Details",
please_select_amount: "Please select amount",
please_select_coin_amount: "Please select coin amount",
please_select_payee: "Please select payee",
transfer_dollar_success: "Transfer succeeded",
transfer_dollar_failed: "Transfer failed",
transfer_error: "Transfer error",
exchange_success: "Exchange succeeded",
exchange_error_occurred: "Exchange failed",
amount: "Amount",
coins: "Coins",
dollar: "Dollar",
minimum_cash_out: "Minimum cash out is $50",
bd_policy_link: "BD policy",
bd_leader_policy: "BD leader policy",
bd_policy: "BD policy",
agency_list_link: "Agency list",
bd_list_link: "BD List",
invite_become_agent: "Invite to become agent",
invite_become_BD: "Invite to become BD",
bank_cards_link: "Bank cards",
bank_card: "Bank Card",
kyc: "KYC",
category: "Category",
card_number_label: "Card number",
card_number: "Card number",
payee: "Payee",
more: "More",
add_payment_method: "Add payment method",
complete_information: "Complete information",
passport_id_card_title: "Passport/ID card",
contact_number: "Contact number",
other_description: "Other description",
review_passed: "Review passed",
review_failed: "Review failed",
awaiting_review: "Awaiting review",
currently_in_use: "Currently in use",
all_bank_cards: "All bank cards",
no_bank_cards_in_use: "No bank cards in use",
no_bank_cards_added: "No bank cards added",
add_new_payment_method: "Add new payment method",
withdrawal_method: "Withdrawal method",
choose_withdrawal_method: "Choose withdrawal method",
card_name: "Card name",
enter_card_name: "Enter card name",
enter_card_number: "Enter card number",
enter_payee: "Enter payee",
delete: "Delete",
use: "Use",
using: "Using",
confirm: "Confirm",
submit: "Submit",
change_kyc: "Change KYC",
enter_contact_number: "Enter contact number",
enter_other_description: "Enter other description",
current_status: "Current status",
tips: "Tips",
confirm_modify_kyc: "Confirm modify KYC?",
cancel: "Cancel",
application_submitted: "Application submitted",
agency_number: "Agency number",
number_of_bd: "BD number",
bd_number: "BD number",
team_salary: "Team salary",
team_recharge: "Team recharge",
salary: "Salary",
recharge: "Recharge",
host: "Host",
agencies: "Agencies",
user_id_prefix: "ID: ",
revenue_details: "Revenue details",
bd_leader_history: "BD Leader history",
total_income: "Total income",
previous_period_income: "Previous period income",
income_details_label: "Income details",
team_top_up: "Team top up",
bd_income_salary: "BD income salary",
bd_income_top_up: "BD income top up",
total_team_salary: "Total team salary",
num_teams: "Num teams",
in_progress: "In Progress",
pending: "Pending",
completed: "Completed",
out_of_account: "Out of account",
information: "Information",
enter_user_id: "Enter User ID",
invite: "Invite",
no_users_found: "No users found",
success: "Success",
invitation_submitted: "Invitation submitted",
application_cancelled: "Application cancelled",
something_went_wrong: "Something went wrong",
cancellation_failed_try_again: "Cancellation failed, try again"
,
item_distribution: "Item distribution",
frames: "Frames",
vehicles: "Vehicles",
vip: "VIP",
send: "Send",
validity: "Validity",
day: " day",
enter_user_id_placeholder: "Enter user ID",
gift_delivery_successful: "Gift delivery successful",
available_salaries: "Available salaries",
details: "Details",
bd: "BD",
bd_leader: "BD Leader",
my_bd_salary: "My BD salary",
my_bd_leader_salary: "My BD leader salary",
exchange: "Exchange",
transfer: "Transfer",
cash_out: "Cash out",
please_select_amount: "Please select amount",
please_select_payee: "Please select payee",
transfer_dollar_success: "Transfer successful",
transfer_dollar_failed: "Transfer failed",
transfer_error: "Transfer error",
please_select_coin_amount: "Please select coin amount",
exchange_success: "Exchange successful",
exchange_error_occurred: "Exchange failed",
confirm_exchange: "Confirm exchange?",
fill_info_tips: "Please complete bank card and KYC information",
card_issue_tips: "Bank card information is required before cash out",
to_improve: "To improve",
cash_withdrawal_tips: "Cash withdrawal submitted",
enter_cash_out_amount: "Enter cash out amount",
cash_out_minimum: "Cash out minimum: $50",
change_withdrawal_method: "Change withdrawal method",
please_select_bank_card: "Please select bank card",
prompt: "Prompt",
failed_to_load_balance: "Failed to load balance"
},
ar: {},
tr: {},
bn: {}
};
["ar", "tr", "bn"].forEach((lang) => {
text[lang] = { ...text.en };
});
let headers = {
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",
"content-type": "application/json"
};
export const lang = resolveLanguage();
export function t(key) {
return text[lang]?.[key] || text.en[key] || key;
}
export function applyLang() {
document.documentElement.lang = lang;
document.documentElement.dir = lang === "ar" ? "rtl" : "ltr";
document.querySelectorAll("[data-i18n]").forEach((node) => {
node.textContent = t(node.dataset.i18n);
});
const language = document.querySelector("[data-action='language']");
if (language) language.textContent = lang.toUpperCase();
}
function resolveLanguage() {
const params = new URLSearchParams(window.location.search);
const hash = window.location.hash.includes("?") ? window.location.hash.split("?")[1] : "";
const hashParams = new URLSearchParams(hash);
const value = params.get("lang") || hashParams.get("lang") || navigator.language || "en";
if (value.startsWith("ar")) return "ar";
if (value.startsWith("tr")) return "tr";
if (value.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 parseAppHeaders(raw) {
const data = JSON.parse(raw);
const appIntel = splitHeaderPairs(data["Req-App-Intel"]);
const sysOrigin = splitHeaderPairs(data["Req-Sys-Origin"]);
return {
authorization: data.Authorization,
reqLang: data["Req-Lang"],
appVersion: appIntel.version,
buildVersion: appIntel.build,
channel: appIntel.channel,
reqImei: appIntel["Req-Imei"],
origin: sysOrigin.origin,
child: sysOrigin.child
};
}
export function setHeadersFromApp(rawOrObject) {
const data = typeof rawOrObject === "string" ? parseAppHeaders(rawOrObject) : rawOrObject || {};
const next = {};
if (data.authorization) next.authorization = data.authorization;
if (data.reqLang) next["req-lang"] = data.reqLang;
if (data.appVersion || data.buildVersion || data.channel) {
next["req-app-intel"] = [
data.appVersion ? `version=${data.appVersion}` : "",
data.buildVersion ? `build=${data.buildVersion}` : "",
data.channel ? `channel=${data.channel}` : ""
].filter(Boolean).join(";");
}
if (data.reqImei) next["req-imei"] = data.reqImei;
if (data.origin) next["req-sys-origin"] = [`origin=${data.origin}`, data.child ? `child=${data.child}` : ""].filter(Boolean).join(";");
headers = { ...headers, ...next };
}
export async function connectToApp() {
window.renderData = (raw) => raw && setHeadersFromApp(raw);
window.getIosAccessOriginParam = window.renderData;
if (window.app && typeof window.app.getAccessOrigin === "function") {
const raw = window.app.getAccessOrigin();
if (raw) setHeadersFromApp(raw);
return;
}
if (window.FlutterApp && typeof window.FlutterApp.postMessage === "function") {
window.FlutterApp.postMessage("requestAccessOrigin");
}
setHeadersFromApp({ reqLang: lang, authorization: "" });
}
export async function request(method, path, body) {
const options = { method, headers };
if (body !== undefined) options.body = JSON.stringify(body);
const response = await fetch(`${API_BASE_URL}${path}`, options);
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;
}
export const get = (path) => request("GET", path);
export const post = (path, body) => request("POST", path, body);
export function navigateTo(target) {
const url = new URL(target, window.location.origin);
const currentLang = new URLSearchParams(window.location.search).get("lang");
if (currentLang && !url.searchParams.has("lang")) url.searchParams.set("lang", currentLang);
window.location.href = `${url.pathname}${url.search}${url.hash}`;
}
export 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");
}
}
export function bindHeader(titleKey) {
document.body.insertAdjacentHTML("afterbegin", `
<header class="header">
<button class="icon-btn" type="button" data-action="back" aria-label="Back"><img src="../bd-static/icons/back.svg" alt=""></button>
<h1>${t(titleKey)}</h1>
<button class="lang-btn" type="button" data-action="language">EN</button>
</header>
`);
document.querySelector("[data-action='back']").addEventListener("click", closePage);
document.querySelector("[data-action='language']").addEventListener("click", () => navigateTo("/language"));
applyLang();
}
export function money(value) {
if (value === null || value === undefined || value === "") return "0";
const number = Number(value);
if (Number.isNaN(number)) return String(value);
return Number.isInteger(number) ? String(number) : number.toFixed(2);
}
export function safeJsonArray(value) {
try {
const result = JSON.parse(value || "[]");
return Array.isArray(result) ? result : [];
} catch {
return [];
}
}
export function getUserId() {
const keys = ["userInfo", "user", "USER_INFO"];
for (const key of keys) {
try {
const raw = localStorage.getItem(key) || sessionStorage.getItem(key);
if (!raw) continue;
const data = JSON.parse(raw);
if (data.id || data.userId) return data.id || data.userId;
} catch {}
}
return "";
}
export function toast(message) {
let node = document.getElementById("toast");
if (!node) {
node = document.createElement("div");
node.id = "toast";
node.className = "toast";
document.body.appendChild(node);
}
node.textContent = message;
node.hidden = false;
window.clearTimeout(toast.timer);
toast.timer = window.setTimeout(() => {
node.hidden = true;
}, 2500);
}
export function createMask(content) {
const old = document.getElementById("modalMask");
if (old) old.remove();
const mask = document.createElement("div");
mask.id = "modalMask";
mask.className = "mask";
mask.addEventListener("click", () => mask.remove());
content.addEventListener("click", (event) => event.stopPropagation());
mask.appendChild(content);
document.body.appendChild(mask);
return mask;
}
export function statusClass(status) {
if (status === "Completed") return "completed";
if (status === "Pending") return "pending";
if (status === "Out of account" || status === "HANG_UP") return "out";
return "";
}
export function statusText(status) {
const map = {
SETTLED: t("completed"),
PAY_OUT: t("completed"),
UNPAID: t("in_progress"),
HANG_UP: t("out_of_account"),
"In Progress": t("in_progress"),
Pending: t("pending"),
Completed: t("completed"),
"Out of account": t("out_of_account")
};
return map[status] || status || "";
}
export function avatarError(img) {
img.addEventListener("error", () => {
img.removeAttribute("src");
});
}

View File

@ -0,0 +1,332 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: "Baloo 2", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.35;
}
html,
body {
width: 100%;
min-height: 100%;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-webkit-overflow-scrolling: touch;
-webkit-tap-highlight-color: transparent;
}
body {
color: rgba(0, 0, 0, 0.8);
background: #fff;
}
button {
border: 0;
background: transparent;
color: inherit;
font: inherit;
}
.exchange-gold-coins {
width: 100vw;
min-height: 100vh;
background-color: #fff;
background-image: url("/assets/secondBg-CSqvFWr0-1776148661447.png");
background-size: cover;
background-position: top center;
}
.page-header {
position: sticky;
top: 0;
z-index: 10;
display: grid;
grid-template-columns: 44px 1fr 58px;
align-items: center;
min-height: calc(48px + env(safe-area-inset-top));
padding: env(safe-area-inset-top) 10px 0;
background: rgba(255, 255, 255, 0.96);
}
.page-header h1 {
min-width: 0;
color: #111;
font-size: 1.25em;
font-weight: 700;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.back-button,
.lang-button {
min-width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
}
.back-button {
padding: 6px;
}
.back-button img {
width: 100%;
height: 100%;
}
.lang-button {
justify-self: end;
padding: 0 10px;
border-radius: 999px;
background: #dff5e7;
color: #1f6f46;
font-size: 0.86em;
font-weight: 700;
}
.content {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.section-title {
color: rgba(0, 0, 0, 0.8);
font-weight: 700;
}
.details-button {
display: flex;
align-items: center;
color: rgba(0, 0, 0, 0.42);
font-weight: 600;
}
.details-button img {
width: 16px;
margin-left: 4px;
}
.surface-card {
margin-bottom: 12px;
border: 1px solid rgba(194, 217, 240, 0.82);
border-radius: 20px;
background: #fff;
box-shadow: 0 8px 20px rgba(69, 129, 89, 0.12);
}
.salary-card {
padding: 16px;
}
.salary-label {
font-size: 1em;
font-weight: 600;
}
.salary-value {
margin-left: 6px;
color: #333;
font-size: 1.1em;
font-weight: 800;
}
.loading-text {
margin-left: 6px;
color: #1f6f46;
font-weight: 700;
}
.coins-title {
margin-bottom: 10px;
}
.coins-card {
padding: 16px;
}
.coin-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.coin-item {
aspect-ratio: 1 / 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 6px;
border: 2px solid #ffdb81;
border-radius: 12px;
background: #ffe5a1;
cursor: pointer;
transition: transform 0.2s, border-color 0.2s, background-color 0.2s;
}
.coin-item.selected {
border-color: #ffb700;
background: #fdca49;
}
.coin-item:active {
transform: scale(0.98);
}
.coin-item img {
width: 40%;
}
.coin-price {
color: #131111;
font-weight: 600;
}
.action-row {
display: flex;
justify-content: center;
margin-top: 10px;
}
.exchange-button {
width: 70%;
min-height: 44px;
border-radius: 32px;
background: #ff9500;
color: #fff;
font-weight: 700;
cursor: pointer;
}
.exchange-button:disabled {
background: #ccc;
cursor: not-allowed;
}
.modal-layer {
position: fixed;
inset: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.45);
}
.modal-card {
width: 80%;
max-width: 420px;
display: flex;
flex-direction: column;
align-items: center;
gap: 3vw;
padding: 12px;
border-radius: 12px;
background: #fff;
}
.modal-title {
color: rgba(0, 0, 0, 0.8);
font-size: 1.2em;
font-weight: 800;
}
.modal-message {
color: rgba(0, 0, 0, 0.42);
font-size: 1em;
font-weight: 700;
text-align: center;
}
.modal-actions {
width: 80%;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-primary,
.modal-secondary {
width: 45%;
min-height: 38px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 70px;
padding: 8px 12px;
font-weight: 800;
}
.modal-secondary {
border: 1px solid #ffb805;
background: #fff;
color: #ff9500;
}
.modal-primary {
background: #ff9500;
color: #fff;
}
.toast {
position: fixed;
left: 50%;
bottom: calc(22px + env(safe-area-inset-bottom));
z-index: 30;
max-width: calc(100% - 40px);
transform: translateX(-50%);
padding: 9px 13px;
border-radius: 999px;
background: rgba(22, 56, 36, 0.92);
color: #fff;
font-size: 0.9em;
font-weight: 700;
text-align: center;
}
[hidden] {
display: none !important;
}
[dir="rtl"] .details-button {
flex-direction: row-reverse;
}
[dir="rtl"] .details-button img {
margin-left: 0;
margin-right: 4px;
transform: scaleX(-1);
}
@media screen and (max-width: 360px) {
* {
font-size: 10px;
}
}
@media screen and (min-width: 360px) {
* {
font-size: 12px;
}
}
@media screen and (min-width: 768px) {
* {
font-size: 24px;
}
}

View File

@ -0,0 +1,442 @@
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();

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<circle cx="32" cy="32" r="26" fill="#FFD45A" stroke="#A07100" stroke-width="4"/>
<circle cx="32" cy="32" r="18" fill="#FFE79B" stroke="#C28A00" stroke-width="3"/>
<path d="M32 20v24M39 25h-9a4 4 0 1 0 0 8h4a4 4 0 1 1 0 8h-9" stroke="#8A6200" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 395 B

View File

@ -1,13 +1,61 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <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"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Exchange Coins</title> <title>Exchange Coins</title>
<link rel="stylesheet" href="../logic-pages/app.css"> <link rel="stylesheet" href="./exchange-gold-coins.css" />
</head> </head>
<body> <body>
<script>window.PAGE_CONFIG = {"title":"Exchange Coins","mode":"walletAction","action":"Exchange"};</script> <div class="exchange-gold-coins">
<script src="../logic-pages/app.js"></script> <header class="page-header">
<button class="back-button" type="button" data-action="back" aria-label="Back"><img id="backIcon" alt="" /></button>
<h1 data-i18n="exchange_gold_coins">Exchange Coins</h1>
<button class="lang-button" type="button" data-action="language">EN</button>
</header>
<main class="content">
<section>
<div class="section-header">
<div class="section-title" data-i18n="information">Information</div>
<button class="details-button" type="button" data-route="/information-details">
<span data-i18n="details">Details</span>
<img id="detailsArrowIcon" alt="" />
</button>
</div>
<section class="surface-card salary-card">
<span class="salary-label" data-i18n="available_salaries">Available salaries</span>
<span class="loading-text" id="balanceLoading" hidden><span data-i18n="loading">Loading</span>...</span>
<span class="salary-value" id="balanceValue">0.00</span>
</section>
</section>
<section>
<div class="section-title coins-title" data-i18n="exchange_gold_coins">Exchange Coins</div>
<section class="surface-card coins-card">
<div class="coin-grid" id="coinGrid"></div>
</section>
<div class="action-row">
<button class="exchange-button" type="button" id="exchangeButton" disabled data-i18n="exchange">Exchange</button>
</div>
</section>
</main>
<div class="modal-layer" id="confirmModal" hidden>
<div class="modal-card">
<div class="modal-title" data-i18n="tips">Tips</div>
<div class="modal-message" data-i18n="confirm_exchange">Confirm exchange?</div>
<div class="modal-actions">
<button class="modal-secondary" type="button" data-action="cancel" data-i18n="cancel">Cancel</button>
<button class="modal-primary" type="button" data-action="confirm" data-i18n="confirm">Confirm</button>
</div>
</div>
</div>
<div class="toast" id="toast" hidden></div>
</div>
<script type="module" src="./exchange-gold-coins.js"></script>
</body> </body>
</html> </html>

View File

@ -1,5 +1,5 @@
const API_BASE_URL = "https://jvapi.haiyihy.com"; const API_BASE_URL = "https://jvapi.haiyihy.com";
const DEFAULT_AVATAR = "/assets/defaultAvatar-CdxWBK1k-1776148661448.png"; const DEFAULT_AVATAR = "/assets/defaultAvatar-CdxWBK1k.png";
const icons = { const icons = {
policy: "./icons/policy.svg", policy: "./icons/policy.svg",
exchange: "./icons/exchange.svg", exchange: "./icons/exchange.svg",
@ -214,16 +214,6 @@ function connectToApp() {
window.getIosAccessOriginParam = resolve; window.getIosAccessOriginParam = resolve;
if (!(window.app || window.webkit || window.FlutterPageControl)) { 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({ success: true, environment: "browser" }); resolve({ success: true, environment: "browser" });
return; return;
} }
@ -632,16 +622,16 @@ async function init() {
await connectToApp(); await connectToApp();
await fetchUserProfile(); await fetchUserProfile();
await Promise.all([ await Promise.all([
runTask(checkIdentityAndRedirect, "identity check"),
runTask(fetchTeamEntry, "team entry"), runTask(fetchTeamEntry, "team entry"),
runTask(fetchAgencyProfile, "agency profile"), runTask(fetchAgencyProfile, "agency profile"),
runTask(fetchBankBalance, "bank balance"), runTask(fetchBankBalance, "bank balance"),
runTask(fetchWorkStatistics, "work statistics"), runTask(fetchWorkStatistics, "work statistics"),
runTask(fetchBankCards, "bank cards"), runTask(fetchBankCards, "bank cards"),
runTask(fetchWithdrawInfo, "withdraw info"), runTask(fetchWithdrawInfo, "withdraw info"),
runTask(fetchWithdrawEnabled, "withdraw enabled"), runTask(fetchWithdrawEnabled, "withdraw enabled")
runTask(fetchMemberWork, "member work")
]); ]);
await checkIdentityAndRedirect();
runTask(fetchMemberWork, "member work");
} catch (error) { } catch (error) {
console.error("Host center data request failed:", error); console.error("Host center data request failed:", error);
showToast(error.response?.errorMsg || error.message || t("failed_to_load_balance")); showToast(error.response?.errorMsg || error.message || t("failed_to_load_balance"));

View File

@ -0,0 +1,269 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: "Baloo 2", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.35;
}
html,
body {
width: 100%;
min-height: 100%;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-webkit-overflow-scrolling: touch;
-webkit-tap-highlight-color: transparent;
}
body {
color: rgba(0, 0, 0, 0.8);
background: #edf8f1;
}
button {
border: 0;
background: transparent;
color: inherit;
font: inherit;
}
.host-setting {
width: 100vw;
min-height: 100vh;
background: #edf8f1;
}
.page-header {
position: sticky;
top: 0;
z-index: 10;
display: grid;
grid-template-columns: 44px 1fr 58px;
align-items: center;
min-height: calc(48px + env(safe-area-inset-top));
padding: env(safe-area-inset-top) 10px 0;
background: #f8fff9;
border-bottom: 1px solid #cce9d7;
box-shadow: 0 4px 14px rgba(75, 137, 95, 0.14);
}
.page-header h1 {
min-width: 0;
color: #153d28;
font-size: 1.25em;
font-weight: 700;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.header-icon,
.lang-button {
min-width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
color: #1f6f46;
font-weight: 700;
}
.header-icon {
padding: 6px;
}
.header-icon img {
width: 100%;
height: 100%;
display: block;
}
.lang-button {
justify-self: end;
padding: 0 10px;
border-radius: 999px;
background: #dff5e7;
font-size: 0.86em;
}
.page-content {
position: relative;
display: flex;
flex-direction: column;
gap: 2vw;
padding: 16px;
}
.setting-section {
display: flex;
flex-direction: column;
gap: 2vw;
}
.section-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.section-title,
.kyc-title {
display: flex;
align-items: center;
color: #173a28;
font-size: 1.4em;
font-weight: 700;
}
.more-button {
display: flex;
align-items: center;
color: rgba(0, 0, 0, 0.42);
font-weight: 500;
}
.surface-card {
border: 1px solid #b7e0c6;
border-radius: 20px;
background: #fbfffc;
box-shadow: 0 8px 20px rgba(69, 129, 89, 0.12);
}
.empty-row {
width: 100%;
min-height: 54px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
color: rgba(0, 0, 0, 0.8);
font-weight: 600;
}
.bank-detail,
.kyc-detail {
width: 100%;
display: flex;
flex-direction: column;
gap: 2vw;
padding: 12px;
}
.field-label {
color: rgba(0, 0, 0, 0.8);
font-weight: 600;
}
.field-value,
.field-text {
color: rgba(0, 0, 0, 0.42);
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.field-text {
font-size: 0.8em;
overflow-wrap: break-word;
white-space: normal;
}
.status-text {
margin-left: 4px;
font-size: 0.9em;
font-weight: 700;
}
.status-pass {
color: #2fb95d;
}
.status-failed {
color: #e04444;
}
.status-pending {
color: #d79a12;
}
.icon-route {
width: 6vw;
min-width: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.arrow-icon {
display: block;
width: 6vw;
min-width: 24px;
max-width: 34px;
}
.preview-list {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
}
.preview-list img {
display: block;
width: 15vw;
aspect-ratio: 1 / 1;
object-fit: cover;
border-radius: 5vw;
background: #edf8f1;
}
.toast {
position: fixed;
left: 50%;
bottom: calc(22px + env(safe-area-inset-bottom));
z-index: 30;
max-width: calc(100% - 40px);
transform: translateX(-50%);
padding: 9px 13px;
border-radius: 999px;
background: rgba(22, 56, 36, 0.92);
color: #fff;
font-size: 0.9em;
font-weight: 700;
text-align: center;
}
[hidden] {
display: none !important;
}
[dir="rtl"] .status-text {
margin-right: 4px;
margin-left: 0;
}
[dir="rtl"] .arrow-icon,
[dir="rtl"] .header-icon img {
transform: scaleX(-1);
}
@media screen and (max-width: 360px) {
* {
font-size: 10px;
}
}
@media screen and (min-width: 360px) {
* {
font-size: 12px;
}
}
@media screen and (min-width: 768px) {
* {
font-size: 24px;
}
}

View File

@ -0,0 +1,452 @@
const API_BASE_URL = "https://jvapi.haiyihy.com";
const icons = {
back: "./icons/back.svg",
arrow: "./icons/arrow.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 i18n = {
en: {
host_setting: "Host Setting",
bank_card: "Bank card",
more: "More",
add_payment_method: "Add payment method",
category: "Category",
card_number_label: "Card number",
payee: "Payee",
kyc: "KYC",
review_passed: "Review passed",
review_failed: "Review failed",
awaiting_review: "Awaiting review",
complete_information: "Complete information",
passport_id_card_title: "Passport / ID card",
contact_number: "Contact number",
other_description: "Other description",
something_went_wrong: "Something went wrong."
},
ar: {
host_setting: "إعدادات المضيف",
bank_card: "البطاقة البنكية",
more: "المزيد",
add_payment_method: "إضافة طريقة دفع",
category: "الفئة",
card_number_label: "رقم البطاقة",
payee: "المستفيد",
kyc: "KYC",
review_passed: "تمت الموافقة",
review_failed: "فشل التحقق",
awaiting_review: "بانتظار المراجعة",
complete_information: "إكمال المعلومات",
passport_id_card_title: "جواز السفر / الهوية",
contact_number: "رقم التواصل",
other_description: "وصف آخر",
something_went_wrong: "حدث خطأ ما."
},
tr: {
host_setting: "Host Ayarı",
bank_card: "Banka kartı",
more: "Daha fazla",
add_payment_method: "Ödeme yöntemi ekle",
category: "Kategori",
card_number_label: "Kart numarası",
payee: "Alacaklı",
kyc: "KYC",
review_passed: "İnceleme geçti",
review_failed: "İnceleme başarısız",
awaiting_review: "İnceleme bekliyor",
complete_information: "Bilgileri tamamla",
passport_id_card_title: "Pasaport / kimlik kartı",
contact_number: "İletişim numarası",
other_description: "Diğer açıklama",
something_went_wrong: "Bir şeyler ters gitti."
},
bn: {
host_setting: "হোস্ট সেটিং",
bank_card: "ব্যাংক কার্ড",
more: "আরও",
add_payment_method: "পেমেন্ট পদ্ধতি যোগ করুন",
category: "ক্যাটাগরি",
card_number_label: "কার্ড নম্বর",
payee: "প্রাপক",
kyc: "KYC",
review_passed: "রিভিউ পাস হয়েছে",
review_failed: "রিভিউ ব্যর্থ হয়েছে",
awaiting_review: "রিভিউ অপেক্ষমাণ",
complete_information: "তথ্য সম্পূর্ণ করুন",
passport_id_card_title: "পাসপোর্ট / আইডি কার্ড",
contact_number: "যোগাযোগ নম্বর",
other_description: "অন্যান্য বিবরণ",
something_went_wrong: "কিছু ভুল হয়েছে।"
}
};
const state = {
lang: resolveLanguage(),
bankCard: null,
withdrawInfo: null,
profile: {}
};
const $ = (selector) => document.querySelector(selector);
const $$ = (selector) => Array.from(document.querySelectorAll(selector));
const t = (key) => (i18n[state.lang] && i18n[state.lang][key]) || i18n.en[key] || key;
function resolveLanguage() {
const params = new URLSearchParams(window.location.search);
const hashQuery = window.location.hash.includes("?") ? window.location.hash.split("?")[1] : "";
const hashParams = new URLSearchParams(hashQuery);
const lang = params.get("lang") || hashParams.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)) {
resolve({ success: true, environment: "browser" });
return;
}
let attempt = 0;
const poll = () => {
attempt += 1;
if (window.app && typeof window.app.getAccessOrigin === "function") {
const raw = window.app.getAccessOrigin();
if (raw) setHeadersFromApp(parseAccessOrigin(raw));
resolve({ success: true, environment: "app" });
return;
}
if (attempt < 50) {
window.setTimeout(poll, 100);
return;
}
if (window.FlutterApp && typeof window.FlutterApp.postMessage === "function") {
window.FlutterApp.postMessage("requestAccessOrigin");
}
resolve({ success: true, environment: "app" });
};
poll();
}).then((result) => {
if (typeof result === "string") {
setHeadersFromApp(parseAccessOrigin(result));
return { success: true, environment: "app" };
}
return result;
});
}
async function apiGet(path) {
const response = await fetch(`${API_BASE_URL}${path}`, {
method: "GET",
headers: commonHeaders
});
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;
}
async function fetchUserProfile() {
const cached = localStorage.getItem("userProfile");
if (cached) {
try {
state.profile = JSON.parse(cached) || {};
} catch (error) {
console.error("Failed to parse userProfile:", error);
}
}
try {
const result = await apiGet("/team/member/profile");
if (result.status && result.body) {
state.profile = result.body;
localStorage.setItem("userProfile", JSON.stringify(result.body));
}
} catch (error) {
console.error("Failed to fetch user profile:", error);
}
}
function getMemberId() {
return state.profile.id || state.profile.userId || state.profile.account || null;
}
function calculateRoles(identity) {
if (!identity) return ["guest"];
const roles = [];
if (identity.freightAgent) roles.push("freightAgent");
if (identity.agent) roles.push("agent");
if (identity.bdLeader) roles.push("bdLeader");
if (identity.bd) roles.push("bd");
if (identity.anchor) roles.push("anchor");
if (identity.admin) roles.push("admin");
return roles.length ? roles : ["guest"];
}
function getPrimaryRole(roles) {
const weight = {
guest: 0,
anchor: 1,
agent: 2,
bd: 3,
bdLeader: 4,
freightAgent: 5,
admin: 6
};
return roles.reduce((current, role) => (weight[role] > weight[current] ? role : current), "guest");
}
function getDefaultPage(role) {
return {
admin: "/admin-center",
freightAgent: "/coin-seller",
agent: "/agency-center",
bdLeader: "/bd-leader-center",
bd: "/bd-center",
anchor: "/host-center",
guest: "/apply"
}[role] || "/apply";
}
async function checkIdentityAndRedirect() {
const memberId = getMemberId();
if (!memberId) {
navigateTo("/apply");
return;
}
const result = await apiGet("/app/h5/identity");
if (!result.status || !result.body) return;
const roles = calculateRoles(result.body);
if (!roles.includes("anchor")) {
navigateTo(getDefaultPage(getPrimaryRole(roles)));
}
}
async function fetchBankCards() {
const result = await apiGet("/wallet/bank-card/list");
if (result.status && result.body) {
state.bankCard = result.body.find((item) => item.use) || null;
}
}
async function fetchWithdrawInfo() {
const result = await apiGet("/wallet/withdraw-info/list");
if (result.status && result.body && result.body.length > 0) {
const first = result.body[0];
let previewUrls = [];
try {
previewUrls = JSON.parse(first.passportFrontUrl || "[]").map((item) => item);
} catch (error) {
console.error("Failed to parse passportFrontUrl:", error);
}
state.withdrawInfo = { previewUrls, ...first };
}
}
function normalizeRoute(target) {
const url = new URL(target, window.location.origin);
const currentLang = new URLSearchParams(window.location.search).get("lang");
if (currentLang && !url.searchParams.has("lang")) url.searchParams.set("lang", currentLang);
return `${url.pathname}${url.search}${url.hash}`;
}
function navigateTo(target) {
window.location.href = normalizeRoute(target);
}
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;
toast.hidden = false;
window.clearTimeout(showToast.timer);
showToast.timer = window.setTimeout(() => {
toast.hidden = true;
}, 2500);
}
function renderI18n() {
document.documentElement.lang = state.lang;
document.documentElement.dir = state.lang === "ar" ? "rtl" : "ltr";
$("[data-action='language']").textContent = state.lang.toUpperCase();
$$("[data-i18n]").forEach((node) => {
node.textContent = t(node.dataset.i18n);
});
}
function renderIcons() {
$("#backIcon").src = icons.back;
["#bankAddArrow", "#kycHeaderArrow", "#kycAddArrow"].forEach((selector) => {
$(selector).src = icons.arrow;
});
}
function renderBankCard() {
const hasCard = Boolean(state.bankCard);
$("#bankMoreButton").hidden = !hasCard;
$("#addBankCardRow").hidden = hasCard;
$("#bankDetail").hidden = !hasCard;
if (!hasCard) return;
$("#cardType").textContent = state.bankCard.cardType || "-";
$("#cardNo").textContent = state.bankCard.cardNo || "-";
$("#payee").textContent = state.bankCard.payee || "-";
}
function renderKyc() {
const info = state.withdrawInfo;
const hasInfo = Boolean(info);
$("#kycColon").hidden = !hasInfo;
$("#kycStatus").hidden = !hasInfo;
$("#kycHeaderButton").hidden = !hasInfo;
$("#completeKycRow").hidden = hasInfo;
$("#kycDetail").hidden = !hasInfo;
if (!hasInfo) return;
const status = $("#kycStatus");
status.className = "status-text";
if (info.status === "PASS") {
status.textContent = t("review_passed");
status.classList.add("status-pass");
} else if (info.status === "NOT_PASS") {
status.textContent = t("review_failed");
status.classList.add("status-failed");
} else if (info.status === "PENDING") {
status.textContent = t("awaiting_review");
status.classList.add("status-pending");
} else {
status.textContent = info.status || "";
}
const previewList = $("#previewList");
previewList.innerHTML = "";
(info.previewUrls || []).forEach((url) => {
const image = document.createElement("img");
image.src = url;
image.alt = "";
previewList.appendChild(image);
});
$("#contactNumber").textContent = info.contactNumber || "-";
$("#otherDescription").textContent = info.otherDescription || "-";
}
function render() {
renderI18n();
renderIcons();
renderBankCard();
renderKyc();
}
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));
});
}
async function runTask(task, label) {
try {
await task();
} catch (error) {
console.error(`${label} failed:`, error);
if (label === "withdraw info") showToast(error.response?.errorMsg || error.message || t("something_went_wrong"));
}
}
async function init() {
localStorage.setItem("identity", JSON.stringify({ identity: "HOST" }));
bindEvents();
render();
try {
await connectToApp();
await fetchUserProfile();
await runTask(checkIdentityAndRedirect, "identity check");
await Promise.all([
runTask(fetchBankCards, "bank cards"),
runTask(fetchWithdrawInfo, "withdraw info")
]);
} catch (error) {
console.error("Host setting initialization failed:", error);
showToast(error.response?.errorMsg || error.message || t("something_went_wrong"));
} finally {
render();
}
}
init();

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<path d="M12 8l8 8-8 8" stroke="#7FA48D" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 189 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<path d="M20 8l-8 8 8 8" stroke="#1F6F46" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 190 B

View File

@ -1,13 +1,70 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <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"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Host Setting</title> <title>Host Setting</title>
<link rel="stylesheet" href="../logic-pages/app.css"> <link rel="stylesheet" href="./host-setting.css" />
</head> </head>
<body> <body>
<script>window.PAGE_CONFIG = {"title":"Host Setting","mode":"bankCards"};</script> <div class="host-setting">
<script src="../logic-pages/app.js"></script> <header class="page-header">
<button class="header-icon" type="button" data-action="back" aria-label="Back"><img id="backIcon" alt="" /></button>
<h1 data-i18n="host_setting">Host Setting</h1>
<button class="lang-button" type="button" data-action="language">EN</button>
</header>
<main class="page-content">
<section class="setting-section">
<div class="section-head">
<div class="section-title" data-i18n="bank_card">Bank card</div>
<button class="more-button" id="bankMoreButton" type="button" data-route="/bank-card" data-i18n="more" hidden>More</button>
</div>
<div class="surface-card" id="bankCardPanel">
<button class="empty-row" id="addBankCardRow" type="button" data-route="/bank-card">
<span data-i18n="add_payment_method">Add payment method</span>
<img class="arrow-icon" id="bankAddArrow" alt="" />
</button>
<div class="bank-detail" id="bankDetail" hidden>
<div class="field-label" data-i18n="category">Category</div>
<div class="field-value" id="cardType">-</div>
<div class="field-label" data-i18n="card_number_label">Card number</div>
<div class="field-value" id="cardNo">-</div>
<div class="field-label"><span data-i18n="payee">Payee</span>:</div>
<div class="field-value" id="payee">-</div>
</div>
</div>
</section>
<section class="setting-section">
<div class="section-head">
<div class="kyc-title">
<span data-i18n="kyc">KYC</span>
<span id="kycColon" hidden>:</span>
<span class="status-text" id="kycStatus" hidden></span>
</div>
<button class="icon-route" id="kycHeaderButton" type="button" data-route="/KYC" hidden><img class="arrow-icon" id="kycHeaderArrow" alt="" /></button>
</div>
<div class="surface-card">
<button class="empty-row" id="completeKycRow" type="button" data-route="/KYC">
<span data-i18n="complete_information">Complete information</span>
<img class="arrow-icon" id="kycAddArrow" alt="" />
</button>
<div class="kyc-detail" id="kycDetail" hidden>
<div class="field-label"><span data-i18n="passport_id_card_title">Passport / ID card</span>:</div>
<div class="preview-list" id="previewList"></div>
<div class="field-label"><span data-i18n="contact_number">Contact number</span>:</div>
<div class="field-text" id="contactNumber">-</div>
<div class="field-label"><span data-i18n="other_description">Other description</span>:</div>
<div class="field-text" id="otherDescription">-</div>
</div>
</div>
</section>
</main>
<div class="toast" id="toast" hidden></div>
</div>
<script type="module" src="./host-setting.js"></script>
</body> </body>
</html> </html>

View File

@ -1,110 +1,28 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8">
<meta <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
name="viewport" <title>Invite Agent</title>
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" <link rel="stylesheet" href="../bd-static/shared.css">
/>
<title>Invite User To Become Agent</title>
<link rel="stylesheet" href="./yumi-invite-agency.css" />
</head> </head>
<body> <body>
<div class="fullPage" id="pageRoot"> <main class="page">
<div class="status-bar-placeholder" id="statusBarPlaceholder"></div> <section class="content">
<div class="header-container"> <div class="row">
<div class="side-area"> <div class="input-box" style="flex:1">
<button class="back-btn" id="backButton" type="button"> <div style="display:flex;align-items:center;gap:8px">
<img class="flip-img" id="backButtonIcon" src="" alt="" style="width: 100%; aspect-ratio: 1 / 1; display: block" /> <img src="../bd-static/icons/search.svg" alt="" style="width:22px;height:22px;opacity:.8">
</button> <input class="field" id="searchInput" type="text" data-placeholder="enter_user_id" autocomplete="off">
</div> <button type="button" id="clearButton" hidden style="color:#9ca3af;font-size:20px;line-height:1">x</button>
<h1 class="header-title" id="pageTitle" style="color: black">Invite User To Become Agent</h1>
<div class="actions-area">
<div class="language-entry" id="languageEntry">
<div class="language-name" id="languageName">English</div>
</div>
</div>
</div>
<div style="padding: 16px; position: relative">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px">
<div
class="surface-border-wrapper"
style="
flex: 1;
min-width: 0;
border-radius: 4px;
"
>
<div class="surface-border-content">
<div style="padding: 0 8px; display: flex; align-items: center; height: max-content">
<input
id="searchInput"
type="text"
name=""
placeholder="Enter User ID"
autocomplete="off"
style="
width: 100%;
color: rgba(0, 0, 0, 0.4);
font-weight: 600;
outline: none;
border: none;
background-color: transparent;
padding: 8px;
"
/>
<button
id="clearButton"
type="button"
hidden
style="background: none; border: none; color: #9ca3af; cursor: pointer; padding: 0 4px"
>
×
</button>
</div>
</div> </div>
</div> </div>
<button style="font-weight: 600" class="invite-btn" id="searchButton" type="button" disabled>Search</button> <button class="primary-btn" id="inviteButton" type="button" disabled data-i18n="invite">Invite</button>
</div> </div>
<div id="list"></div>
<div id="resultList" style="margin: 16px 0; display: flex; flex-direction: column; gap: 12px"></div> <div class="subtle" id="empty" data-i18n="no_users_found">No users found</div>
<div id="noData" class="no-data">please input user id</div> </section>
</div> </main>
</div> <script type="module" src="./invite.js"></script>
<template id="userCardTemplate">
<div
class="surface-border-wrapper user-card"
style="
position: relative;
z-index: 0;
border-radius: 20px;
"
>
<div class="surface-border-content">
<div style="padding: 10px; transition: all 0.2s">
<div class="status-badge" style="position: absolute; z-index: 3; top: -2px; right: -2px"></div>
<div style="font-weight: 600">Information:</div>
<div style="display: flex; align-items: center; justify-content: space-between; gap: 4px">
<div style="flex: 1; min-width: 0; align-self: stretch; display: flex; align-items: center; gap: 4px">
<div style="width: 3em; display: flex; align-items: center; justify-content: center">
<img class="avatar-image" src="" alt="" style="width: 100%; aspect-ratio: 1/1; object-fit: cover; border-radius: 50%" />
</div>
<div style="flex: 1; min-width: 0; align-self: stretch">
<div class="nickname" style="margin-bottom: 4px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap"></div>
<div class="account" style="font-size: 0.9em; color: rgba(0, 0, 0, 0.4); font-weight: 500"></div>
</div>
</div>
<div style="width: auto; min-width: 0; align-self: stretch; display: flex; justify-content: center; align-items: center">
<div class="pending-actions"></div>
</div>
</div>
</div>
</div>
</div>
</template>
<script src="./yumi-invite-agency.js"></script>
</body> </body>
</html> </html>

179
h5/invite-agency/invite.js Normal file
View File

@ -0,0 +1,179 @@
import { applyLang, avatarError, bindHeader, connectToApp, get, post, t, toast } from "../bd-static/shared.js";
const state = {
query: "",
invited: [],
searched: [],
selected: null,
showSearch: false,
locked: false
};
const input = document.getElementById("searchInput");
const clearButton = document.getElementById("clearButton");
const inviteButton = document.getElementById("inviteButton");
const list = document.getElementById("list");
const empty = document.getElementById("empty");
bindHeader("invite_become_agent");
applyLang();
input.placeholder = t("enter_user_id");
input.addEventListener("input", () => {
state.query = input.value;
clearButton.hidden = !state.query;
state.showSearch = false;
state.selected = null;
inviteButton.disabled = true;
if (!state.query.trim()) render();
});
input.addEventListener("keyup", (event) => {
if (event.key === "Enter") search();
});
clearButton.addEventListener("click", () => {
state.query = "";
state.searched = [];
state.selected = null;
state.showSearch = false;
input.value = "";
clearButton.hidden = true;
inviteButton.disabled = true;
render();
});
inviteButton.addEventListener("click", inviteSelected);
let searchTimer = 0;
input.addEventListener("input", () => {
window.clearTimeout(searchTimer);
if (state.query.trim()) searchTimer = window.setTimeout(search, 500);
});
async function init() {
try {
await connectToApp();
await loadInvited();
} catch (error) {
toast(error.response?.errorMsg || error.message);
}
}
async function loadInvited() {
const response = await get("/team/bd/invite-message-own");
state.invited = response.status && Array.isArray(response.body) ? response.body : [];
render();
}
async function search() {
if (state.locked) return;
const value = state.query.trim();
state.selected = null;
inviteButton.disabled = true;
if (!value) {
state.showSearch = false;
state.searched = [];
render();
return;
}
state.locked = true;
state.showSearch = true;
state.searched = [];
render();
try {
const response = await get(`/user/user-profile/open-search?account=${encodeURIComponent(value)}`);
state.searched = response.status && response.body ? [{ userProfile: response.body }] : [];
} catch (error) {
toast(error.response?.errorMsg || error.message);
} finally {
state.locked = false;
render();
}
}
function selectItem(item) {
const profile = item.userProfile || {};
if (state.selected?.id === profile.id) {
state.selected = null;
inviteButton.disabled = true;
render();
return;
}
state.selected = profile;
inviteButton.disabled = item.status === 0 || item.status === 1;
render();
}
async function inviteSelected() {
if (!state.selected?.id) return;
try {
const response = await post("/team/bd/invite-agent", { inviteUserId: state.selected.id });
if (response.status) toast(t("invitation_submitted"));
} catch (error) {
toast(error.response?.errorMsg || t("something_went_wrong"));
}
state.query = "";
state.searched = [];
state.selected = null;
state.showSearch = false;
input.value = "";
clearButton.hidden = true;
inviteButton.disabled = true;
await loadInvited();
}
async function cancelInvitation(id) {
try {
const response = await post(`/team/bd/invite-agent-cancel/${id}`);
if (response.status) {
toast(t("application_cancelled"));
await loadInvited();
}
} catch (error) {
const message = error.response?.errorMsg?.replace(/^.*\]\s*/, "") || t("cancellation_failed_try_again");
toast(message);
}
}
function render() {
const rows = state.showSearch ? state.searched : state.invited;
list.innerHTML = "";
rows.forEach((item) => list.appendChild(card(item)));
empty.hidden = rows.length > 0;
}
function card(item) {
const profile = item.userProfile || {};
const node = document.createElement("article");
node.className = "card card-pad";
node.style.cssText = "position:relative;margin-top:12px";
if (state.selected?.id === profile.id) node.style.outline = "2px solid #32a96b";
node.innerHTML = `
${item.status === 0 ? `<span class="badge pending" style="position:absolute;right:0;top:0;border-radius:0 12px">${t("pending")}</span>` : ""}
${item.status === 1 ? `<span class="badge success" style="position:absolute;right:0;top:0;border-radius:0 12px">${t("success")}</span>` : ""}
<div style="font-weight:700;margin-bottom:8px">${t("information")}:</div>
<div class="row">
<div style="display:flex;align-items:center;gap:10px;min-width:0">
<img class="avatar" src="${profile.userAvatar || ""}" alt="">
<div class="member-main">
<div class="member-name">${profile.userNickname || "-"}</div>
<div class="member-meta">${t("user_id_prefix")}${profile.account || "-"}</div>
</div>
</div>
${item.status === 0 ? `<button class="danger-btn" type="button" data-cancel>${t("cancel")}</button>` : ""}
</div>
`;
avatarError(node.querySelector("img"));
node.addEventListener("click", () => selectItem(item));
const cancel = node.querySelector("[data-cancel]");
if (cancel) {
cancel.addEventListener("click", (event) => {
event.stopPropagation();
cancelInvitation(item.id);
});
}
return node;
}
init();

View File

@ -1,177 +0,0 @@
html {
-webkit-text-size-adjust: 100%;
-webkit-overflow-scrolling: touch;
}
body {
margin: 0;
min-height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Arial, sans-serif;
background: #edf8f1;
color: #1f3528;
}
* { box-sizing: border-box; }
button, input { font: inherit; }
[hidden] { display: none !important; }
.fullPage {
width: 100vw;
min-height: 100vh;
background: #edf8f1;
position: relative;
}
.fullPage input::placeholder {
font-weight: 700;
color: #7b8d81;
}
.invite-btn, .result-action-btn {
padding: 7px 14px;
border-radius: 32px;
background: #dff5e7;
color: #1f6f46;
border: 1px solid #b7e0c6;
font-weight: 800;
cursor: pointer;
transition: transform 0.2s, background 0.2s;
white-space: nowrap;
}
.invite-btn:disabled, .result-action-btn:disabled {
background: #e1e7e3;
border-color: #ccd6d0;
color: #7b8d81;
cursor: not-allowed;
}
.no-data {
text-align: center;
padding: 40px 20px;
color: #6d7e72;
font-weight: 700;
}
.user-card { cursor: default; }
.status-bar-placeholder { display: none; height: 30px; }
.status-bar-placeholder.is-visible { display: block; }
.header-container {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
height: 60px;
background: #f8fff9;
border-bottom: 1px solid #cce9d7;
box-shadow: 0 4px 14px rgba(75, 137, 95, 0.14);
font-size: 16px;
}
.side-area, .actions-area {
min-width: 1.5em;
display: flex;
align-items: center;
}
.actions-area {
justify-content: flex-end;
gap: 8px;
}
.header-title {
flex: 1;
margin: 0;
color: #153d28 !important;
font-size: 1.2em;
font-weight: 800;
text-align: center;
letter-spacing: 0;
}
.back-btn, .help-btn {
width: 1.5em;
border: none;
background: transparent;
transition: transform 0.2s ease;
padding: 0;
}
.back-btn:active, .help-btn:active { transform: scale(0.9); }
.language-entry {
display: flex;
align-items: center;
border-radius: 999px;
background: #dff5e7;
padding: 5px 10px;
color: #24724a !important;
cursor: pointer;
}
.language-name { font-weight: 800; }
.surface-border-wrapper, .gradient-border-wrapper {
position: relative;
border: 1px solid #b7e0c6;
background: #ffffff;
box-shadow: 0 8px 20px rgba(69, 129, 89, 0.12);
}
.surface-border-content, .gradient-border-content {
position: relative;
border-radius: inherit;
width: 100%;
height: 100%;
background: #fbfffc;
}
.modal-overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
padding: 20px;
}
.modal {
background: #fff;
border-radius: 16px;
box-shadow: 0 20px 25px rgba(0, 0, 0, 0.1), 0 10px 10px rgba(0, 0, 0, 0.04);
width: 100%;
max-width: 320px;
padding: 24px 20px 20px;
text-align: center;
position: relative;
}
.modal-title {
font-size: 18px;
font-weight: 700;
color: #1f3528;
margin-bottom: 12px;
}
.modal-message {
font-size: 14px;
color: #6b7280;
line-height: 1.5;
margin-bottom: 20px;
}
.modal-button {
border: none;
border-radius: 12px;
background: #dff5e7;
color: #1f6f46;
padding: 10px 16px;
font-weight: 800;
}

View File

@ -1,694 +0,0 @@
(function () {
const API_BASE = "https://jvapi.haiyihy.com";
const BACK_ICON =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGXSURBVHgB7dvBTcNAFIThB5xTQFwADVBBmqEnUgZQAAXAjRqgACgAj2wfECbJZXZnrPmkVRT5lF9ODn4vVRERERERETEaxnNb8cd+PA/jeZ3PY02xqG7KA+Ic6/eds5vfPxfRdelb4uxXrt0VmXqgU3Hgo8iUA52LA9SvF1yVpkviHOdDpRhIJg6oBZKKA0qB5OKASiDJOKAQSDYO9A4kHQd6BpKPA70CWcSBHoFs4kDrQFZxoGUguzjQKpBlHGgRyDYOsANZxwFmIPs4wAq0iTjACvRUG4gDjEeumDRsIg4wAn3VhjDmYt81jWOGf64vo5q3MsAaHOLDH2oa7q2xicQKhLvopTYQiTl63kQk9mzePlKL5QXrSK22O2wjtVx/sYzUej/ILlKPBSqrSL02zGwi9VzBs4jUe0dRPpLCEqd0JJUtV9lISmvAkpHU9qTlIikukktFUt20vzQSHu++F5HqGvDi3PgIgQ5FpL5p/zme+/l1za7IHP6rcSoSfdPeIRCsRcIPtM18rSUMJoeKiIiIiIjw9QNKh3OJhFQBoAAAAABJRU5ErkJggg==";
const DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k-1776148661448.png";
const TEXT = {
pageTitle: "Invite User To Become Agent",
language: "English",
placeholder: "Enter User ID",
search: "Search",
invite: "Invite",
idlePrompt: "please input user id",
noUsersFound: "No users found",
information: "Information:",
userIdPrefix: "User ID:",
pending: "Pending",
success: "Success",
cancel: "Cancel",
invitationSubmitted: "Invitation submitted",
applicationCancelled: "Application cancelled",
somethingWentWrong: "Something went wrong",
cancellationFailed: "Cancellation failed, try again",
informationTitle: "Information",
successTitle: "Success",
errorTitle: "Error",
ok: "OK",
};
const state = {
query: "",
invitedList: [],
searchResults: [],
showSearchResults: false,
searching: false,
hasSearched: false,
};
const dom = {
statusBarPlaceholder: document.getElementById("statusBarPlaceholder"),
backButton: document.getElementById("backButton"),
backButtonIcon: document.getElementById("backButtonIcon"),
pageTitle: document.getElementById("pageTitle"),
languageEntry: document.getElementById("languageEntry"),
languageName: document.getElementById("languageName"),
searchInput: document.getElementById("searchInput"),
clearButton: document.getElementById("clearButton"),
searchButton: document.getElementById("searchButton"),
resultList: document.getElementById("resultList"),
noData: document.getElementById("noData"),
userCardTemplate: document.getElementById("userCardTemplate"),
};
init();
function init() {
document.title = TEXT.pageTitle;
document.documentElement.lang = "en";
document.documentElement.dir = resolveDocumentDirection();
dom.backButtonIcon.src = BACK_ICON;
dom.pageTitle.textContent = TEXT.pageTitle;
dom.languageName.textContent = TEXT.language;
dom.searchInput.placeholder = TEXT.placeholder;
dom.searchButton.textContent = TEXT.search;
dom.noData.textContent = TEXT.idlePrompt;
syncStatusBarPlaceholder();
bindEvents();
render();
loadInvitedList();
}
function bindEvents() {
dom.backButton.addEventListener("click", handleBack);
dom.languageEntry.addEventListener("click", handleLanguageClick);
dom.searchInput.addEventListener("input", handleSearchInput);
dom.clearButton.addEventListener("click", clearSearch);
dom.searchButton.addEventListener("click", triggerSearchNow);
}
function resolveDocumentDirection() {
const lang = (new URLSearchParams(window.location.search).get("lang") || "").toLowerCase();
return lang.startsWith("ar") ? "rtl" : "ltr";
}
function syncStatusBarPlaceholder() {
const hasAppBridge =
Boolean(window.app) ||
Boolean(window.FlutterPageControl) ||
Boolean(window.webkit && window.webkit.messageHandlers);
const isIOS = /iphone|ipad|ipod/i.test(window.navigator.userAgent || "");
dom.statusBarPlaceholder.classList.toggle("is-visible", hasAppBridge && isIOS);
}
function handleBack() {
if (window.history.length > 1) {
window.history.back();
return;
}
window.location.href = "/";
}
function handleLanguageClick() {
window.location.href = "/language";
}
function handleSearchInput(event) {
state.query = event.target.value;
dom.clearButton.hidden = !state.query;
state.showSearchResults = false;
state.hasSearched = false;
render();
}
function clearSearch() {
state.query = "";
state.searchResults = [];
state.showSearchResults = false;
state.hasSearched = false;
dom.searchInput.value = "";
dom.clearButton.hidden = true;
render();
}
async function triggerSearchNow() {
if (state.searching) {
return;
}
const query = state.query.trim();
if (!query) {
state.showSearchResults = false;
state.hasSearched = false;
render();
return;
}
state.searching = true;
state.showSearchResults = true;
state.hasSearched = true;
state.searchResults = [];
render();
try {
const payload = await searchUser(query);
state.searchResults = payload ? [mergeWithInvitationStatus(payload.userProfile)] : [];
} catch (error) {
state.searchResults = [];
showToast(extractErrorMessage(error), {
title: TEXT.errorTitle,
type: "error",
});
} finally {
state.searching = false;
render();
}
}
async function loadInvitedList() {
try {
const response = await request("GET", "/team/bd/invite-message-own");
state.invitedList = response && response.status && Array.isArray(response.body) ? response.body : [];
state.searchResults = state.searchResults.map(function (item) {
return mergeWithInvitationStatus(item.userProfile || {});
});
render();
} catch (error) {
state.invitedList = [];
render();
}
}
async function searchUser(query) {
try {
const response = await request(
"GET",
"/user/user-profile/open-search?account=" + encodeURIComponent(query)
);
if (response && response.status && response.body) {
return { userProfile: response.body };
}
} catch (error) {
if (!/^\d+$/.test(query)) {
throw error;
}
}
if (!/^\d+$/.test(query)) {
return null;
}
const fallbackResponse = await request(
"GET",
"/user/user-profile?userId=" + encodeURIComponent(query)
);
if (fallbackResponse && fallbackResponse.status && fallbackResponse.body) {
return { userProfile: fallbackResponse.body };
}
return null;
}
function mergeWithInvitationStatus(userProfile) {
const matchedInvitation = state.invitedList.find(function (item) {
return (
item &&
item.userProfile &&
userProfile &&
item.userProfile.id === userProfile.id
);
});
return Object.assign({}, matchedInvitation || {}, {
userProfile: userProfile || {},
});
}
async function inviteUser(item) {
if (!item || !item.userProfile || !item.userProfile.id) {
return;
}
try {
const response = await request("POST", "/team/bd/invite-agent", {
body: {
inviteUserId: item.userProfile.id,
},
});
if (response && response.status) {
showToast(TEXT.invitationSubmitted, {
title: TEXT.successTitle,
type: "success",
});
await loadInvitedList();
}
} catch (error) {
showToast(extractErrorMessage(error, TEXT.somethingWentWrong), {
title: TEXT.errorTitle,
type: "error",
});
}
}
async function cancelInvitation(invitationId) {
try {
const response = await request("POST", "/team/bd/invite-agent-cancel/" + invitationId);
if (response && response.status) {
showToast(TEXT.applicationCancelled, {
title: TEXT.informationTitle,
type: "info",
});
await loadInvitedList();
}
} catch (error) {
showToast(extractErrorMessage(error, TEXT.cancellationFailed), {
title: TEXT.errorTitle,
type: "error",
});
}
}
function render() {
renderSearchButton();
renderList();
}
function renderSearchButton() {
dom.searchButton.disabled = !state.query.trim() || state.searching;
}
function renderList() {
const currentList = state.showSearchResults ? state.searchResults : state.invitedList;
dom.resultList.innerHTML = "";
currentList.forEach(function (item) {
dom.resultList.appendChild(createUserCard(item, state.showSearchResults ? "search" : "invited"));
});
if (currentList.length > 0 || (state.searching && state.showSearchResults)) {
dom.noData.hidden = true;
return;
}
dom.noData.hidden = false;
dom.noData.textContent =
state.showSearchResults && state.hasSearched ? TEXT.noUsersFound : TEXT.idlePrompt;
}
function createUserCard(item, mode) {
const fragment = dom.userCardTemplate.content.cloneNode(true);
const statusBadge = fragment.querySelector(".status-badge");
const avatarImage = fragment.querySelector(".avatar-image");
const nickname = fragment.querySelector(".nickname");
const account = fragment.querySelector(".account");
const pendingActions = fragment.querySelector(".pending-actions");
const userProfile = item.userProfile || {};
avatarImage.src = userProfile.userAvatar || DEFAULT_AVATAR;
avatarImage.alt = userProfile.userNickname || "";
avatarImage.onerror = handleAvatarError;
nickname.textContent = userProfile.userNickname || "";
account.textContent = TEXT.userIdPrefix + " " + (userProfile.account || userProfile.id || "");
if (item.status === 0) {
statusBadge.innerHTML =
'<div style="background: #eef7fb; border: 1px solid #c0e1f1; padding: 4px 8px; border-radius: 0 12px; color: #28627c; font-weight: 700">' +
TEXT.pending +
"</div>";
} else if (item.status === 1) {
statusBadge.innerHTML =
'<div style="background: #dff5e7; border: 1px solid #b7e0c6; padding: 4px 8px; border-radius: 0 12px; color: #1f6f46; font-weight: 700">' +
TEXT.success +
"</div>";
}
if (mode === "search") {
const inviteButton = document.createElement("button");
inviteButton.type = "button";
inviteButton.className = "result-action-btn";
inviteButton.textContent = TEXT.invite;
inviteButton.disabled = item.status === 0 || item.status === 1;
inviteButton.addEventListener("click", function (event) {
event.stopPropagation();
inviteUser(item);
});
pendingActions.appendChild(inviteButton);
} else if (item.status === 0) {
const cancelButton = document.createElement("div");
cancelButton.textContent = TEXT.cancel;
cancelButton.style.padding = "4px 8px";
cancelButton.style.borderRadius = "32px";
cancelButton.style.backgroundColor = "red";
cancelButton.style.color = "white";
cancelButton.style.fontWeight = "600";
cancelButton.style.zIndex = "5";
cancelButton.addEventListener("click", function (event) {
event.stopPropagation();
cancelInvitation(item.id);
});
pendingActions.appendChild(cancelButton);
}
return fragment;
}
function handleAvatarError(event) {
event.target.onerror = null;
event.target.src = DEFAULT_AVATAR;
}
function extractErrorMessage(error, fallback) {
return (
(error &&
error.response &&
(error.response.errorMsg || error.response.message || error.response.error)) ||
(error && error.message) ||
fallback ||
TEXT.somethingWentWrong
);
}
function showToast(message, options) {
const config = Object.assign(
{
title: TEXT.informationTitle,
type: "info",
confirmText: TEXT.ok,
},
options || {}
);
const iconMap = {
error: "✕",
warning: "⚠",
success: "✓",
info: "i",
simple: "",
};
const overlay = document.createElement("div");
overlay.className = "modal-overlay";
overlay.innerHTML =
'<div class="modal modal-enter modal-' +
config.type +
'">' +
(config.type === "simple"
? ""
: '<div class="modal-icon"><div class="icon-circle icon-circle-' +
config.type +
'"><span class="icon-symbol icon-symbol-' +
config.type +
'">' +
iconMap[config.type] +
"</span></div></div>") +
(config.type === "simple" || !config.title
? ""
: '<div class="modal-title">' + escapeHtml(config.title) + "</div>") +
'<div class="modal-message">' +
escapeHtml(message) +
"</div>" +
'<div class="modal-actions"><button class="modal-btn modal-btn-' +
config.type +
'">' +
escapeHtml(config.confirmText) +
"</button></div>" +
"</div>";
const modal = overlay.querySelector(".modal");
const closeButton = overlay.querySelector(".modal-btn");
const close = function () {
modal.classList.remove("modal-enter");
modal.classList.add("modal-leave");
window.setTimeout(function () {
overlay.remove();
}, 200);
};
closeButton.addEventListener("click", close);
overlay.addEventListener("click", function (event) {
if (event.target === overlay) {
close();
}
});
document.body.appendChild(overlay);
}
function escapeHtml(value) {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
async function request(method, path, options) {
const config = options || {};
const headers = buildRequestHeaders(Boolean(config.body), config.headers || {});
const response = await fetch(API_BASE + path, {
method: method,
credentials: "include",
headers: headers,
body: config.body ? JSON.stringify(config.body) : undefined,
});
const text = await response.text();
let payload = {};
if (text) {
try {
payload = JSON.parse(text);
} catch (error) {
payload = {
status: response.ok,
body: text,
};
}
}
if (!response.ok || payload.status === false) {
const requestError = new Error(
payload.errorMsg || payload.message || response.statusText || TEXT.somethingWentWrong
);
requestError.response = payload;
throw requestError;
}
return payload;
}
function buildRequestHeaders(hasBody, overrideHeaders) {
const discovered = discoverAppHeaders();
const headers = Object.assign({}, overrideHeaders || {});
if (hasBody) {
headers["Content-Type"] = headers["Content-Type"] || "application/json";
}
headers.Accept = headers.Accept || "application/json, text/plain, */*";
if (discovered.authorization) {
headers.Authorization = headers.Authorization || discovered.authorization;
}
if (discovered.reqLang) {
headers["Req-Lang"] = headers["Req-Lang"] || discovered.reqLang;
}
if (discovered.appVersion) {
headers["App-Version"] = headers["App-Version"] || discovered.appVersion;
}
if (discovered.buildVersion) {
headers["Build-Version"] = headers["Build-Version"] || discovered.buildVersion;
}
if (discovered.appChannel) {
headers["App-Channel"] = headers["App-Channel"] || discovered.appChannel;
}
if (discovered.reqImei) {
headers["Req-Imei"] = headers["Req-Imei"] || discovered.reqImei;
}
if (discovered.sysOrigin) {
headers["Sys-Origin"] = headers["Sys-Origin"] || discovered.sysOrigin;
}
if (discovered.sysOriginChild) {
headers["Sys-Origin-Child"] =
headers["Sys-Origin-Child"] || discovered.sysOriginChild;
}
return headers;
}
function discoverAppHeaders() {
const query = new URLSearchParams(window.location.search);
const collected = [];
collectCandidate(collected, window.__APP_HEADERS__);
collectCandidate(collected, window.__HEADER_INFO__);
collectCandidate(collected, window.headerInfo);
collectCandidate(collected, parseHeaderObjectFromQuery(query));
collectFromStorage(collected, window.localStorage);
collectFromStorage(collected, window.sessionStorage);
return collected.reduce(function (result, candidate) {
return Object.assign(result, candidate);
}, {
reqLang: query.get("lang") || document.documentElement.lang || "en",
});
}
function parseHeaderObjectFromQuery(query) {
const candidate = {};
mapHeaderValue(candidate, "authorization", query.get("authorization"));
mapHeaderValue(candidate, "authorization", query.get("token"));
mapHeaderValue(candidate, "reqLang", query.get("lang"));
mapHeaderValue(candidate, "appVersion", query.get("appVersion"));
mapHeaderValue(candidate, "buildVersion", query.get("buildVersion"));
mapHeaderValue(candidate, "appChannel", query.get("appChannel"));
mapHeaderValue(candidate, "reqImei", query.get("reqImei"));
mapHeaderValue(candidate, "sysOrigin", query.get("sysOrigin"));
mapHeaderValue(candidate, "sysOriginChild", query.get("sysOriginChild"));
return candidate;
}
function collectFromStorage(collected, storage) {
if (!storage) {
return;
}
const preferredKeys = [
"authorization",
"Authorization",
"token",
"accessToken",
"headerInfo",
"headersFromApp",
"appHeaders",
"persist:root",
"user",
"userInfo",
];
preferredKeys.forEach(function (key) {
try {
collectCandidate(collected, storage.getItem(key));
} catch (error) {
return;
}
});
for (let index = 0; index < storage.length; index += 1) {
try {
const key = storage.key(index);
collectCandidate(collected, storage.getItem(key));
} catch (error) {
return;
}
}
}
function collectCandidate(collected, value) {
const normalized = normalizeHeaderCandidate(value, 0);
if (normalized && Object.keys(normalized).length > 0) {
collected.push(normalized);
}
}
function normalizeHeaderCandidate(value, depth) {
if (depth > 4 || value == null) {
return null;
}
if (typeof value === "string") {
const trimmed = value.trim();
if (!trimmed) {
return null;
}
if (trimmed[0] === "{" || trimmed[0] === "[") {
try {
return normalizeHeaderCandidate(JSON.parse(trimmed), depth + 1);
} catch (error) {
return null;
}
}
if (/bearer\s+/i.test(trimmed)) {
return {
authorization: trimmed,
};
}
return null;
}
if (Array.isArray(value)) {
return value.reduce(function (result, entry) {
const normalized = normalizeHeaderCandidate(entry, depth + 1);
return normalized ? Object.assign(result, normalized) : result;
}, {});
}
if (typeof value !== "object") {
return null;
}
const normalized = {};
const aliasMap = {
authorization: ["authorization", "Authorization", "token", "accessToken"],
reqLang: ["reqLang", "Req-Lang", "lang", "language"],
appVersion: ["appVersion", "App-Version"],
buildVersion: ["buildVersion", "Build-Version"],
appChannel: ["appChannel", "App-Channel"],
reqImei: ["reqImei", "Req-Imei"],
sysOrigin: ["sysOrigin", "Sys-Origin"],
sysOriginChild: ["sysOriginChild", "Sys-Origin-Child"],
};
Object.keys(aliasMap).forEach(function (targetKey) {
aliasMap[targetKey].forEach(function (sourceKey) {
mapHeaderValue(normalized, targetKey, value[sourceKey]);
});
});
["headerInfo", "headers", "auth", "data", "state"].forEach(function (nestedKey) {
const nestedValue = normalizeHeaderCandidate(value[nestedKey], depth + 1);
if (nestedValue) {
Object.assign(normalized, nestedValue);
}
});
Object.keys(value).forEach(function (key) {
if (
key === "headerInfo" ||
key === "headers" ||
key === "auth" ||
key === "data" ||
key === "state"
) {
return;
}
if (typeof value[key] === "object") {
const nestedValue = normalizeHeaderCandidate(value[key], depth + 1);
if (nestedValue) {
Object.assign(normalized, nestedValue);
}
}
});
return normalized;
}
function mapHeaderValue(target, key, value) {
if (value == null || value === "" || target[key]) {
return;
}
target[key] = String(value);
}
})();

View File

@ -2,7 +2,7 @@
const API_BASE = "https://jvapi.haiyihy.com"; const API_BASE = "https://jvapi.haiyihy.com";
const BACK_ICON = const BACK_ICON =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGXSURBVHgB7dvBTcNAFIThB5xTQFwADVBBmqEnUgZQAAXAjRqgACgAj2wfECbJZXZnrPmkVRT5lF9ODn4vVRERERERETEaxnNb8cd+PA/jeZ3PY02xqG7KA+Ic6/eds5vfPxfRdelb4uxXrt0VmXqgU3Hgo8iUA52LA9SvF1yVpkviHOdDpRhIJg6oBZKKA0qB5OKASiDJOKAQSDYO9A4kHQd6BpKPA70CWcSBHoFs4kDrQFZxoGUguzjQKpBlHGgRyDYOsANZxwFmIPs4wAq0iTjACvRUG4gDjEeumDRsIg4wAn3VhjDmYt81jWOGf64vo5q3MsAaHOLDH2oa7q2xicQKhLvopTYQiTl63kQk9mzePlKL5QXrSK22O2wjtVx/sYzUej/ILlKPBSqrSL02zGwi9VzBs4jUe0dRPpLCEqd0JJUtV9lISmvAkpHU9qTlIikukktFUt20vzQSHu++F5HqGvDi3PgIgQ5FpL5p/zme+/l1za7IHP6rcSoSfdPeIRCsRcIPtM18rSUMJoeKiIiIiIjw9QNKh3OJhFQBoAAAAABJRU5ErkJggg=="; "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGXSURBVHgB7dvBTcNAFIThB5xTQFwADVBBmqEnUgZQAAXAjRqgACgAj2wfECbJZXZnrPmkVRT5lF9ODn4vVRERERERETEaxnNb8cd+PA/jeZ3PY02xqG7KA+Ic6/eds5vfPxfRdelb4uxXrt0VmXqgU3Hgo8iUA52LA9SvF1yVpkviHOdDpRhIJg6oBZKKA0qB5OKASiDJOKAQSDYO9A4kHQd6BpKPA70CWcSBHoFs4kDrQFZxoGUguzjQKpBlHGgRyDYOsANZxwFmIPs4wAq0iTjACvRUG4gDjEeumDRsIg4wAn3VhjDmYt81jWOGf64vo5q3MsAaHOLDH2oa7q2xicQKhLvopTYQiTl63kQk9mzePlKL5QXrSK22O2wjtVx/sYzUej/ILlKPBSqrSL02zGwi9VzBs4jUe0dRPpLCEqd0JJUtV9lISmvAkpHU9qTlIikukktFUt20vzQSHu++F5HqGvDi3PgIgQ5FpL5p/zme+/l1za7IHP6rcSoSfdPeIRCsRcIPtM18rSUMJoeKiIiIiIjw9QNKh3OJhFQBoAAAAABJRU5ErkJggg==";
const DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k-1776148661448.png"; const DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k.png";
const ENDPOINTS = { const ENDPOINTS = {
invitedList: "/team/bd/invite-bdleader-message-own", invitedList: "/team/bd/invite-bdleader-message-own",
invite: "/team/bd/invite-bdleader", invite: "/team/bd/invite-bdleader",

View File

@ -1,109 +1,24 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8">
<meta <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
name="viewport" <title>Invite BD</title>
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" <link rel="stylesheet" href="../bd-static/shared.css">
/>
<title>Invite User To Become BD</title>
<link rel="stylesheet" href="./yumi-invite-bd.css" />
</head> </head>
<body> <body>
<div class="fullPage" id="pageRoot"> <main class="page">
<div class="status-bar-placeholder" id="statusBarPlaceholder"></div> <section class="content">
<div class="header-container"> <div class="row">
<div class="side-area"> <div class="input-box" style="flex:1">
<button class="back-btn" id="backButton" type="button"> <input class="field" id="searchInput" type="text" autocomplete="off">
<img class="flip-img" id="backButtonIcon" src="" alt="" style="width: 100%; aspect-ratio: 1 / 1; display: block" />
</button>
</div>
<h1 class="header-title" id="pageTitle" style="color: black">Invite User To Become BD</h1>
<div class="actions-area">
<div class="language-entry" id="languageEntry">
<div class="language-name" id="languageName">English</div>
</div> </div>
<button class="primary-btn" id="inviteButton" type="button" disabled data-i18n="invite">Invite</button>
</div> </div>
</div> <div id="list"></div>
<div class="subtle" id="empty" data-i18n="no_users_found">No users found</div>
<div style="padding: 16px; position: relative"> </section>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px"> </main>
<div <script type="module" src="./invite.js"></script>
class="surface-border-wrapper"
style="
flex: 1;
min-width: 0;
border-radius: 4px;
"
>
<div class="surface-border-content">
<div style="padding: 0 8px; display: flex; align-items: center; height: max-content">
<input
id="searchInput"
type="text"
placeholder="Enter User ID"
autocomplete="off"
style="
width: 100%;
color: rgba(0, 0, 0, 0.4);
font-weight: 600;
outline: none;
border: none;
background-color: transparent;
padding: 8px;
"
/>
<button
id="clearButton"
type="button"
hidden
style="background: none; border: none; color: #9ca3af; cursor: pointer; padding: 0 4px"
>
×
</button>
</div>
</div>
</div>
<button style="font-weight: 600" class="invite-btn" id="searchButton" type="button" disabled>Search</button>
</div>
<div id="resultList" style="margin: 16px 0; display: flex; flex-direction: column; gap: 12px"></div>
<div id="noData" class="no-data">please input user id</div>
</div>
</div>
<template id="userCardTemplate">
<div
class="surface-border-wrapper user-card"
style="
position: relative;
z-index: 0;
border-radius: 20px;
"
>
<div class="surface-border-content">
<div style="padding: 10px; transition: all 0.2s">
<div class="status-badge" style="position: absolute; z-index: 3; top: -2px; right: -2px"></div>
<div style="font-weight: 600">Information:</div>
<div style="display: flex; align-items: center; justify-content: space-between; gap: 4px">
<div style="flex: 1; min-width: 0; align-self: stretch; display: flex; align-items: center; gap: 4px">
<div style="width: 3em; display: flex; align-items: center; justify-content: center">
<img class="avatar-image" src="" alt="" style="width: 100%; aspect-ratio: 1/1; object-fit: cover; border-radius: 50%" />
</div>
<div style="flex: 1; min-width: 0; align-self: stretch">
<div class="nickname" style="margin-bottom: 4px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap"></div>
<div class="account" style="font-size: 0.9em; color: rgba(0, 0, 0, 0.4); font-weight: 500"></div>
</div>
</div>
<div style="width: auto; min-width: 0; align-self: stretch; display: flex; justify-content: center; align-items: center">
<div class="pending-actions"></div>
</div>
</div>
</div>
</div>
</div>
</template>
<script src="./yumi-invite-bd.js"></script>
</body> </body>
</html> </html>

118
h5/invite-bd/invite.js Normal file
View File

@ -0,0 +1,118 @@
import { applyLang, avatarError, bindHeader, connectToApp, get, post, t, toast } from "../bd-static/shared.js";
const config = {
title: "invite_become_BD",
list: "/team/bd/invite-bd-message-own",
invite: "/team/bd/invite-bd",
cancel: (id) => `/team/bd/invite-bd-cancel/${id}`
};
let invited = [];
let searchResult = [];
let selected = null;
bindHeader(config.title);
applyLang();
const input = document.getElementById("searchInput");
input.placeholder = t("enter_user_id");
input.addEventListener("input", () => {
selected = null;
render();
});
input.addEventListener("keyup", (event) => {
if (event.key === "Enter") search();
});
document.getElementById("inviteButton").addEventListener("click", invite);
connectToApp().then(loadInvited);
async function loadInvited() {
try {
const response = await get(config.list);
invited = response.body || [];
render();
} catch (error) {
toast(error.response?.errorMsg || error.message);
}
}
async function search() {
const account = input.value.trim();
selected = null;
searchResult = [];
if (!account) {
await loadInvited();
return;
}
try {
const response = await get(`/user/user-profile/open-search?account=${encodeURIComponent(account)}`);
if (response.status && response.body) searchResult = [{ userProfile: response.body }];
} catch (error) {
toast(error.response?.errorMsg || error.message);
}
render();
}
function render() {
const list = document.getElementById("list");
const data = input.value.trim() ? searchResult : invited;
list.innerHTML = "";
document.getElementById("empty").hidden = data.length > 0;
data.forEach((item) => {
const user = item.userProfile || {};
const card = document.createElement("article");
card.className = "member-card";
card.innerHTML = `
<img class="avatar" src="${user.userAvatar || ""}" alt="">
<div class="member-main">
<div class="member-name">${user.userNickname || "-"}</div>
<div class="member-meta">${t("user_id_prefix")}${user.account || "-"}</div>
</div>
<div class="member-side"></div>
`;
card.addEventListener("click", () => {
if (!input.value.trim() || item.status === 0 || item.status === 1) return;
selected = selected?.id === user.id ? null : user;
render();
});
const side = card.querySelector(".member-side");
if (item.status === 0) {
side.innerHTML = `<div class="badge pending">${t("pending")}</div><button class="danger-btn" type="button">${t("cancel")}</button>`;
side.querySelector("button").addEventListener("click", (event) => {
event.stopPropagation();
cancelInvite(item.id);
});
} else if (item.status === 1) {
side.innerHTML = `<span class="badge success">${t("success")}</span>`;
} else if (selected?.id === user.id) {
side.innerHTML = `<span class="badge success">✓</span>`;
}
avatarError(card.querySelector("img"));
list.appendChild(card);
});
document.getElementById("inviteButton").disabled = !selected;
}
async function invite() {
if (!selected) return;
try {
await post(config.invite, { inviteUserId: selected.id });
toast(t("invitation_submitted"));
input.value = "";
selected = null;
searchResult = [];
await loadInvited();
} catch (error) {
toast(error.response?.errorMsg || t("something_went_wrong"));
}
}
async function cancelInvite(id) {
try {
await post(config.cancel(id));
toast(t("application_cancelled"));
await loadInvited();
} catch (error) {
const msg = error.response?.errorMsg?.replace(/^.*\]\s*/, "") || t("cancellation_failed_try_again");
toast(msg);
}
}

View File

@ -2,7 +2,7 @@
const API_BASE = "https://jvapi.haiyihy.com"; const API_BASE = "https://jvapi.haiyihy.com";
const BACK_ICON = const BACK_ICON =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGXSURBVHgB7dvBTcNAFIThB5xTQFwADVBBmqEnUgZQAAXAjRqgACgAj2wfECbJZXZnrPmkVRT5lF9ODn4vVRERERERETEaxnNb8cd+PA/jeZ3PY02xqG7KA+Ic6/eds5vfPxfRdelb4uxXrt0VmXqgU3Hgo8iUA52LA9SvF1yVpkviHOdDpRhIJg6oBZKKA0qB5OKASiDJOKAQSDYO9A4kHQd6BpKPA70CWcSBHoFs4kDrQFZxoGUguzjQKpBlHGgRyDYOsANZxwFmIPs4wAq0iTjACvRUG4gDjEeumDRsIg4wAn3VhjDmYt81jWOGf64vo5q3MsAaHOLDH2oa7q2xicQKhLvopTYQiTl63kQk9mzePlKL5QXrSK22O2wjtVx/sYzUej/ILlKPBSqrSL02zGwi9VzBs4jUe0dRPpLCEqd0JJUtV9lISmvAkpHU9qTlIikukktFUt20vzQSHu++F5HqGvDi3PgIgQ5FpL5p/zme+/l1za7IHP6rcSoSfdPeIRCsRcIPtM18rSUMJoeKiIiIiIjw9QNKh3OJhFQBoAAAAABJRU5ErkJggg=="; "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGXSURBVHgB7dvBTcNAFIThB5xTQFwADVBBmqEnUgZQAAXAjRqgACgAj2wfECbJZXZnrPmkVRT5lF9ODn4vVRERERERETEaxnNb8cd+PA/jeZ3PY02xqG7KA+Ic6/eds5vfPxfRdelb4uxXrt0VmXqgU3Hgo8iUA52LA9SvF1yVpkviHOdDpRhIJg6oBZKKA0qB5OKASiDJOKAQSDYO9A4kHQd6BpKPA70CWcSBHoFs4kDrQFZxoGUguzjQKpBlHGgRyDYOsANZxwFmIPs4wAq0iTjACvRUG4gDjEeumDRsIg4wAn3VhjDmYt81jWOGf64vo5q3MsAaHOLDH2oa7q2xicQKhLvopTYQiTl63kQk9mzePlKL5QXrSK22O2wjtVx/sYzUej/ILlKPBSqrSL02zGwi9VzBs4jUe0dRPpLCEqd0JJUtV9lISmvAkpHU9qTlIikukktFUt20vzQSHu++F5HqGvDi3PgIgQ5FpL5p/zme+/l1za7IHP6rcSoSfdPeIRCsRcIPtM18rSUMJoeKiIiIiIjw9QNKh3OJhFQBoAAAAABJRU5ErkJggg==";
const DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k-1776148661448.png"; const DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k.png";
const ENDPOINTS = { const ENDPOINTS = {
invitedList: "/team/bd/invite-bd-message-own", invitedList: "/team/bd/invite-bd-message-own",
invite: "/team/bd/invite-bd", invite: "/team/bd/invite-bd",

View File

@ -2,7 +2,7 @@
const API_BASE = "https://jvapi.haiyihy.com"; const API_BASE = "https://jvapi.haiyihy.com";
const BACK_ICON = const BACK_ICON =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGXSURBVHgB7dvBTcNAFIThB5xTQFwADVBBmqEnUgZQAAXAjRqgACgAj2wfECbJZXZnrPmkVRT5lF9ODn4vVRERERERETEaxnNb8cd+PA/jeZ3PY02xqG7KA+Ic6/eds5vfPxfRdelb4uxXrt0VmXqgU3Hgo8iUA52LA9SvF1yVpkviHOdDpRhIJg6oBZKKA0qB5OKASiDJOKAQSDYO9A4kHQd6BpKPA70CWcSBHoFs4kDrQFZxoGUguzjQKpBlHGgRyDYOsANZxwFmIPs4wAq0iTjACvRUG4gDjEeumDRsIg4wAn3VhjDmYt81jWOGf64vo5q3MsAaHOLDH2oa7q2xicQKhLvopTYQiTl63kQk9mzePlKL5QXrSK22O2wjtVx/sYzUej/ILlKPBSqrSL02zGwi9VzBs4jUe0dRPpLCEqd0JJUtV9lISmvAkpHU9qTlIikukktFUt20vzQSHu++F5HqGvDi3PgIgQ5FpL5p/zme+/l1za7IHP6rcSoSfdPeIRCsRcIPtM18rSUMJoeKiIiIiIjw9QNKh3OJhFQBoAAAAABJRU5ErkJggg=="; "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGXSURBVHgB7dvBTcNAFIThB5xTQFwADVBBmqEnUgZQAAXAjRqgACgAj2wfECbJZXZnrPmkVRT5lF9ODn4vVRERERERETEaxnNb8cd+PA/jeZ3PY02xqG7KA+Ic6/eds5vfPxfRdelb4uxXrt0VmXqgU3Hgo8iUA52LA9SvF1yVpkviHOdDpRhIJg6oBZKKA0qB5OKASiDJOKAQSDYO9A4kHQd6BpKPA70CWcSBHoFs4kDrQFZxoGUguzjQKpBlHGgRyDYOsANZxwFmIPs4wAq0iTjACvRUG4gDjEeumDRsIg4wAn3VhjDmYt81jWOGf64vo5q3MsAaHOLDH2oa7q2xicQKhLvopTYQiTl63kQk9mzePlKL5QXrSK22O2wjtVx/sYzUej/ILlKPBSqrSL02zGwi9VzBs4jUe0dRPpLCEqd0JJUtV9lISmvAkpHU9qTlIikukktFUt20vzQSHu++F5HqGvDi3PgIgQ5FpL5p/zme+/l1za7IHP6rcSoSfdPeIRCsRcIPtM18rSUMJoeKiIiIiIjw9QNKh3OJhFQBoAAAAABJRU5ErkJggg==";
const DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k-1776148661448.png"; const DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k.png";
const ENDPOINTS = { const ENDPOINTS = {
invitedList: "/team/bd/invite-recharge-agent-message-own", invitedList: "/team/bd/invite-recharge-agent-message-own",
invite: "/team/bd/invite-recharge-agent", invite: "/team/bd/invite-recharge-agent",

View File

@ -1 +1 @@
const e=r=>{console.log("头像资源出错"),r.target.onerror=null,r.target.src=new URL("/assets/defaultAvatar-CdxWBK1k-1776148661448.png",import.meta.url).href},t=(r,a)=>{console.log("图片资源出错"),r.target.onerror=null,a=="GOLD"?r.target.src=new URL("/assets/coin-lz3Tp0nX-1776148661447.png",import.meta.url).href:a=="DOLLARS"&&(r.target.src=new URL("/assets/dollar-fhua8OJo-1776148661448.png",import.meta.url).href)};export{t as a,e as h}; const e=r=>{console.log("头像资源出错"),r.target.onerror=null,r.target.src=new URL("/assets/defaultAvatar-CdxWBK1k.png",import.meta.url).href},t=(r,a)=>{console.log("图片资源出错"),r.target.onerror=null,a=="GOLD"?r.target.src=new URL("/assets/coin-lz3Tp0nX-1776148661447.png",import.meta.url).href:a=="DOLLARS"&&(r.target.src=new URL("/assets/dollar-fhua8OJo-1776148661448.png",import.meta.url).href)};export{t as a,e as h};

View File

@ -365,7 +365,7 @@ async function renderMessage() {
async function updateApply(id, status, node) { async function updateApply(id, status, node) {
try { try {
await post("/team/user/apply/record/update", { id, status }); await post("/team/process/user/apply", { id, status });
node.remove(); node.remove();
toast(status === "AGREE" ? "Approved" : "Rejected"); toast(status === "AGREE" ? "Approved" : "Rejected");
} catch (error) { } catch (error) {

View File

@ -1,13 +1,25 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <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"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Platform Policy</title> <title>Platform Policy</title>
<link rel="stylesheet" href="../logic-pages/app.css"> <link rel="stylesheet" href="./platform-policy.css" />
</head> </head>
<body> <body>
<script>window.PAGE_CONFIG = {"title":"Platform Policy","mode":"platformPolicy"};</script> <div class="platform-policy">
<script src="../logic-pages/app.js"></script> <header class="page-header">
<button class="back-button" type="button" data-action="back" aria-label="Back">
<svg viewBox="0 0 32 32" aria-hidden="true"><path d="M20 8l-8 8 8 8"/></svg>
</button>
<h1 data-i18n="platform_policy">Platform Policy</h1>
<div class="header-space"></div>
</header>
<main class="content" id="policyList"></main>
<div class="toast" id="toast" hidden></div>
</div>
<script type="module" src="./platform-policy.js"></script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,229 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: "Baloo 2", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.35;
}
html,
body {
width: 100%;
min-height: 100%;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-webkit-overflow-scrolling: touch;
-webkit-tap-highlight-color: transparent;
}
body {
color: rgba(0, 0, 0, 0.8);
background: #fff;
}
button {
border: 0;
background: transparent;
color: inherit;
font: inherit;
}
.platform-policy {
width: 100vw;
min-height: 100vh;
background-color: #fff;
background-image: url("/assets/secondBg-CSqvFWr0-1776148661447.png");
background-size: cover;
background-position: top center;
}
.page-header {
position: sticky;
top: 0;
z-index: 10;
display: grid;
grid-template-columns: 44px 1fr 44px;
align-items: center;
min-height: calc(48px + env(safe-area-inset-top));
padding: env(safe-area-inset-top) 10px 0;
background: rgba(255, 255, 255, 0.96);
}
.page-header h1 {
min-width: 0;
color: #111;
font-size: 1.25em;
font-weight: 700;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.back-button {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
}
.back-button svg {
width: 28px;
height: 28px;
}
.back-button path {
fill: none;
stroke: #111;
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
}
.content {
position: relative;
z-index: 2;
padding: 16px;
}
.policy-stack {
display: flex;
flex-direction: column;
gap: 16px;
}
.policy-card {
position: relative;
padding: 16px 20vw 16px 16px;
border: 1px solid rgba(194, 217, 240, 0.82);
border-radius: 20px;
background: #fff;
box-shadow: 0 8px 20px rgba(69, 129, 89, 0.12);
}
.level-badge {
position: absolute;
top: -1vw;
right: 2vw;
width: 11vw;
min-width: 48px;
max-width: 82px;
aspect-ratio: 1 / 1;
background-image: url("/assets/levelBg--Rsqxs_B-1776148661447.png");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 1.5em;
font-weight: 400;
}
.policy-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 5px;
}
.policy-cell {
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
text-align: center;
}
.policy-cell.align-left {
width: max-content;
}
.policy-cell.align-right {
align-items: flex-end;
}
.label {
color: rgba(0, 0, 0, 0.8);
font-weight: 600;
}
.value {
color: rgba(0, 0, 0, 0.8);
font-weight: 500;
overflow-wrap: anywhere;
}
.empty-state {
min-height: 50vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #666;
text-align: center;
}
.empty-icon {
margin-bottom: 16px;
font-size: 48px;
}
.empty-state p {
margin: 0;
font-size: 16px;
}
.toast {
position: fixed;
left: 50%;
bottom: calc(22px + env(safe-area-inset-bottom));
z-index: 30;
max-width: calc(100% - 40px);
transform: translateX(-50%);
padding: 9px 13px;
border-radius: 999px;
background: rgba(22, 56, 36, 0.92);
color: #fff;
font-size: 0.9em;
font-weight: 700;
text-align: center;
}
[hidden] {
display: none !important;
}
[dir="rtl"] .policy-grid {
direction: rtl;
}
[dir="rtl"] .level-badge {
right: auto;
left: 2vw;
}
[dir="rtl"] .policy-card {
padding: 16px 16px 16px 20vw;
}
@media screen and (max-width: 360px) {
* {
font-size: 10px;
}
}
@media screen and (min-width: 360px) {
* {
font-size: 12px;
}
}
@media screen and (min-width: 768px) {
* {
font-size: 24px;
}
}

View File

@ -0,0 +1,285 @@
const API_BASE_URL = "https://jvapi.haiyihy.com";
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 i18n = {
en: {
platform_policy: "Platform Policy",
time: "Time",
hours: "hours",
target: "Target",
host_salary: "Host salary",
agent_salary: "Agent salary",
total_salary: "Total salary",
no_policy_data_available: "No policy data available"
},
ar: {
platform_policy: "سياسة المنصة",
time: "الوقت",
hours: "ساعات",
target: "الهدف",
host_salary: "راتب المضيف",
agent_salary: "راتب الوكيل",
total_salary: "إجمالي الراتب",
no_policy_data_available: "لا توجد بيانات سياسة"
},
tr: {
platform_policy: "Platform politikası",
time: "Süre",
hours: "saat",
target: "Hedef",
host_salary: "Host maaşı",
agent_salary: "Ajans maaşı",
total_salary: "Toplam maaş",
no_policy_data_available: "Politika verisi yok"
},
bn: {
platform_policy: "প্ল্যাটফর্ম নীতি",
time: "সময়",
hours: "ঘণ্টা",
target: "টার্গেট",
host_salary: "হোস্ট বেতন",
agent_salary: "এজেন্ট বেতন",
total_salary: "মোট বেতন",
no_policy_data_available: "কোনো নীতি ডেটা নেই"
}
};
const state = {
lang: resolveLanguage(),
fromAgencyCenter: new URLSearchParams(window.location.search).get("from") === "agencycenter",
policies: []
};
const $ = (selector) => document.querySelector(selector);
const t = (key) => (i18n[state.lang] && i18n[state.lang][key]) || i18n.en[key] || key;
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
});
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;
}
function getTeamId() {
const cached = localStorage.getItem("teamInfo");
if (cached) {
try {
const teamInfo = JSON.parse(cached);
if (teamInfo && teamInfo.id) return teamInfo.id;
} catch (error) {
console.error("Failed to parse teamInfo:", error);
}
}
return null;
}
function formatTarget(value) {
if (!value) return "0";
const number = parseFloat(value);
if (Number.isNaN(number)) return value;
if (number >= 1000000) return `${(number / 1000000).toFixed(1)}M`;
if (number >= 1000) return `${(number / 1000).toFixed(1)}K`;
return number.toString();
}
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 renderI18n() {
document.documentElement.lang = state.lang;
document.documentElement.dir = state.lang === "ar" ? "rtl" : "ltr";
document.querySelectorAll("[data-i18n]").forEach((node) => {
node.textContent = t(node.dataset.i18n);
});
}
function policyCell(label, value, className = "") {
return `<div class="policy-cell ${className}">
<div class="label">${label}</div>
<div class="value">${value}</div>
</div>`;
}
function renderPolicies() {
const root = $("#policyList");
if (!state.policies.length) {
root.innerHTML = `<div class="empty-state"><div class="empty-icon">📋</div><p>${t("no_policy_data_available")}</p></div>`;
return;
}
root.innerHTML = `<div class="policy-stack">${state.policies.map((item) => `
<section class="policy-card">
<div class="level-badge">Lv.${item.level}</div>
<div class="policy-grid">
${policyCell(`${t("time")} (${t("hours")})`, item.onlineTime || 0, "align-left")}
${policyCell(t("target"), formatTarget(item.target))}
${policyCell(t("host_salary"), `$${item.memberSalary || 0}`, "align-right")}
${state.fromAgencyCenter ? policyCell(t("agent_salary"), `$${item.ownSalary || 0}`, "align-left") : ""}
${state.fromAgencyCenter ? policyCell(t("total_salary"), `$${item.totalSalary || 0}`) : ""}
</div>
</section>
`).join("")}</div>`;
}
async function fetchPolicies() {
const teamId = getTeamId();
const result = await apiGet(`/team/release/policy?id=${teamId}`);
if (result.status && result.body && result.body.policy) {
state.policies = result.body.policy || [];
} else {
state.policies = [];
}
}
async function init() {
renderI18n();
$("[data-action='back']").addEventListener("click", closePage);
try {
await connectToApp();
await fetchPolicies();
} catch (error) {
console.error("Failed to fetch team policy:", error);
state.policies = [];
showToast(error.response?.errorMsg || error.message);
} finally {
renderPolicies();
}
}
init();

View File

@ -1,13 +1,19 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <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"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>BD Policy</title> <title>BD Policy</title>
<link rel="stylesheet" href="../logic-pages/app.css"> <link rel="stylesheet" href="../bd-static/shared.css">
</head> </head>
<body> <body>
<script>window.PAGE_CONFIG = {"title":"BD Policy","mode":"policy"};</script> <main class="page">
<script src="../logic-pages/app.js"></script> <section class="content">
<article class="card card-pad">
<div class="subtle" id="policyHint"></div>
</article>
</section>
</main>
<script type="module" src="./policy.js"></script>
</body> </body>
</html> </html>

7
h5/policy/policy.js Normal file
View File

@ -0,0 +1,7 @@
import { applyLang, bindHeader, connectToApp, t } from "../bd-static/shared.js";
const from = new URLSearchParams(window.location.search).get("from") || "BDLead";
bindHeader(from === "BD" ? "bd_policy" : "bd_leader_policy");
applyLang();
document.getElementById("policyHint").textContent = from === "BD" ? t("bd_policy") : t("bd_leader_policy");
connectToApp();

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<circle cx="14" cy="14" r="8" stroke="#1F6F46" stroke-width="3"/>
<path d="M20 20l6 6" stroke="#1F6F46" stroke-width="3" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 230 B

View File

@ -1,13 +1,47 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <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"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Search Payee</title> <title>Search Payee</title>
<link rel="stylesheet" href="../logic-pages/app.css"> <link rel="stylesheet" href="./search-payee.css" />
</head> </head>
<body> <body>
<script>window.PAGE_CONFIG = {"title":"Search Payee","mode":"teamMembers"};</script> <div class="search-payee">
<script src="../logic-pages/app.js"></script> <header class="page-header">
<button class="header-icon" type="button" data-action="back" aria-label="Back"><img id="backIcon" alt="" /></button>
<h1 data-i18n="search_payee">Search Payee</h1>
<button class="lang-button" type="button" data-action="language">EN</button>
</header>
<main class="content">
<section class="search-row">
<div class="search-box">
<img id="searchIcon" alt="" />
<input id="searchInput" type="text" class="search-input" data-i18n-placeholder="search_user" placeholder="Search user" />
</div>
<button id="searchButton" class="search-action" type="button" data-i18n="search">Search</button>
<button id="cancelButton" class="search-action strong" type="button" data-i18n="cancel" hidden>Cancel</button>
</section>
<section id="results" class="results"></section>
<section id="loadingState" class="state" hidden><span data-i18n="searching">Searching</span>...</section>
</main>
<div class="modal-layer" id="confirmModal" hidden>
<div class="modal-card">
<div class="modal-title" data-i18n="tips">Tips</div>
<div class="modal-message" data-i18n="confirm_transfer_to_user">Confirm transfer to this user?</div>
<div class="modal-actions">
<button class="modal-secondary" type="button" data-action="cancel-select" data-i18n="cancel">Cancel</button>
<button class="modal-primary" type="button" data-action="confirm-select" data-i18n="confirm">Confirm</button>
</div>
</div>
</div>
<div class="toast" id="toast" hidden></div>
</div>
<script type="module" src="./search-payee.js"></script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,315 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: "Baloo 2", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.35;
}
html,
body {
width: 100%;
min-height: 100%;
background: #edf8f1;
color: rgba(0, 0, 0, 0.8);
-webkit-text-size-adjust: 100%;
-webkit-overflow-scrolling: touch;
-webkit-tap-highlight-color: transparent;
}
button,
input {
border: 0;
background: transparent;
color: inherit;
font: inherit;
}
.search-payee {
width: 100vw;
min-height: 100vh;
background: #edf8f1;
}
.page-header {
position: sticky;
top: 0;
z-index: 10;
display: grid;
grid-template-columns: 44px 1fr 58px;
align-items: center;
min-height: calc(48px + env(safe-area-inset-top));
padding: env(safe-area-inset-top) 10px 0;
background: #f8fff9;
border-bottom: 1px solid #cce9d7;
box-shadow: 0 4px 14px rgba(75, 137, 95, 0.14);
}
.page-header h1 {
min-width: 0;
color: #153d28;
font-size: 1.25em;
font-weight: 800;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.header-icon,
.lang-button {
min-width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
color: #1f6f46;
font-weight: 800;
}
.header-icon {
padding: 6px;
}
.header-icon img {
width: 100%;
height: 100%;
display: block;
}
.lang-button {
justify-self: end;
padding: 0 10px;
border-radius: 999px;
background: #dff5e7;
font-size: 0.86em;
}
.content {
display: flex;
flex-direction: column;
gap: 14px;
padding: 16px;
}
.search-row {
display: flex;
gap: 4px;
}
.search-box {
flex: 1;
display: flex;
align-items: center;
gap: 4px;
border: 1px solid #cce9d7;
border-radius: 32px;
background: #fff;
padding: 4px 12px;
}
.search-box img {
width: 1.5em;
opacity: 0.65;
}
.search-input {
flex: 1;
width: 100%;
outline: none;
padding: 4px 0;
}
.search-action {
color: rgba(0, 0, 0, 0.45);
font-weight: 800;
}
.search-action.strong {
color: #173a28;
}
.results {
margin: 6px 0;
}
.result-card {
margin-bottom: 12px;
border: 1px solid #b7e0c6;
border-radius: 18px;
background: #fbfffc;
box-shadow: 0 8px 20px rgba(69, 129, 89, 0.12);
padding: 16px;
}
.result-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
}
.result-main {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 8px;
}
.avatar-wrap {
width: 3.5em;
height: 3.5em;
flex: 0 0 3.5em;
}
.avatar-wrap img {
display: block;
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
background: #dff5e7;
}
.result-info {
flex: 1;
min-width: 0;
}
.result-name {
font-weight: 900;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.result-id {
color: rgba(0, 0, 0, 0.45);
font-size: 0.9em;
font-weight: 700;
}
.add-button {
width: auto;
border-radius: 12px;
background: #ff9500;
color: #fff;
font-size: 0.9em;
font-weight: 800;
padding: 4px 10px;
}
.state {
padding: 20px 0;
color: #5f7468;
font-weight: 800;
text-align: center;
}
.modal-layer {
position: fixed;
inset: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.45);
}
.modal-card {
width: 80%;
max-width: 420px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 20px;
border-radius: 12px;
background: #fff;
}
.modal-title {
color: rgba(0, 0, 0, 0.8);
font-size: 1.2em;
font-weight: 900;
}
.modal-message {
color: rgba(0, 0, 0, 0.45);
font-size: 1em;
font-weight: 700;
text-align: center;
}
.modal-actions {
width: 100%;
display: flex;
align-items: center;
justify-content: space-around;
}
.modal-primary,
.modal-secondary {
width: 45%;
display: flex;
align-items: center;
justify-content: center;
min-height: 38px;
padding: 8px 12px;
border-radius: 70px;
font-weight: 900;
}
.modal-primary {
background: #ff9500;
color: #fff;
}
.modal-secondary {
border: 1px solid #ffb805;
background: #fff;
color: #ff9500;
}
.toast {
position: fixed;
left: 50%;
bottom: calc(22px + env(safe-area-inset-bottom));
z-index: 30;
max-width: calc(100% - 40px);
transform: translateX(-50%);
padding: 9px 13px;
border-radius: 999px;
background: rgba(22, 56, 36, 0.92);
color: #fff;
font-size: 0.9em;
font-weight: 700;
text-align: center;
}
[hidden] {
display: none !important;
}
[dir="rtl"] .header-icon img {
transform: scaleX(-1);
}
@media screen and (max-width: 360px) {
* {
font-size: 10px;
}
}
@media screen and (min-width: 360px) {
* {
font-size: 12px;
}
}
@media screen and (min-width: 768px) {
* {
font-size: 24px;
}
}

View File

@ -0,0 +1,387 @@
const API_BASE_URL = "https://jvapi.haiyihy.com";
const DEFAULT_AVATAR = "/assets/defaultAvatar-CdxWBK1k-1776148661448.png";
const PAYEE_KEY = "transfer.payee";
const icons = {
back: "../host-center/icons/back.svg",
search: "./icons/search.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 i18n = {
en: {
search_payee: "Search Payee",
search_user: "Search user",
search: "Search",
cancel: "Cancel",
searching: "Searching",
tips: "Tips",
confirm_transfer_to_user: "Confirm transfer to this user?",
confirm: "Confirm",
add: "Add",
user_id_prefix: "ID:",
user_not_found_or_search_failed: "User not found or search failed"
},
ar: {
search_payee: "بحث عن المستلم",
search_user: "ابحث عن المستخدم",
search: "بحث",
cancel: "إلغاء",
searching: "جاري البحث",
tips: "نصائح",
confirm_transfer_to_user: "تأكيد التحويل إلى هذا المستخدم؟",
confirm: "تأكيد",
add: "إضافة",
user_id_prefix: "ID:",
user_not_found_or_search_failed: "لم يتم العثور على المستخدم أو فشل البحث"
},
tr: {
search_payee: "Alıcı Ara",
search_user: "Kullanıcı ara",
search: "Ara",
cancel: "İptal",
searching: "Aranıyor",
tips: "İpuçları",
confirm_transfer_to_user: "Bu kullanıcıya transferi onaylıyor musunuz?",
confirm: "Onayla",
add: "Ekle",
user_id_prefix: "ID:",
user_not_found_or_search_failed: "Kullanıcı bulunamadı veya arama başarısız"
},
bn: {
search_payee: "প্রাপক সার্চ",
search_user: "ব্যবহারকারী সার্চ",
search: "সার্চ",
cancel: "বাতিল",
searching: "সার্চ হচ্ছে",
tips: "টিপস",
confirm_transfer_to_user: "এই ব্যবহারকারীকে ট্রান্সফার নিশ্চিত করবেন?",
confirm: "নিশ্চিত",
add: "যোগ",
user_id_prefix: "ID:",
user_not_found_or_search_failed: "ব্যবহারকারী পাওয়া যায়নি বা সার্চ ব্যর্থ"
}
};
const state = {
lang: resolveLanguage(),
query: "",
results: [],
loading: false,
searched: false,
selected: null
};
const $ = (selector) => document.querySelector(selector);
const $$ = (selector) => Array.from(document.querySelectorAll(selector));
const t = (key) => (i18n[state.lang] && i18n[state.lang][key]) || i18n.en[key] || key;
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({ success: true });
return;
}
let attempt = 0;
const poll = () => {
attempt += 1;
if (window.app && typeof window.app.getAccessOrigin === "function") {
const raw = window.app.getAccessOrigin();
if (raw) setHeadersFromApp(parseAccessOrigin(raw));
resolve({ success: true });
return;
}
if (attempt < 50) {
window.setTimeout(poll, 100);
return;
}
if (window.FlutterApp && typeof window.FlutterApp.postMessage === "function") {
window.FlutterApp.postMessage("requestAccessOrigin");
}
resolve({ success: true });
};
poll();
}).then((result) => {
if (typeof result === "string") setHeadersFromApp(parseAccessOrigin(result));
return result;
});
}
async function apiGet(path) {
const response = await fetch(`${API_BASE_URL}${path}`, {
method: "GET",
headers: commonHeaders
});
const data = await response.json().catch(() => ({}));
if (!response.ok || data.status === false) {
const error = new Error(data.errorMsg || data.message || `HTTP ${response.status}`);
error.response = data;
throw error;
}
return data;
}
async function searchPayee() {
const query = $("#searchInput").value.trim();
state.query = query;
if (!query) {
state.results = [];
renderResults();
return;
}
state.loading = true;
state.searched = false;
renderResults();
try {
const result = await apiGet(`/wallet/bank/search/user-profile?account=${encodeURIComponent(query)}&type=AGENT&searchType=TRANSFER`);
if (result.status && result.body) {
state.results = [{
id: result.body.id,
account: result.body.account,
name: result.body.userNickname,
avatar: result.body.userAvatar,
sex: result.body.userSex,
age: result.body.age,
countryName: result.body.countryName,
countryCode: result.body.countryCode,
accountStatus: result.body.accountStatus,
goodID: result.body.ownSpecialId?.account
}];
} else {
state.results = [];
}
} catch (error) {
console.error("Search failed:", error);
state.results = [];
showToast(t("user_not_found_or_search_failed"));
} finally {
state.loading = false;
state.searched = true;
renderResults();
}
}
function resetSearch() {
state.searched = false;
state.query = "";
state.results = [];
$("#searchInput").value = "";
renderResults();
}
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 showConfirm(user) {
state.selected = user;
$("#confirmModal").hidden = false;
}
function hideConfirm() {
state.selected = null;
$("#confirmModal").hidden = true;
}
function normalizeRoute(target) {
const url = new URL(target, window.location.origin);
const currentLang = new URLSearchParams(window.location.search).get("lang");
if (currentLang && !url.searchParams.has("lang")) url.searchParams.set("lang", currentLang);
return `${url.pathname}${url.search}${url.hash}`;
}
function goBackToTransfer() {
const from = new URLSearchParams(window.location.search).get("from") || "/transfer";
window.location.href = normalizeRoute(from);
}
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 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 = t(node.dataset.i18n);
});
$$("[data-i18n-placeholder]").forEach((node) => {
node.placeholder = t(node.dataset.i18nPlaceholder);
});
}
function renderIcons() {
$("#backIcon").src = icons.back;
$("#searchIcon").src = icons.search;
}
function renderSearchActions() {
const hasQuery = Boolean($("#searchInput").value.trim());
$("#searchButton").hidden = state.searched || !hasQuery;
$("#cancelButton").hidden = !state.searched && !hasQuery;
}
function renderResults() {
$("#loadingState").hidden = !state.loading;
renderSearchActions();
const root = $("#results");
root.innerHTML = "";
if (state.loading) return;
state.results.forEach((user) => {
const card = document.createElement("div");
card.className = "result-card";
card.innerHTML = `
<div class="result-row">
<div class="result-main">
<div class="avatar-wrap"><img src="${user.avatar || ""}" alt="" /></div>
<div class="result-info">
<div class="result-name"></div>
<div class="result-id"><span>${t("user_id_prefix")}</span> <span></span></div>
</div>
</div>
<button class="add-button" type="button">${t("add")}</button>
</div>`;
card.querySelector(".result-name").textContent = user.name;
card.querySelector(".result-id span:last-child").textContent = user.goodID || user.account || "-";
card.querySelector(".add-button").addEventListener("click", () => showConfirm(user));
card.querySelector("img").addEventListener("error", (event) => {
event.target.onerror = null;
event.target.src = DEFAULT_AVATAR;
});
root.appendChild(card);
});
}
function render() {
renderI18n();
renderIcons();
renderResults();
}
function bindEvents() {
$("[data-action='back']").addEventListener("click", closePage);
$(".lang-button").addEventListener("click", () => {
window.location.href = normalizeRoute("/language");
});
$("#searchInput").addEventListener("input", () => {
if (state.searched) state.searched = false;
renderSearchActions();
});
$("#searchInput").addEventListener("keyup", (event) => {
if (event.key === "Enter") searchPayee();
});
$("#searchButton").addEventListener("click", searchPayee);
$("#cancelButton").addEventListener("click", resetSearch);
$("#confirmModal").addEventListener("click", hideConfirm);
$(".modal-card").addEventListener("click", (event) => event.stopPropagation());
$("[data-action='cancel-select']").addEventListener("click", hideConfirm);
$("[data-action='confirm-select']").addEventListener("click", () => {
if (!state.selected) return;
sessionStorage.setItem(PAYEE_KEY, JSON.stringify(state.selected));
hideConfirm();
goBackToTransfer();
});
}
async function init() {
bindEvents();
render();
await connectToApp();
}
init();

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<circle cx="24" cy="24" r="20" fill="#FFF3C8"/>
<circle cx="24" cy="24" r="14" fill="#FFFFFF" stroke="#A07100" stroke-width="2.6"/>
<path d="M24 14v20M29 18h-7a3.5 3.5 0 0 0 0 7h4a3.5 3.5 0 0 1 0 7h-7" stroke="#A07100" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 373 B

View File

@ -1,13 +1,67 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <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"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Transfer</title> <title>Transfer</title>
<link rel="stylesheet" href="../logic-pages/app.css"> <link rel="stylesheet" href="./transfer.css" />
</head> </head>
<body> <body>
<script>window.PAGE_CONFIG = {"title":"Transfer","mode":"walletAction","action":"Transfer"};</script> <div class="transfer">
<script src="../logic-pages/app.js"></script> <header class="page-header">
<button class="header-icon" type="button" data-action="back" aria-label="Back"><img id="backIcon" alt="" /></button>
<h1 data-i18n="transfer">Transfer</h1>
<button class="lang-button" type="button" data-action="language">EN</button>
</header>
<main class="content">
<section>
<div class="section-row">
<div class="section-title" data-i18n="information">Information</div>
<button class="details-link" type="button" data-route="/information-details">
<span data-i18n="details">Details</span>
<img id="detailsArrow" alt="" />
</button>
</div>
<div class="surface-card info-card">
<span class="info-label" data-i18n="available_salaries">Available salaries</span>
<span class="loading-text" id="balanceLoading" hidden><span data-i18n="loading">Loading</span>...</span>
<span class="balance-value" id="balanceValue">0.00</span>
</div>
</section>
<section>
<div class="section-title" data-i18n="transfer_to_others">Transfer to others</div>
<div class="surface-card payee-card">
<div id="payeeSelected" class="payee-selected" hidden>
<div class="payee-row">
<div class="avatar-wrap"><img id="payeeAvatar" alt="" /></div>
<div class="payee-info">
<div id="payeeName" class="payee-name"></div>
<div class="payee-id"><span data-i18n="user_id_prefix">ID:</span> <span id="payeeAccount"></span></div>
</div>
<button class="change-button" type="button" data-action="search-payee" data-i18n="change">Change</button>
</div>
</div>
<div id="payeeEmpty" class="payee-empty">
<button class="search-button" type="button" data-action="search-payee" data-i18n="search">Search</button>
</div>
</div>
</section>
<section>
<div class="surface-card amount-card">
<div class="amount-grid" id="amountGrid"></div>
</div>
<div class="button-wrap">
<button class="transfer-button" id="transferButton" type="button" disabled data-i18n="transfer">Transfer</button>
</div>
</section>
</main>
<div class="toast" id="toast" hidden></div>
</div>
<script type="module" src="./transfer.js"></script>
</body> </body>
</html> </html>

332
h5/transfer/transfer.css Normal file
View File

@ -0,0 +1,332 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: "Baloo 2", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.35;
}
html,
body {
width: 100%;
min-height: 100%;
background: #edf8f1;
color: rgba(0, 0, 0, 0.8);
-webkit-text-size-adjust: 100%;
-webkit-overflow-scrolling: touch;
-webkit-tap-highlight-color: transparent;
}
button {
border: 0;
background: transparent;
color: inherit;
font: inherit;
}
.transfer {
width: 100vw;
min-height: 100vh;
background: #edf8f1;
}
.page-header {
position: sticky;
top: 0;
z-index: 10;
display: grid;
grid-template-columns: 44px 1fr 58px;
align-items: center;
min-height: calc(48px + env(safe-area-inset-top));
padding: env(safe-area-inset-top) 10px 0;
background: #f8fff9;
border-bottom: 1px solid #cce9d7;
box-shadow: 0 4px 14px rgba(75, 137, 95, 0.14);
}
.page-header h1 {
min-width: 0;
color: #153d28;
font-size: 1.25em;
font-weight: 800;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.header-icon,
.lang-button {
min-width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
color: #1f6f46;
font-weight: 800;
}
.header-icon {
padding: 6px;
}
.header-icon img {
width: 100%;
height: 100%;
display: block;
}
.lang-button {
justify-self: end;
padding: 0 10px;
border-radius: 999px;
background: #dff5e7;
font-size: 0.86em;
}
.content {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
gap: 14px;
padding: 16px;
}
.section-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.section-title {
margin-bottom: 10px;
color: #244735;
font-weight: 800;
}
.section-row .section-title {
margin-bottom: 0;
}
.details-link {
display: flex;
align-items: center;
color: rgba(0, 0, 0, 0.42);
font-weight: 700;
}
.details-link img {
width: 16px;
height: 16px;
margin-left: 4px;
}
.surface-card {
border: 1px solid #b7e0c6;
border-radius: 18px;
background: #fbfffc;
box-shadow: 0 8px 20px rgba(69, 129, 89, 0.12);
}
.info-card {
display: flex;
align-items: center;
gap: 8px;
padding: 16px;
}
.info-label {
font-size: 1em;
font-weight: 700;
}
.balance-value {
color: #173a28;
font-size: 1.1em;
font-weight: 900;
}
.loading-text {
color: #6d7e72;
font-weight: 700;
}
.payee-card {
min-height: 78px;
}
.payee-empty {
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
.search-button {
padding: 10px 16px;
border-radius: 8px;
background: #fff3c8;
color: rgba(0, 0, 0, 0.8);
font-weight: 800;
}
.payee-selected {
width: 100%;
}
.payee-row {
display: flex;
align-items: center;
gap: 8px;
padding: 16px;
}
.avatar-wrap {
width: 3.5em;
height: 3.5em;
flex: 0 0 3.5em;
}
.avatar-wrap img {
display: block;
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
background: #dff5e7;
}
.payee-info {
flex: 1;
min-width: 0;
}
.payee-name {
margin-bottom: 4px;
color: #333;
font-weight: 800;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.payee-id {
color: #666;
font-size: 0.9em;
}
.change-button {
padding: 2px 8px;
border: 1px solid #d9eadf;
border-radius: 12px;
color: rgba(0, 0, 0, 0.42);
font-weight: 700;
}
.amount-card {
margin-bottom: 12px;
padding: 16px;
}
.amount-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.coin-item {
aspect-ratio: 1 / 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 2px solid #cce9d7;
border-radius: 12px;
background: #e9f8ef;
cursor: pointer;
transition: transform 0.2s, border-color 0.2s, background 0.2s;
}
.coin-item.selected {
border-color: #49a86d;
background: #c9efd7;
}
.coin-item:active {
transform: scale(0.98);
}
.coin-item img {
width: 40%;
margin-bottom: 6px;
}
.coin-price {
color: #131111;
font-weight: 800;
}
.button-wrap {
display: flex;
justify-content: center;
margin-top: 10px;
}
.transfer-button {
width: 70%;
border-radius: 32px;
background: #ff9500;
padding: 12px;
color: #fff;
font-weight: 800;
}
.transfer-button:disabled {
background: #c5c5c5;
color: #fff;
}
.toast {
position: fixed;
left: 50%;
bottom: calc(22px + env(safe-area-inset-bottom));
z-index: 30;
max-width: calc(100% - 40px);
transform: translateX(-50%);
padding: 9px 13px;
border-radius: 999px;
background: rgba(22, 56, 36, 0.92);
color: #fff;
font-size: 0.9em;
font-weight: 700;
text-align: center;
}
[hidden] {
display: none !important;
}
[dir="rtl"] .details-link img,
[dir="rtl"] .header-icon img {
transform: scaleX(-1);
}
@media screen and (max-width: 360px) {
* {
font-size: 10px;
}
}
@media screen and (min-width: 360px) {
* {
font-size: 12px;
}
}
@media screen and (min-width: 768px) {
* {
font-size: 24px;
}
}

438
h5/transfer/transfer.js Normal file
View File

@ -0,0 +1,438 @@
const API_BASE_URL = "https://jvapi.haiyihy.com";
const DEFAULT_AVATAR = "/assets/defaultAvatar-CdxWBK1k-1776148661448.png";
const PAYEE_KEY = "transfer.payee";
const icons = {
back: "../host-center/icons/back.svg",
arrow: "../host-center/icons/arrow.svg",
dollar: "./icons/dollar.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"
};
let preservePayeeOnLeave = false;
const i18n = {
en: {
transfer: "Transfer",
information: "Information",
details: "Details",
available_salaries: "Available salaries",
loading: "Loading",
transfer_to_others: "Transfer to others",
search: "Search",
change: "Change",
user_id_prefix: "ID:",
failed_to_load_balance: "Failed to load balance",
please_select_amount: "Please select amount",
please_select_payee: "Please select payee",
transfer_dollar_success: "Transfer ${price} to {name} successfully"
},
ar: {
transfer: "تحويل",
information: "المعلومات",
details: "التفاصيل",
available_salaries: "الرواتب المتاحة",
loading: "تحميل",
transfer_to_others: "تحويل للآخرين",
search: "بحث",
change: "تغيير",
user_id_prefix: "ID:",
failed_to_load_balance: "فشل تحميل الرصيد",
please_select_amount: "يرجى اختيار المبلغ",
please_select_payee: "يرجى اختيار المستلم",
transfer_dollar_success: "تم تحويل ${price} إلى {name} بنجاح"
},
tr: {
transfer: "Transfer",
information: "Bilgi",
details: "Detaylar",
available_salaries: "Kullanılabilir maaşlar",
loading: "Yükleniyor",
transfer_to_others: "Başkalarına transfer",
search: "Ara",
change: "Değiştir",
user_id_prefix: "ID:",
failed_to_load_balance: "Bakiye yüklenemedi",
please_select_amount: "Lütfen tutar seçin",
please_select_payee: "Lütfen alıcı seçin",
transfer_dollar_success: "${price}, {name} kullanıcısına başarıyla aktarıldı"
},
bn: {
transfer: "ট্রান্সফার",
information: "তথ্য",
details: "বিস্তারিত",
available_salaries: "উপলভ্য বেতন",
loading: "লোড হচ্ছে",
transfer_to_others: "অন্যদের ট্রান্সফার",
search: "সার্চ",
change: "পরিবর্তন",
user_id_prefix: "ID:",
failed_to_load_balance: "ব্যালেন্স লোড করতে ব্যর্থ",
please_select_amount: "পরিমাণ নির্বাচন করুন",
please_select_payee: "প্রাপক নির্বাচন করুন",
transfer_dollar_success: "${price} {name} কে সফলভাবে ট্রান্সফার হয়েছে"
}
};
const state = {
lang: resolveLanguage(),
balance: "0.00",
loading: false,
selectedId: null,
payee: {
id: null,
account: "",
name: "",
avatar: "",
type: "",
isHost: false,
isAgency: false,
hasSalary: false
},
amounts: [
{ 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 $ = (selector) => document.querySelector(selector);
const $$ = (selector) => Array.from(document.querySelectorAll(selector));
const t = (key, params = {}) => {
let text = (i18n[state.lang] && i18n[state.lang][key]) || i18n.en[key] || key;
Object.entries(params).forEach(([name, value]) => {
text = text.replaceAll(`{${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({ success: true });
return;
}
let attempt = 0;
const poll = () => {
attempt += 1;
if (window.app && typeof window.app.getAccessOrigin === "function") {
const raw = window.app.getAccessOrigin();
if (raw) setHeadersFromApp(parseAccessOrigin(raw));
resolve({ success: true });
return;
}
if (attempt < 50) {
window.setTimeout(poll, 100);
return;
}
if (window.FlutterApp && typeof window.FlutterApp.postMessage === "function") {
window.FlutterApp.postMessage("requestAccessOrigin");
}
resolve({ success: true });
};
poll();
}).then((result) => {
if (typeof result === "string") setHeadersFromApp(parseAccessOrigin(result));
return result;
});
}
async function request(method, path, body) {
const response = await fetch(`${API_BASE_URL}${path}`, {
method,
headers: {
...commonHeaders,
"content-type": "application/json"
},
body: body === undefined ? undefined : JSON.stringify(body)
});
const data = await response.json().catch(() => ({}));
if (!response.ok || data.status === false) {
const error = new Error(data.errorMsg || data.message || `HTTP ${response.status}`);
error.response = data;
throw error;
}
return data;
}
const apiGet = (path) => request("GET", path);
const apiPost = (path, body) => request("POST", path, body);
function formatMoney(value) {
const number = Number(value || 0);
return Number.isFinite(number) ? number.toFixed(2) : "0.00";
}
function getStoredPayee() {
try {
const payee = JSON.parse(sessionStorage.getItem(PAYEE_KEY) || "null");
if (payee) Object.assign(state.payee, payee);
} catch (error) {
console.error("Failed to parse transfer payee:", error);
}
}
function clearPayee() {
Object.assign(state.payee, {
id: null,
account: "",
name: "",
avatar: "",
type: "",
isHost: false,
isAgency: false,
hasSalary: false
});
sessionStorage.removeItem(PAYEE_KEY);
}
async function fetchBalance() {
state.loading = 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(t("failed_to_load_balance"));
} finally {
state.loading = false;
renderBalance();
}
}
async function submitTransfer() {
if (!state.selectedId) {
showToast(t("please_select_amount"));
return;
}
if (!state.payee.id) {
showToast(t("please_select_payee"));
return;
}
const selected = state.amounts.find((item) => item.id === state.selectedId);
const payload = {
amount: selected.price,
acceptUserId: state.payee.id,
acceptMethod: "BANK_TRANSFER"
};
try {
const result = await apiPost("/wallet/bank/transferGold", payload);
if (result.status) {
showToast(t("transfer_dollar_success", { price: selected.price, name: state.payee.name }));
state.selectedId = null;
await fetchBalance();
renderAmounts();
}
} catch (error) {
console.error("Transfer failed:", error);
showToast(error.response?.errorMsg || error.message);
}
}
function normalizeRoute(target) {
const url = new URL(target, window.location.origin);
const currentLang = new URLSearchParams(window.location.search).get("lang");
if (currentLang && !url.searchParams.has("lang")) url.searchParams.set("lang", currentLang);
return `${url.pathname}${url.search}${url.hash}`;
}
function navigateTo(target) {
window.location.href = normalizeRoute(target);
}
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 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 = t(node.dataset.i18n);
});
}
function renderIcons() {
$("#backIcon").src = icons.back;
$("#detailsArrow").src = icons.arrow;
}
function renderBalance() {
$("#balanceLoading").hidden = !state.loading;
$("#balanceValue").hidden = state.loading;
$("#balanceValue").textContent = state.balance;
}
function renderPayee() {
const hasPayee = Boolean(state.payee.id);
$("#payeeSelected").hidden = !hasPayee;
$("#payeeEmpty").hidden = hasPayee;
if (!hasPayee) return;
$("#payeeName").textContent = state.payee.name;
$("#payeeAccount").textContent = state.payee.goodID || state.payee.account || "-";
const avatar = $("#payeeAvatar");
avatar.src = state.payee.avatar || "";
avatar.alt = state.payee.name || "";
}
function renderAmounts() {
const root = $("#amountGrid");
root.innerHTML = "";
state.amounts.forEach((item) => {
const button = document.createElement("button");
button.type = "button";
button.className = `coin-item${state.selectedId === item.id ? " selected" : ""}`;
button.innerHTML = `<img src="${icons.dollar}" alt="" /><div class="coin-price">$${item.price}</div>`;
button.addEventListener("click", () => {
state.selectedId = state.selectedId === item.id ? null : item.id;
renderAmounts();
});
root.appendChild(button);
});
$("#transferButton").disabled = !state.selectedId || !state.payee.id;
}
function render() {
renderI18n();
renderIcons();
renderBalance();
renderPayee();
renderAmounts();
}
function bindEvents() {
$("[data-action='back']").addEventListener("click", closePage);
$(".lang-button").addEventListener("click", () => navigateTo("/language"));
$$("[data-route]").forEach((node) => {
node.addEventListener("click", () => navigateTo(node.dataset.route));
});
$$("[data-action='search-payee']").forEach((node) => {
node.addEventListener("click", () => {
clearPayee();
preservePayeeOnLeave = true;
navigateTo("/search-payee?from=/transfer");
});
});
$("#transferButton").addEventListener("click", submitTransfer);
window.addEventListener("pagehide", () => {
if (!preservePayeeOnLeave) sessionStorage.removeItem(PAYEE_KEY);
});
$("#payeeAvatar").addEventListener("error", (event) => {
event.target.onerror = null;
event.target.src = DEFAULT_AVATAR;
});
}
async function init() {
getStoredPayee();
bindEvents();
render();
await connectToApp();
await fetchBalance();
}
init();

View File

@ -2,7 +2,7 @@
const API_BASE = "https://jvapi.haiyihy.com"; const API_BASE = "https://jvapi.haiyihy.com";
const BACK_ICON = const BACK_ICON =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGXSURBVHgB7dvBTcNAFIThB5xTQFwADVBBmqEnUgZQAAXAjRqgACgAj2wfECbJZXZnrPmkVRT5lF9ODn4vVRERERERETEaxnNb8cd+PA/jeZ3PY02xqG7KA+Ic6/eds5vfPxfRdelb4uxXrt0VmXqgU3Hgo8iUA52LA9SvF1yVpkviHOdDpRhIJg6oBZKKA0qB5OKASiDJOKAQSDYO9A4kHQd6BpKPA70CWcSBHoFs4kDrQFZxoGUguzjQKpBlHGgRyDYOsANZxwFmIPs4wAq0iTjACvRUG4gDjEeumDRsIg4wAn3VhjDmYt81jWOGf64vo5q3MsAaHOLDH2oa7q2xicQKhLvopTYQiTl63kQk9mzePlKL5QXrSK22O2wjtVx/sYzUej/ILlKPBSqrSL02zGwi9VzBs4jUe0dRPpLCEqd0JJUtV9lISmvAkpHU9qTlIikukktFUt20vzQSHu++F5HqGvDi3PgIgQ5FpL5p/zme+/l1za7IHP6rcSoSfdPeIRCsRcIPtM18rSUMJoeKiIiIiIjw9QNKh3OJhFQBoAAAAABJRU5ErkJggg=="; "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGXSURBVHgB7dvBTcNAFIThB5xTQFwADVBBmqEnUgZQAAXAjRqgACgAj2wfECbJZXZnrPmkVRT5lF9ODn4vVRERERERETEaxnNb8cd+PA/jeZ3PY02xqG7KA+Ic6/eds5vfPxfRdelb4uxXrt0VmXqgU3Hgo8iUA52LA9SvF1yVpkviHOdDpRhIJg6oBZKKA0qB5OKASiDJOKAQSDYO9A4kHQd6BpKPA70CWcSBHoFs4kDrQFZxoGUguzjQKpBlHGgRyDYOsANZxwFmIPs4wAq0iTjACvRUG4gDjEeumDRsIg4wAn3VhjDmYt81jWOGf64vo5q3MsAaHOLDH2oa7q2xicQKhLvopTYQiTl63kQk9mzePlKL5QXrSK22O2wjtVx/sYzUej/ILlKPBSqrSL02zGwi9VzBs4jUe0dRPpLCEqd0JJUtV9lISmvAkpHU9qTlIikukktFUt20vzQSHu++F5HqGvDi3PgIgQ5FpL5p/zme+/l1za7IHP6rcSoSfdPeIRCsRcIPtM18rSUMJoeKiIiIiIjw9QNKh3OJhFQBoAAAAABJRU5ErkJggg==";
const DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k-1776148661448.png"; const DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k.png";
const TEXT = { const TEXT = {
pageTitle: "Invite User To Become Agent", pageTitle: "Invite User To Become Agent",

View File

@ -2,7 +2,7 @@
const API_BASE = "https://jvapi.haiyihy.com"; const API_BASE = "https://jvapi.haiyihy.com";
const BACK_ICON = const BACK_ICON =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGXSURBVHgB7dvBTcNAFIThB5xTQFwADVBBmqEnUgZQAAXAjRqgACgAj2wfECbJZXZnrPmkVRT5lF9ODn4vVRERERERETEaxnNb8cd+PA/jeZ3PY02xqG7KA+Ic6/eds5vfPxfRdelb4uxXrt0VmXqgU3Hgo8iUA52LA9SvF1yVpkviHOdDpRhIJg6oBZKKA0qB5OKASiDJOKAQSDYO9A4kHQd6BpKPA70CWcSBHoFs4kDrQFZxoGUguzjQKpBlHGgRyDYOsANZxwFmIPs4wAq0iTjACvRUG4gDjEeumDRsIg4wAn3VhjDmYt81jWOGf64vo5q3MsAaHOLDH2oa7q2xicQKhLvopTYQiTl63kQk9mzePlKL5QXrSK22O2wjtVx/sYzUej/ILlKPBSqrSL02zGwi9VzBs4jUe0dRPpLCEqd0JJUtV9lISmvAkpHU9qTlIikukktFUt20vzQSHu++F5HqGvDi3PgIgQ5FpL5p/zme+/l1za7IHP6rcSoSfdPeIRCsRcIPtM18rSUMJoeKiIiIiIjw9QNKh3OJhFQBoAAAAABJRU5ErkJggg=="; "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGXSURBVHgB7dvBTcNAFIThB5xTQFwADVBBmqEnUgZQAAXAjRqgACgAj2wfECbJZXZnrPmkVRT5lF9ODn4vVRERERERETEaxnNb8cd+PA/jeZ3PY02xqG7KA+Ic6/eds5vfPxfRdelb4uxXrt0VmXqgU3Hgo8iUA52LA9SvF1yVpkviHOdDpRhIJg6oBZKKA0qB5OKASiDJOKAQSDYO9A4kHQd6BpKPA70CWcSBHoFs4kDrQFZxoGUguzjQKpBlHGgRyDYOsANZxwFmIPs4wAq0iTjACvRUG4gDjEeumDRsIg4wAn3VhjDmYt81jWOGf64vo5q3MsAaHOLDH2oa7q2xicQKhLvopTYQiTl63kQk9mzePlKL5QXrSK22O2wjtVx/sYzUej/ILlKPBSqrSL02zGwi9VzBs4jUe0dRPpLCEqd0JJUtV9lISmvAkpHU9qTlIikukktFUt20vzQSHu++F5HqGvDi3PgIgQ5FpL5p/zme+/l1za7IHP6rcSoSfdPeIRCsRcIPtM18rSUMJoeKiIiIiIjw9QNKh3OJhFQBoAAAAABJRU5ErkJggg==";
const DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k-1776148661448.png"; const DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k.png";
const ENDPOINTS = { const ENDPOINTS = {
invitedList: "/team/bd/invite-bdleader-message-own", invitedList: "/team/bd/invite-bdleader-message-own",
invite: "/team/bd/invite-bdleader", invite: "/team/bd/invite-bdleader",

View File

@ -2,7 +2,7 @@
const API_BASE = "https://jvapi.haiyihy.com"; const API_BASE = "https://jvapi.haiyihy.com";
const BACK_ICON = const BACK_ICON =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGXSURBVHgB7dvBTcNAFIThB5xTQFwADVBBmqEnUgZQAAXAjRqgACgAj2wfECbJZXZnrPmkVRT5lF9ODn4vVRERERERETEaxnNb8cd+PA/jeZ3PY02xqG7KA+Ic6/eds5vfPxfRdelb4uxXrt0VmXqgU3Hgo8iUA52LA9SvF1yVpkviHOdDpRhIJg6oBZKKA0qB5OKASiDJOKAQSDYO9A4kHQd6BpKPA70CWcSBHoFs4kDrQFZxoGUguzjQKpBlHGgRyDYOsANZxwFmIPs4wAq0iTjACvRUG4gDjEeumDRsIg4wAn3VhjDmYt81jWOGf64vo5q3MsAaHOLDH2oa7q2xicQKhLvopTYQiTl63kQk9mzePlKL5QXrSK22O2wjtVx/sYzUej/ILlKPBSqrSL02zGwi9VzBs4jUe0dRPpLCEqd0JJUtV9lISmvAkpHU9qTlIikukktFUt20vzQSHu++F5HqGvDi3PgIgQ5FpL5p/zme+/l1za7IHP6rcSoSfdPeIRCsRcIPtM18rSUMJoeKiIiIiIjw9QNKh3OJhFQBoAAAAABJRU5ErkJggg=="; "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGXSURBVHgB7dvBTcNAFIThB5xTQFwADVBBmqEnUgZQAAXAjRqgACgAj2wfECbJZXZnrPmkVRT5lF9ODn4vVRERERERETEaxnNb8cd+PA/jeZ3PY02xqG7KA+Ic6/eds5vfPxfRdelb4uxXrt0VmXqgU3Hgo8iUA52LA9SvF1yVpkviHOdDpRhIJg6oBZKKA0qB5OKASiDJOKAQSDYO9A4kHQd6BpKPA70CWcSBHoFs4kDrQFZxoGUguzjQKpBlHGgRyDYOsANZxwFmIPs4wAq0iTjACvRUG4gDjEeumDRsIg4wAn3VhjDmYt81jWOGf64vo5q3MsAaHOLDH2oa7q2xicQKhLvopTYQiTl63kQk9mzePlKL5QXrSK22O2wjtVx/sYzUej/ILlKPBSqrSL02zGwi9VzBs4jUe0dRPpLCEqd0JJUtV9lISmvAkpHU9qTlIikukktFUt20vzQSHu++F5HqGvDi3PgIgQ5FpL5p/zme+/l1za7IHP6rcSoSfdPeIRCsRcIPtM18rSUMJoeKiIiIiIjw9QNKh3OJhFQBoAAAAABJRU5ErkJggg==";
const DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k-1776148661448.png"; const DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k.png";
const ENDPOINTS = { const ENDPOINTS = {
invitedList: "/team/bd/invite-bd-message-own", invitedList: "/team/bd/invite-bd-message-own",
invite: "/team/bd/invite-bd", invite: "/team/bd/invite-bd",

View File

@ -2,7 +2,7 @@
const API_BASE = "https://jvapi.haiyihy.com"; const API_BASE = "https://jvapi.haiyihy.com";
const BACK_ICON = const BACK_ICON =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGXSURBVHgB7dvBTcNAFIThB5xTQFwADVBBmqEnUgZQAAXAjRqgACgAj2wfECbJZXZnrPmkVRT5lF9ODn4vVRERERERETEaxnNb8cd+PA/jeZ3PY02xqG7KA+Ic6/eds5vfPxfRdelb4uxXrt0VmXqgU3Hgo8iUA52LA9SvF1yVpkviHOdDpRhIJg6oBZKKA0qB5OKASiDJOKAQSDYO9A4kHQd6BpKPA70CWcSBHoFs4kDrQFZxoGUguzjQKpBlHGgRyDYOsANZxwFmIPs4wAq0iTjACvRUG4gDjEeumDRsIg4wAn3VhjDmYt81jWOGf64vo5q3MsAaHOLDH2oa7q2xicQKhLvopTYQiTl63kQk9mzePlKL5QXrSK22O2wjtVx/sYzUej/ILlKPBSqrSL02zGwi9VzBs4jUe0dRPpLCEqd0JJUtV9lISmvAkpHU9qTlIikukktFUt20vzQSHu++F5HqGvDi3PgIgQ5FpL5p/zme+/l1za7IHP6rcSoSfdPeIRCsRcIPtM18rSUMJoeKiIiIiIjw9QNKh3OJhFQBoAAAAABJRU5ErkJggg=="; "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGXSURBVHgB7dvBTcNAFIThB5xTQFwADVBBmqEnUgZQAAXAjRqgACgAj2wfECbJZXZnrPmkVRT5lF9ODn4vVRERERERETEaxnNb8cd+PA/jeZ3PY02xqG7KA+Ic6/eds5vfPxfRdelb4uxXrt0VmXqgU3Hgo8iUA52LA9SvF1yVpkviHOdDpRhIJg6oBZKKA0qB5OKASiDJOKAQSDYO9A4kHQd6BpKPA70CWcSBHoFs4kDrQFZxoGUguzjQKpBlHGgRyDYOsANZxwFmIPs4wAq0iTjACvRUG4gDjEeumDRsIg4wAn3VhjDmYt81jWOGf64vo5q3MsAaHOLDH2oa7q2xicQKhLvopTYQiTl63kQk9mzePlKL5QXrSK22O2wjtVx/sYzUej/ILlKPBSqrSL02zGwi9VzBs4jUe0dRPpLCEqd0JJUtV9lISmvAkpHU9qTlIikukktFUt20vzQSHu++F5HqGvDi3PgIgQ5FpL5p/zme+/l1za7IHP6rcSoSfdPeIRCsRcIPtM18rSUMJoeKiIiIiIjw9QNKh3OJhFQBoAAAAABJRU5ErkJggg==";
const DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k-1776148661448.png"; const DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k.png";
const ENDPOINTS = { const ENDPOINTS = {
invitedList: "/team/bd/invite-recharge-agent-message-own", invitedList: "/team/bd/invite-recharge-agent-message-own",
invite: "/team/bd/invite-recharge-agent", invite: "/team/bd/invite-recharge-agent",