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>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>KYC</title>
<link rel="stylesheet" href="../logic-pages/app.css">
<link rel="stylesheet" href="../bd-static/shared.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"KYC","mode":"bankCards"};</script>
<script src="../logic-pages/app.js"></script>
<main class="page">
<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>
</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>
<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 DEFAULT_WEEK_STAR_API_BASE = "https://jvapi.haiyihy.com/";
const TOP_RANK_ICONS = {

View File

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

View File

@ -1,13 +1,24 @@
<!doctype html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Agency List</title>
<link rel="stylesheet" href="../logic-pages/app.css">
<link rel="stylesheet" href="../bd-static/shared.css">
</head>
<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>
<script src="../logic-pages/app.js"></script>
<main class="page">
<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>
</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>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Available Income</title>
<link rel="stylesheet" href="../logic-pages/app.css">
<title>Available Salaries</title>
<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>
<body>
<script>window.PAGE_CONFIG = {"title":"Available Income","mode":"walletAction"};</script>
<script src="../logic-pages/app.js"></script>
<main class="page">
<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>
</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>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Bank Card</title>
<link rel="stylesheet" href="../logic-pages/app.css">
<link rel="stylesheet" href="../bd-static/shared.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Bank Card","mode":"bankCards"};</script>
<script src="../logic-pages/app.js"></script>
<main class="page">
<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>
</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>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Bank Cards</title>
<link rel="stylesheet" href="../logic-pages/app.css">
<link rel="stylesheet" href="../bd-static/shared.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Bank Cards","mode":"bankCards"};</script>
<script src="../logic-pages/app.js"></script>
<main class="page">
<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>
</html>

View File

@ -1,13 +1,25 @@
<!doctype html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>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>
<body>
<script>window.PAGE_CONFIG = {"title":"BD Item Distribution","mode":"itemDistribution","admin":false};</script>
<script src="../logic-pages/app.js"></script>
<main class="page">
<div class="tabs" id="tabs"></div>
<div class="goods" id="goods"></div>
</main>
<script type="module" src="./item-distribution.js"></script>
</body>
</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>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>BD List</title>
<link rel="stylesheet" href="../logic-pages/app.css">
<link rel="stylesheet" href="../bd-static/shared.css">
</head>
<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>
<script src="../logic-pages/app.js"></script>
<main class="page">
<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>
</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>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Exchange Coins</title>
<link rel="stylesheet" href="../logic-pages/app.css">
<link rel="stylesheet" href="./exchange-gold-coins.css" />
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Exchange Coins","mode":"walletAction","action":"Exchange"};</script>
<script src="../logic-pages/app.js"></script>
<div class="exchange-gold-coins">
<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>
</html>

View File

@ -1,5 +1,5 @@
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 = {
policy: "./icons/policy.svg",
exchange: "./icons/exchange.svg",
@ -214,16 +214,6 @@ function connectToApp() {
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, environment: "browser" });
return;
}
@ -632,16 +622,16 @@ async function init() {
await connectToApp();
await fetchUserProfile();
await Promise.all([
runTask(checkIdentityAndRedirect, "identity check"),
runTask(fetchTeamEntry, "team entry"),
runTask(fetchAgencyProfile, "agency profile"),
runTask(fetchBankBalance, "bank balance"),
runTask(fetchWorkStatistics, "work statistics"),
runTask(fetchBankCards, "bank cards"),
runTask(fetchWithdrawInfo, "withdraw info"),
runTask(fetchWithdrawEnabled, "withdraw enabled"),
runTask(fetchMemberWork, "member work")
runTask(fetchWithdrawEnabled, "withdraw enabled")
]);
await checkIdentityAndRedirect();
runTask(fetchMemberWork, "member work");
} catch (error) {
console.error("Host center data request failed:", error);
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>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Host Setting</title>
<link rel="stylesheet" href="../logic-pages/app.css">
<link rel="stylesheet" href="./host-setting.css" />
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Host Setting","mode":"bankCards"};</script>
<script src="../logic-pages/app.js"></script>
<div class="host-setting">
<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>
</html>

View File

@ -1,110 +1,28 @@
<!doctype html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
/>
<title>Invite User To Become Agent</title>
<link rel="stylesheet" href="./yumi-invite-agency.css" />
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Invite Agent</title>
<link rel="stylesheet" href="../bd-static/shared.css">
</head>
<body>
<div class="fullPage" id="pageRoot">
<div class="status-bar-placeholder" id="statusBarPlaceholder"></div>
<div class="header-container">
<div class="side-area">
<button class="back-btn" id="backButton" type="button">
<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 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>
<main class="page">
<section class="content">
<div class="row">
<div class="input-box" style="flex:1">
<div style="display:flex;align-items:center;gap:8px">
<img src="../bd-static/icons/search.svg" alt="" style="width:22px;height:22px;opacity:.8">
<input class="field" id="searchInput" type="text" data-placeholder="enter_user_id" autocomplete="off">
<button type="button" id="clearButton" hidden style="color:#9ca3af;font-size:20px;line-height:1">x</button>
</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 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-agency.js"></script>
<div id="list"></div>
<div class="subtle" id="empty" data-i18n="no_users_found">No users found</div>
</section>
</main>
<script type="module" src="./invite.js"></script>
</body>
</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 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 DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k.png";
const ENDPOINTS = {
invitedList: "/team/bd/invite-bdleader-message-own",
invite: "/team/bd/invite-bdleader",

View File

@ -1,109 +1,24 @@
<!doctype html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
/>
<title>Invite User To Become BD</title>
<link rel="stylesheet" href="./yumi-invite-bd.css" />
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Invite BD</title>
<link rel="stylesheet" href="../bd-static/shared.css">
</head>
<body>
<div class="fullPage" id="pageRoot">
<div class="status-bar-placeholder" id="statusBarPlaceholder"></div>
<div class="header-container">
<div class="side-area">
<button class="back-btn" id="backButton" type="button">
<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>
<main class="page">
<section class="content">
<div class="row">
<div class="input-box" style="flex:1">
<input class="field" id="searchInput" type="text" autocomplete="off">
</div>
<button class="primary-btn" id="inviteButton" type="button" disabled data-i18n="invite">Invite</button>
</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"
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>
<div id="list"></div>
<div class="subtle" id="empty" data-i18n="no_users_found">No users found</div>
</section>
</main>
<script type="module" src="./invite.js"></script>
</body>
</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 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 DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k.png";
const ENDPOINTS = {
invitedList: "/team/bd/invite-bd-message-own",
invite: "/team/bd/invite-bd",

View File

@ -2,7 +2,7 @@
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 DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k.png";
const ENDPOINTS = {
invitedList: "/team/bd/invite-recharge-agent-message-own",
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) {
try {
await post("/team/user/apply/record/update", { id, status });
await post("/team/process/user/apply", { id, status });
node.remove();
toast(status === "AGREE" ? "Approved" : "Rejected");
} catch (error) {

View File

@ -1,13 +1,25 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Platform Policy</title>
<link rel="stylesheet" href="../logic-pages/app.css">
<link rel="stylesheet" href="./platform-policy.css" />
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Platform Policy","mode":"platformPolicy"};</script>
<script src="../logic-pages/app.js"></script>
<div class="platform-policy">
<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>
</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>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>BD Policy</title>
<link rel="stylesheet" href="../logic-pages/app.css">
<link rel="stylesheet" href="../bd-static/shared.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"BD Policy","mode":"policy"};</script>
<script src="../logic-pages/app.js"></script>
<main class="page">
<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>
</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>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Search Payee</title>
<link rel="stylesheet" href="../logic-pages/app.css">
<link rel="stylesheet" href="./search-payee.css" />
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Search Payee","mode":"teamMembers"};</script>
<script src="../logic-pages/app.js"></script>
<div class="search-payee">
<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>
</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>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Transfer</title>
<link rel="stylesheet" href="../logic-pages/app.css">
<link rel="stylesheet" href="./transfer.css" />
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Transfer","mode":"walletAction","action":"Transfer"};</script>
<script src="../logic-pages/app.js"></script>
<div class="transfer">
<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>
</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 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 DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k.png";
const TEXT = {
pageTitle: "Invite User To Become Agent",

View File

@ -2,7 +2,7 @@
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 DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k.png";
const ENDPOINTS = {
invitedList: "/team/bd/invite-bdleader-message-own",
invite: "/team/bd/invite-bdleader",

View File

@ -2,7 +2,7 @@
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 DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k.png";
const ENDPOINTS = {
invitedList: "/team/bd/invite-bd-message-own",
invite: "/team/bd/invite-bd",

View File

@ -2,7 +2,7 @@
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 DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k.png";
const ENDPOINTS = {
invitedList: "/team/bd/invite-recharge-agent-message-own",
invite: "/team/bd/invite-recharge-agent",