chore: update h5 center static pages

This commit is contained in:
170-carry 2026-04-24 20:42:36 +08:00
parent a9ab799fda
commit 6cdd3bdfbc
73 changed files with 10904 additions and 121 deletions

13
h5/KYC/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>KYC</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"KYC","mode":"bankCards"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

View File

@ -1,35 +1,67 @@
<!doctype html>
<html lang="zh-CN">
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
>
<meta name="format-detection" content="telephone=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Yumi H5</title>
<style>
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-webkit-overflow-scrolling: touch;
}
</style>
<script>
window.__H5_ROUTE_PATH__ = "/admin-center";
</script>
<script src="/assets/yumi-route-entry.js"></script>
<script type="module" crossorigin src="/js/index-CIAVq8iD-1776148658686.js"></script>
<link rel="stylesheet" crossorigin href="/css/index-VRlNQ74k-1776148661679.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>Admin Center</title>
<link rel="stylesheet" href="../center-static/center-static.css" />
</head>
<body>
<div id="app"></div>
<div class="center-page">
<header class="page-header">
<button class="icon-button" type="button" data-action="back" aria-label="Back"><span></span></button>
<h1 id="pageTitle">Admin Center</h1>
<button class="language-button" type="button" data-action="language">EN</button>
</header>
<main class="content">
<section class="surface-card hero-card">
<div class="hero-row">
<div class="hero-title" id="heroTitle">My available salary</div>
<button class="hero-link" id="heroLink" type="button">Details</button>
</div>
<div class="hero-value" id="heroValue">$0</div>
</section>
<section class="summary-grid" id="summaryGrid"></section>
<div id="sections"></div>
</main>
<div class="toast" id="toast" hidden></div>
</div>
<script>
window.CENTER_PAGE = {
title: "Admin Center",
identity: "ADMIN",
heroTitle: "My available salary",
heroLink: "Available income",
heroRoute: "/available-income",
balanceType: "ADMIN_SALARY",
summaries: [
{ label: "Role", value: "Admin" },
{ label: "Scope", value: "BD / Agency" }
],
sections: [
{
title: "Operations",
items: [
{ label: "Item distribution", route: "/item-distribution", icon: "I" },
{ label: "Admin policy", route: "/admin-policy", icon: "P" }
]
},
{
title: "Teams",
items: [
{ label: "Invite BD Leader", route: "/invite-bd-leader", icon: "+" },
{ label: "My BD Leader teams", route: "/my-BDLeader-teams", icon: "L" },
{ label: "Invite BD", route: "/invite-bd", icon: "+" },
{ label: "My BD teams", route: "/my-BD-teams", icon: "B" },
{ label: "Invite Agency", route: "/invite-agency", icon: "+" },
{ label: "My Agency teams", route: "/my-agency-teams", icon: "A" },
{ label: "Invite Recharge Agency", route: "/invite-recharge-agency", icon: "+" },
{ label: "My Recharge Agency", route: "/my-recharge-agency", icon: "R" }
]
}
]
};
</script>
<script src="../center-static/center-static.js"></script>
</body>
</html>

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Admin Policy</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Admin Policy","mode":"policy"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

View File

@ -0,0 +1,414 @@
@font-face {
font-family: "Baloo 2";
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Baloo 2";
font-weight: 600;
font-style: normal;
font-display: swap;
}
* {
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%;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-webkit-overflow-scrolling: touch;
-webkit-tap-highlight-color: transparent;
overflow-x: hidden;
}
body {
color: #21342a;
background: #edf8f1;
}
button {
border: 0;
background: transparent;
color: inherit;
font: inherit;
}
.agency-center {
min-height: 100vh;
overflow-y: auto;
background-color: #edf8f1;
background-image: none;
}
.agency-center::-webkit-scrollbar {
display: none;
}
.page-header {
position: sticky;
top: 0;
z-index: 10;
display: grid;
grid-template-columns: 44px 1fr auto;
align-items: center;
width: 100%;
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;
font-size: 1.35em;
font-weight: 600;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #153d28;
}
.header-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 6px;
}
.icon-button,
.message-button,
.language-button {
min-width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
color: #1f6f46;
}
.icon-button span {
display: block;
font-size: 2em;
line-height: 1;
}
.message-button {
width: 36px;
border-radius: 50%;
background: #fff3c8;
color: #7d5b09;
}
.message-button span {
width: 16px;
height: 16px;
display: block;
border-radius: 50%;
background: #f3c74e;
color: #5d4200;
font-size: 0.8em;
line-height: 16px;
text-align: center;
}
.language-button {
min-width: 42px;
padding: 0 10px;
border-radius: 999px;
background: #dff5e7;
color: #24724a;
font-size: 0.9em;
}
.content {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
gap: 14px;
padding: 16px;
}
.surface-card {
position: relative;
border: 1px solid #b7e0c6;
border-radius: 18px;
background: #ffffff;
box-shadow: 0 8px 20px rgba(69, 129, 89, 0.12);
}
.salary-card {
padding: 18px;
background: #f8fff9;
border-color: #94d9ae;
overflow: hidden;
}
.salary-card::after {
content: "";
position: absolute;
right: 18px;
bottom: 18px;
width: 64px;
height: 64px;
border-radius: 20px;
background: #d7f5e2;
border: 10px solid #ecfbf1;
opacity: 0.9;
}
.salary-title {
position: relative;
z-index: 1;
display: flex;
justify-content: space-between;
gap: 8px;
margin-bottom: 5px;
}
.salary-title span {
min-width: 0;
font-weight: 600;
color: #244735;
}
.salary-title button {
flex-shrink: 0;
padding: 4px 10px;
border-radius: 999px;
background: #fff3c8;
color: #846216;
font-weight: 600;
}
.salary-amount {
position: relative;
z-index: 1;
font-size: 1.5em;
font-weight: 600;
color: #105c36;
}
.task-card {
margin: 0 1%;
padding: 16px;
background: #fffaf0;
border-color: #e7c777;
}
.task-title {
margin-bottom: 12px;
font-weight: 700;
color: #3f3826;
}
.task-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
text-align: center;
}
.task-cell {
min-width: 0;
padding: 8px;
border-radius: 12px;
background: #fffdf7;
border: 1px solid #f0dfaa;
}
.task-label {
margin-bottom: 5px;
font-size: 0.86em;
font-weight: 700;
color: #4b4536;
}
.task-value {
font-size: 0.95em;
font-weight: 800;
color: #1f3528;
overflow-wrap: anywhere;
}
.menu-card {
margin: 0 1% 12px;
padding: 10px;
background: #fbfffc;
}
.menu-row {
width: 100%;
min-height: 4.5em;
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 14px;
text-align: left;
}
.menu-row:nth-of-type(4n + 1) {
background: #eefbf3;
}
.menu-row:nth-of-type(4n + 3) {
background: #f7fbec;
}
.menu-row:nth-of-type(4n + 5) {
background: #eef7fb;
}
.menu-row:nth-of-type(4n + 7) {
background: #fff5ed;
}
.menu-label {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 12px;
}
.menu-label img,
.menu-symbol {
display: flex;
width: 2.7em;
padding: 0.55em;
aspect-ratio: 1 / 1;
flex-shrink: 0;
align-items: center;
justify-content: center;
border-radius: 14px;
background: #d7f5e2;
border: 1px solid #bce5ca;
color: #1e6b43;
font-weight: 800;
}
.menu-row:nth-of-type(4n + 5) .menu-label img,
.menu-row:nth-of-type(4n + 5) .menu-symbol {
background: #dff2fb;
border-color: #c0e1f1;
}
.menu-row:nth-of-type(4n + 7) .menu-label img,
.menu-row:nth-of-type(4n + 7) .menu-symbol {
background: #ffe6d6;
border-color: #ffd2b7;
}
.menu-label span:last-child {
min-width: 0;
font-size: 1em;
font-weight: 600;
color: #244735;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.menu-row > img {
display: block;
width: 1.35em;
flex-shrink: 0;
opacity: 0.5;
}
.divider {
height: 1px;
margin: 0 12px;
background: #dbece0;
}
.modal-mask {
position: fixed;
inset: 0;
z-index: 30;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
background: rgba(0, 0, 0, 0.5);
}
.modal-dialog {
width: min(80vw, 320px);
padding: 14px;
border-radius: 12px;
background: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.18);
}
.modal-title {
color: rgba(0, 0, 0, 0.8);
font-weight: 700;
font-size: 1.2em;
}
.modal-message {
color: rgba(0, 0, 0, 0.8);
font-weight: 600;
text-align: center;
}
.modal-hint {
color: rgba(0, 0, 0, 0.45);
font-weight: 600;
font-size: 0.85em;
text-align: center;
}
.modal-action {
width: 50%;
min-width: 120px;
padding: 8px 12px;
border-radius: 999px;
background: #f3c74e;
color: #3f2b00;
font-weight: 800;
}
.toast {
position: fixed;
left: 50%;
bottom: calc(22px + env(safe-area-inset-bottom));
z-index: 40;
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"] .flip-img {
transform: rotate(180deg);
}

View File

@ -0,0 +1,703 @@
import { _ as policyIcon, a as transferIcon, b as bankIcon } from "../js/database-export-CGwyHyM--1776148658686.js";
import { _ as coinsIcon, a as cashIcon } from "../js/database-dollar-DDq4bT-e-1776148658686.js";
import { _ as arrowIcon } from "../js/arrow-Cf0JWzXk-1776148658686.js";
import { _ as helpIcon } from "../js/help-BgsLqEJs-1776148658686.js";
const API_BASE_URL = "https://jvapi.haiyihy.com";
const labels = {
en: {
agency_center: "Agency Center",
my_available_salary: "My available salary",
my_salary_details: "Salary details",
today_host_task: "My task as a host today is:",
host_type: "Host type",
minutes: "Minutes",
gift_task: "Gift Task",
platform_policy: "Platform policy",
exchange_coins: "Exchange coins",
transfer: "Transfer",
cash_withdraw: "Cash withdraw",
bank_accounts: "Bank Accounts",
team_member: "Team Member",
team_bill: "Team Bill",
invite_members: "Invite Members",
prompt: "Prompt",
fill_info_tips: "Please complete your withdrawal information first.",
card_issue_tips: "Make sure the bank card and identity information are valid.",
to_improve: "To improve",
completed: "Completed",
in_progress: "In progress",
out_of_account: "Out of account",
failed_to_load_data: "Failed to load data. Please try again."
},
ar: {},
tr: {},
bn: {}
};
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 state = {
lang: resolveLanguage(),
userProfile: null,
teamInfo: null,
identityInfo: null,
salaryInfo: {
currentSalary: "0.00"
},
taskInfo: {
anchorType: "Chat",
minutesProgress: 0,
minutesTotal: 120,
giftTask: 0
},
memberQuantity: 0,
memberWorkTotal: "0.00",
teamBill: {
date: "",
hostSalary: "",
agentSalary: "",
total: "",
status: ""
},
bankCards: [],
defaultBankCard: null,
withdrawInfo: null,
withdrawEnabled: false,
showFillInfoModal: false
};
const rolePriority = {
guest: 0,
anchor: 1,
agent: 2,
bd: 3,
bdLeader: 4,
freightAgent: 5,
admin: 6
};
const roleDefaultPages = {
admin: "/admin-center",
freightAgent: "/coin-seller",
agent: "/agency-center",
bdLeader: "/bd-leader-center",
bd: "/bd-center",
anchor: "/host-center",
guest: "/apply"
};
const rolePages = {
admin: [
"/admin-center",
"/invite-bd-leader",
"/invite-bd",
"/invite-recharge-agency",
"/invite-agency",
"/item-distribution",
"/my-BDLeader-teams",
"/my-BD-teams",
"/my-agency-teams",
"/my-recharge-agency",
"/admin-policy"
],
freightAgent: ["/coin-seller", "/seller-records", "/coin-seller-search"],
agent: [
"/agency-center",
"/transfer",
"/exchange-gold-coins",
"/message",
"/platform-policy",
"/agency-setting",
"/team-member",
"/information-details",
"/history-salary",
"/invite-members",
"/team-bill"
],
bdLeader: [
"/bd-leader-center",
"/invite-bd-leader",
"/invite-bd",
"/bd-center",
"/transfer",
"/exchange-gold-coins",
"/message",
"/invite-agency",
"/bd-item-distribution",
"/team-member",
"/history-salary",
"/agency-list",
"/available-income",
"/income-details",
"/policy",
"/bd-list"
],
bd: [
"/bd-center",
"/transfer",
"/exchange-gold-coins",
"/message",
"/invite-agency",
"/bd-item-distribution",
"/team-member",
"/history-salary",
"/agency-list",
"/available-income",
"/income-details",
"/policy"
],
anchor: [
"/host-center",
"/transfer",
"/exchange-gold-coins",
"/message",
"/platform-policy",
"/host-setting",
"/information-details",
"/history-salary",
"/invite-members",
"/team-bill"
],
guest: ["/apply", "/language"]
};
const publicPages = [
"/apply",
"/language",
"/not_app",
"/pay-result",
"/map",
"/top-list",
"/weekly-star",
"/ranking",
"/games-king",
"/couple",
"/invitation-to-register",
"/invitation/invite-new-user",
"/recharge-reward",
"/login-reward",
"/activities/lesser-bairam",
"/activities/lucky-dollars-season3",
"/activities/spring-festival",
"/recharge",
"/recharge-pay-way",
"/recharge-guide",
"/recharge-agency-recruit",
"/search-payee",
"/cash-withdraw",
"/cash-out",
"/cash-out-details",
"/bank-cards",
"/bank-card",
"/KYC",
"/good-ID"
];
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 t(key) {
return labels[state.lang]?.[key] || labels.en[key] || key;
}
function setLanguage() {
document.documentElement.lang = state.lang;
document.documentElement.dir = state.lang === "ar" ? "rtl" : "ltr";
document.querySelector("[data-action='language']").textContent = state.lang.toUpperCase();
document.querySelectorAll("[data-i18n]").forEach((node) => {
node.textContent = t(node.dataset.i18n);
});
}
function setIcons() {
const iconMap = {
policy: policyIcon,
transfer: transferIcon,
bank: bankIcon,
coins: coinsIcon,
cash: cashIcon,
arrow: arrowIcon,
help: helpIcon
};
document.querySelectorAll("[data-icon]").forEach((img) => {
img.src = iconMap[img.dataset.icon] || "";
});
}
function isMobileDevice() {
return /Android|iPad|iPhone|iPod/.test(navigator.userAgent);
}
function isAppEnvironment() {
return Boolean(window.app || window.webkit || window.FlutterPageControl);
}
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: data["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 requestAccessOrigin() {
return new Promise((resolve) => {
window.renderData = resolve;
window.getIosAccessOriginParam = resolve;
if (!isAppEnvironment()) {
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");
}
};
if (isMobileDevice()) {
poll();
return;
}
resolve(null);
});
}
async function connectToApp() {
const raw = await requestAccessOrigin();
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 readCachedJson(key) {
const raw = localStorage.getItem(key);
if (!raw) return null;
try {
return JSON.parse(raw);
} catch (error) {
console.warn(`Failed to parse ${key}:`, error);
return null;
}
}
function writeCachedJson(key, value) {
if (value === null || value === undefined) {
localStorage.removeItem(key);
return;
}
localStorage.setItem(key, JSON.stringify(value));
}
function getUserId() {
return state.userProfile?.id || readCachedJson("userProfile")?.id || null;
}
function getTeamId() {
return state.teamInfo?.id || readCachedJson("teamInfo")?.id || null;
}
function formatMoney(value) {
if (value === null || value === undefined || value === "") return "0";
const number = Number(value);
if (Number.isNaN(number)) return String(value);
return number.toFixed(2);
}
function formatBillStatus(status) {
return {
SETTLED: t("completed"),
UNPAID: t("in_progress"),
HANG_UP: t("out_of_account"),
PAY_OUT: t("completed")
}[status] || status || "";
}
function getIdentityRoles(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) {
return roles.reduce((primary, role) => (rolePriority[role] > rolePriority[primary] ? role : primary), "guest");
}
function getAllowedPages(roles) {
const pages = new Set([...publicPages, "/loading"]);
roles.forEach((role) => {
(rolePages[role] || []).forEach((page) => pages.add(page));
});
return pages;
}
function applyPermissionRedirect() {
const roles = getIdentityRoles(state.identityInfo);
const primaryRole = getPrimaryRole(roles);
const allowedPages = getAllowedPages(roles);
const currentPath = window.location.pathname.replace(/\/$/, "") || "/";
if (!allowedPages.has(currentPath)) {
navigateTo(roleDefaultPages[primaryRole] || "/apply");
}
}
async function fetchIdentity() {
const result = await apiGet("/app/h5/identity");
if (result.status && result.body) state.identityInfo = result.body;
}
async function fetchUserProfile() {
const result = await apiGet("/team/member/profile");
if (result.status && result.body) {
state.userProfile = result.body;
writeCachedJson("userProfile", result.body);
}
}
async function fetchTeamInfo() {
const result = await apiGet("/team/entry");
if (result.status && result.body) {
state.teamInfo = result.body.teamProfile || result.body;
writeCachedJson("teamInfo", state.teamInfo);
} else {
state.teamInfo = null;
writeCachedJson("teamInfo", null);
}
}
async function fetchBankBalance() {
const result = await apiGet("/wallet/bank/balance");
if (result.status && typeof result.body === "number") {
state.salaryInfo.currentSalary = result.body.toFixed(2);
} else {
state.salaryInfo.currentSalary = "0";
}
}
async function fetchWorkStatistics() {
const userId = getUserId();
if (!userId) return;
const result = await apiGet(`/team/member/work/statistics-now?dataType=DAILY&memberId=${encodeURIComponent(userId)}`);
if (result.status && result.body) {
const ownTime = Number(result.body.ownTime) || 0;
const otherTime = Number(result.body.otherTime) || 0;
const progress = ownTime + otherTime;
state.taskInfo.anchorType = "Chat";
state.taskInfo.minutesProgress = Math.min(progress, state.taskInfo.minutesTotal);
state.taskInfo.giftTask = result.body.acceptGiftValue || 0;
}
}
async function fetchMemberWorkTotal() {
const userId = getUserId();
if (!userId) return;
const result = await apiGet(`/team/member-work?userId=${encodeURIComponent(userId)}`);
if (!result.status || !result.body) return;
const targets = result.body.targets || [];
const isAgent = Boolean(state.identityInfo?.agent);
const isAnchor = Boolean(state.identityInfo?.anchor);
const total = targets.reduce((sum, item, index) => {
if (index === 0) return sum;
const settlement = item.target?.settlementResult || {};
if (isAgent) {
return sum + Number(settlement.ownSalary || 0) + Number(settlement.memberSalary || 0);
}
if (isAnchor) {
return sum + Number(settlement.memberSalary || 0);
}
return sum;
}, 0);
state.memberWorkTotal = total.toFixed(2);
}
async function fetchTeamMemberCount() {
const teamId = getTeamId();
if (!teamId) return;
try {
const result = await apiGet(`/team/members/count?id=${encodeURIComponent(teamId)}`);
if (result.status && result.body) {
state.memberQuantity = result.body.memberQuantity || 0;
}
} catch (error) {
console.error("Failed to fetch team member count:", error);
state.memberQuantity = 87;
}
}
async function fetchTeamBill() {
const teamId = getTeamId();
if (!teamId) return;
const result = await apiGet(`/team/bill?id=${encodeURIComponent(teamId)}`);
if (!result.status || !Array.isArray(result.body) || !result.body[0]) return;
const bill = result.body[0];
const billBelong = String(bill.billBelong || "");
state.teamBill = {
date: billBelong.length >= 6 ? `${billBelong.slice(0, 4)}.${billBelong.slice(4, 6)}` : "",
hostSalary: bill.settleResult?.memberSalary?.toFixed(2) || "",
agentSalary: bill.settleResult?.ownSalary?.toFixed(2) || "",
total: bill.settleResult?.totalSalary?.toFixed(2) || "",
status: formatBillStatus(bill.status)
};
}
async function fetchBankCards() {
const result = await apiGet("/wallet/bank-card/list");
if (result.status && result.body) {
state.bankCards = result.body;
state.defaultBankCard = state.bankCards.find((card) => card.use) || null;
}
}
async function fetchWithdrawInfo() {
const result = await apiGet("/wallet/withdraw-info/list");
if (result.status && Array.isArray(result.body) && result.body.length > 0) {
const info = result.body[0];
let previewUrls = [];
try {
previewUrls = JSON.parse(info.passportFrontUrl || "[]").map((item) => item);
} catch (error) {
console.warn("Failed to parse withdraw info preview urls:", error);
}
state.withdrawInfo = { ...info, previewUrls };
}
}
async function fetchWithdrawEnabled() {
const result = await apiGet("/wallet/bank/host-salary/withdraw/enabled");
if (result.status) state.withdrawEnabled = Boolean(result.body);
}
function render() {
document.getElementById("currentSalary").textContent = `$${state.salaryInfo.currentSalary}`;
document.getElementById("anchorType").textContent = state.taskInfo.anchorType;
document.getElementById("minutesProgress").textContent = state.taskInfo.minutesProgress;
document.getElementById("minutesTotal").textContent = state.taskInfo.minutesTotal;
document.getElementById("giftTask").textContent = state.taskInfo.giftTask;
document.querySelectorAll(".cash-withdraw-row").forEach((node) => {
node.hidden = !state.withdrawEnabled;
});
document.getElementById("fillInfoModal").hidden = !state.showFillInfoModal;
}
function showToast(message) {
const toast = document.getElementById("toast");
toast.textContent = message;
toast.hidden = false;
window.clearTimeout(showToast.timer);
showToast.timer = window.setTimeout(() => {
toast.hidden = true;
}, 2500);
}
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 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}`;
}
function openCashWithdraw() {
if (!state.withdrawInfo || !state.defaultBankCard) {
state.showFillInfoModal = true;
render();
return;
}
navigateTo("/cash-withdraw");
}
function bindEvents() {
const actions = {
back: closePage,
language: () => navigateTo("/language"),
message: () => navigateTo("/message"),
salaryDetails: () => navigateTo("/history-salary"),
platformPolicy: () => navigateTo("/platform-policy?from=agencycenter"),
exchangeCoins: () => navigateTo("/exchange-gold-coins"),
transfer: () => navigateTo("/transfer"),
cashWithdraw: openCashWithdraw,
bankAccounts: () => navigateTo("/agency-setting"),
teamMember: () => navigateTo(`/team-member?source=agency-center&members=${state.memberQuantity}`),
teamBill: () => navigateTo("/team-bill"),
inviteMembers: () => navigateTo("/invite-members"),
completeInfo: () => navigateTo("/agency-setting")
};
document.querySelectorAll("[data-action]").forEach((node) => {
const handler = actions[node.dataset.action];
if (handler) node.addEventListener("click", handler);
});
document.getElementById("fillInfoModal").addEventListener("click", (event) => {
if (event.target.id === "fillInfoModal") {
state.showFillInfoModal = false;
render();
}
});
}
async function initializeData() {
const firstWave = [
fetchUserProfile(),
fetchTeamInfo(),
fetchBankBalance(),
fetchWithdrawEnabled(),
fetchBankCards(),
fetchWithdrawInfo(),
fetchIdentity()
];
const firstResults = await Promise.allSettled(firstWave);
firstResults.forEach((result) => {
if (result.status === "rejected") console.warn("Agency center data request failed:", result.reason);
});
applyPermissionRedirect();
const secondWave = [
fetchWorkStatistics(),
fetchTeamMemberCount(),
fetchTeamBill(),
fetchMemberWorkTotal()
];
const secondResults = await Promise.allSettled(secondWave);
secondResults.forEach((result) => {
if (result.status === "rejected") console.warn("Agency center dependent request failed:", result.reason);
});
}
async function init() {
setLanguage();
setIcons();
bindEvents();
render();
localStorage.setItem("identity", JSON.stringify({ identity: "AGENCY" }));
localStorage.removeItem("payee");
try {
await connectToApp();
await initializeData();
} catch (error) {
console.warn("Agency center initialization failed:", error);
showToast(error.response?.errorMsg || error.message || t("failed_to_load_data"));
} finally {
render();
}
}
init();

View File

@ -13,23 +13,149 @@
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Yumi H5</title>
<style>
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-webkit-overflow-scrolling: touch;
}
</style>
<script>
window.__H5_ROUTE_PATH__ = "/agency-center";
</script>
<script src="/assets/yumi-route-entry.js"></script>
<script type="module" crossorigin src="/js/index-CIAVq8iD-1776148658686.js"></script>
<link rel="stylesheet" crossorigin href="/css/index-VRlNQ74k-1776148661679.css">
<title>Agency Center</title>
<link rel="stylesheet" href="./agency-center.css">
</head>
<body>
<div id="app"></div>
<main class="agency-center">
<header class="page-header">
<button class="icon-button" type="button" data-action="back" aria-label="Back">
<span aria-hidden="true"></span>
</button>
<h1 data-i18n="agency_center">Agency Center</h1>
<div class="header-actions">
<button class="message-button" type="button" data-action="message" aria-label="Message">
<span aria-hidden="true">!</span>
</button>
<button class="language-button" type="button" data-action="language" aria-label="Language">
EN
</button>
</div>
</header>
<section class="content">
<article class="surface-card salary-card">
<div class="salary-title">
<span data-i18n="my_available_salary">My available salary</span>
<button type="button" data-action="salaryDetails" data-i18n="my_salary_details">
Salary details
</button>
</div>
<div class="salary-amount" id="currentSalary">$0.00</div>
</article>
<article class="surface-card task-card">
<div class="task-title" data-i18n="today_host_task">My task as a host today is:</div>
<div class="task-grid">
<div class="task-cell">
<div class="task-label" data-i18n="host_type">Host type</div>
<div class="task-value" id="anchorType">Chat</div>
</div>
<div class="task-cell">
<div class="task-label" data-i18n="minutes">Minutes</div>
<div class="task-value"><span id="minutesProgress">0</span>/<span id="minutesTotal">120</span></div>
</div>
<div class="task-cell">
<div class="task-label" data-i18n="gift_task">Gift Task</div>
<div class="task-value" id="giftTask">0</div>
</div>
</div>
</article>
<article class="surface-card menu-card">
<button type="button" class="menu-row" data-action="platformPolicy">
<span class="menu-label">
<img data-icon="policy" alt="">
<span data-i18n="platform_policy">Platform policy</span>
</span>
<img data-icon="arrow" class="flip-img" alt="">
</button>
<div class="divider"></div>
<button type="button" class="menu-row" data-action="exchangeCoins">
<span class="menu-label">
<img data-icon="coins" alt="">
<span data-i18n="exchange_coins">Exchange coins</span>
</span>
<img data-icon="arrow" class="flip-img" alt="">
</button>
<div class="divider"></div>
<button type="button" class="menu-row" data-action="transfer">
<span class="menu-label">
<img data-icon="transfer" alt="">
<span data-i18n="transfer">Transfer</span>
</span>
<img data-icon="arrow" class="flip-img" alt="">
</button>
<div class="divider cash-withdraw-row" hidden></div>
<button type="button" class="menu-row cash-withdraw-row" data-action="cashWithdraw" hidden>
<span class="menu-label">
<img data-icon="cash" alt="">
<span data-i18n="cash_withdraw">Cash withdraw</span>
</span>
<img data-icon="arrow" class="flip-img" alt="">
</button>
<div class="divider"></div>
<button type="button" class="menu-row" data-action="bankAccounts">
<span class="menu-label">
<img data-icon="bank" alt="">
<span data-i18n="bank_accounts">Bank Accounts</span>
</span>
<img data-icon="arrow" class="flip-img" alt="">
</button>
<div class="divider"></div>
<button type="button" class="menu-row" data-action="teamMember">
<span class="menu-label">
<span class="menu-symbol">T</span>
<span data-i18n="team_member">Team Member</span>
</span>
<img data-icon="arrow" class="flip-img" alt="">
</button>
<div class="divider"></div>
<button type="button" class="menu-row" data-action="teamBill">
<span class="menu-label">
<span class="menu-symbol">B</span>
<span data-i18n="team_bill">Team Bill</span>
</span>
<img data-icon="arrow" class="flip-img" alt="">
</button>
<div class="divider"></div>
<button type="button" class="menu-row" data-action="inviteMembers">
<span class="menu-label">
<img data-icon="help" alt="">
<span data-i18n="invite_members">Invite Members</span>
</span>
<img data-icon="arrow" class="flip-img" alt="">
</button>
</article>
</section>
</main>
<div class="modal-mask" id="fillInfoModal" hidden>
<div class="modal-dialog" role="dialog" aria-modal="true" aria-labelledby="fillInfoTitle">
<div class="modal-title" id="fillInfoTitle" data-i18n="prompt">Prompt</div>
<div class="modal-message" data-i18n="fill_info_tips">Please complete your withdrawal information first.</div>
<div class="modal-hint" data-i18n="card_issue_tips">Make sure the bank card and identity information are valid.</div>
<button type="button" class="modal-action" data-action="completeInfo" data-i18n="to_improve">
To improve
</button>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
<script type="module" src="./agency-center.js"></script>
</body>
</html>

13
h5/agency-list/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Agency List</title>
<link rel="stylesheet" href="../logic-pages/app.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>
</body>
</html>

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Agency Setting</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Agency Setting","mode":"bankCards"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

338
h5/apply/apply.css Normal file
View File

@ -0,0 +1,338 @@
@font-face {
font-family: "Baloo 2";
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Baloo 2";
font-weight: 600;
font-style: normal;
font-display: swap;
}
* {
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%;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-webkit-overflow-scrolling: touch;
-webkit-tap-highlight-color: transparent;
overflow-x: hidden;
}
body {
color: #25352b;
background: #edf8f1;
}
button,
input {
font: inherit;
}
button {
border: 0;
background: transparent;
color: inherit;
}
.apply-view {
min-height: 100vh;
overflow-y: auto;
background: #edf8f1;
}
.page-header {
position: sticky;
top: 0;
z-index: 10;
display: grid;
grid-template-columns: 44px 1fr 44px;
align-items: center;
min-height: calc(54px + env(safe-area-inset-top));
padding: env(safe-area-inset-top) 12px 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: #173a28;
font-size: 1.35em;
font-weight: 800;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.help-button {
width: 36px;
height: 36px;
justify-self: end;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #fff3c8;
border: 1px solid #e9cc70;
}
.help-button img {
width: 18px;
height: 18px;
display: block;
}
.content {
display: flex;
flex-direction: column;
gap: 18px;
padding: 18px 12px;
}
.reminder-section {
padding: 14px 10px 4px;
}
.reminder-section h3 {
margin-bottom: 10px;
color: #788279;
font-size: 1.05em;
font-weight: 800;
}
.reminder-section p {
color: #2f302d;
font-size: 1em;
font-weight: 700;
}
.apply-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 12px;
align-items: stretch;
}
.surface-card {
border: 1px solid #b7e0c6;
border-radius: 18px;
background: #ffffff;
box-shadow: 0 8px 20px rgba(69, 129, 89, 0.12);
}
.input-card {
min-height: 56px;
display: flex;
align-items: center;
gap: 10px;
padding: 0 14px;
background: #fffdf7;
border-color: #f0d97d;
}
.input-card label {
flex-shrink: 0;
font-weight: 800;
color: #26352c;
}
.input-card input {
min-width: 0;
width: 100%;
border: 0;
outline: 0;
background: transparent;
color: #27342d;
font-weight: 600;
}
.input-card input::placeholder {
color: #8d9088;
}
.apply-button {
min-width: 96px;
min-height: 56px;
padding: 0 22px;
border-radius: 999px;
background: #f3c74e;
color: #3f2b00;
font-weight: 800;
box-shadow: 0 8px 18px rgba(155, 119, 23, 0.18);
}
.records-section {
padding: 16px;
border-radius: 16px;
background: #fbfffc;
border: 1px solid #cce9d7;
}
.records-section h3 {
margin-bottom: 14px;
color: #173a28;
font-weight: 800;
}
.record-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.record-card {
position: relative;
min-height: 74px;
display: grid;
grid-template-columns: auto 1fr auto;
gap: 10px;
align-items: center;
padding: 12px;
}
.record-status {
color: #d54e42;
font-size: 0.9em;
font-weight: 800;
}
.record-user {
min-width: 0;
display: flex;
align-items: center;
gap: 10px;
}
.record-avatar {
width: 44px;
height: 44px;
flex-shrink: 0;
border-radius: 50%;
overflow: hidden;
background: #d7f5e2;
display: flex;
align-items: center;
justify-content: center;
color: #1e6b43;
font-weight: 800;
}
.record-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.record-info {
min-width: 0;
}
.record-name {
color: #25352b;
font-weight: 700;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.record-id {
color: #6d756f;
font-size: 0.82em;
}
.record-cancel {
padding: 6px 12px;
border-radius: 999px;
background: #f3c74e;
color: #3f2b00;
font-weight: 800;
}
.modal-layer {
position: fixed;
inset: 0;
z-index: 30;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background: rgba(0, 0, 0, 0.5);
}
.modal-card {
width: min(100%, 360px);
padding: 16px;
border-radius: 14px;
background: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.18);
}
.modal-card h3 {
color: rgba(0, 0, 0, 0.8);
font-weight: 800;
font-size: 1.3em;
}
.modal-card p {
color: rgba(0, 0, 0, 0.8);
font-weight: 700;
font-size: 1.05em;
text-align: center;
}
.modal-primary {
width: 80%;
padding: 8px 12px;
border-radius: 999px;
background: #f3c74e;
color: #3f2b00;
font-weight: 800;
}
.toast {
position: fixed;
left: 50%;
bottom: calc(22px + env(safe-area-inset-bottom));
z-index: 40;
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;
}
@media (max-width: 420px) {
.apply-row {
grid-template-columns: 1fr;
}
.apply-button {
width: 100%;
}
}

511
h5/apply/apply.js Normal file
View File

@ -0,0 +1,511 @@
import { _ as helpIcon } from "../js/help-BgsLqEJs-1776148658686.js";
const API_BASE_URL = "https://jvapi.haiyihy.com";
const labels = {
en: {
apply_join_team: "Apply to join the team",
apply_reminder: "Reminder before application:",
apply_reminder_detail:
"Enter the ID of your agent, and you will become the host after the host apply is approved. After becoming an hoster,your income will be entrusted to your agent",
apply_id: "Apply ID:",
enter_agent_id: "Please enter the agent ID",
apply: "Apply",
application_records: "Application records",
pending: "Pending",
cancel: "Cancel",
application_help: "Application help",
application_help_detail: "Enter your agent ID and wait for approval.",
got_it: "Got it",
user_id_prefix: "ID:",
application_submitted: "Application submitted",
application_cancelled: "Application cancelled",
cancellation_failed: "Cancellation failed",
application_record_not_found: "Application record not found",
application_processed_cannot_cancel: "Application processed, cannot cancel",
network_error: "Network error",
cancellation_failed_try_again: "Cancellation failed, please try again"
},
ar: {},
tr: {},
bn: {}
};
class ApiError extends Error {
constructor(message, options = {}) {
super(message);
this.name = "ApiError";
this.type = options.type || "unknown";
this.status = options.status || null;
this.errorCode = options.errorCode || null;
this.errorCodeName = options.errorCodeName || null;
this.errorMsg = options.errorMsg || null;
this.response = options.response || null;
}
}
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 state = {
lang: resolveLanguage(),
agencyId: "",
inlineError: "",
loading: false,
records: [],
selectedTeam: null,
helpOpen: false
};
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 t(key) {
return labels[state.lang]?.[key] || labels.en[key] || key;
}
function setLanguage() {
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);
});
document.querySelectorAll("[data-i18n-placeholder]").forEach((node) => {
node.setAttribute("placeholder", t(node.dataset.i18nPlaceholder));
});
}
function setIcons() {
document.getElementById("helpIcon").src = helpIcon;
}
function isMobileDevice() {
return /Android|iPad|iPhone|iPod/.test(navigator.userAgent);
}
function isAppEnvironment() {
return Boolean(window.app || window.webkit || window.FlutterPageControl);
}
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) {
try {
const data = JSON.parse(raw);
const appIntel = splitHeaderPairs(data["Req-App-Intel"]);
const sysOrigin = splitHeaderPairs(data["Req-Sys-Origin"]);
return {
success: true,
data: {
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
}
};
} catch (error) {
console.error("Failed to parse access origin:", error);
return { success: false, error: "accessOrigin Error.", data: null };
}
}
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 requestAccessOrigin() {
return new Promise((resolve) => {
window.renderData = resolve;
window.getIosAccessOriginParam = resolve;
if (!isAppEnvironment()) {
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");
}
};
if (isMobileDevice()) {
poll();
return;
}
resolve(null);
});
}
async function connectToApp() {
const raw = await requestAccessOrigin();
if (!raw) return { success: true, environment: "browser" };
const parsed = parseAccessOrigin(raw);
if (!parsed.success) {
navigateTo(`/not_app?message=${encodeURIComponent(parsed.error)}`);
return { success: false, error: parsed.error };
}
setHeadersFromApp(parsed.data);
return { success: true, environment: "app" };
}
async function apiRequest(path, options = {}) {
const response = await fetch(`${API_BASE_URL}${path}`, {
method: options.method || "GET",
headers: {
...commonHeaders,
...(options.body ? { "Content-Type": "application/json" } : {})
},
body: options.body ? JSON.stringify(options.body) : undefined
});
let data = {};
try {
data = await response.json();
} catch {
data = {};
}
if (!response.ok) {
throw new ApiError(`HTTP Error: ${response.status} ${response.statusText}`, {
type: "http",
status: response.status,
response: data,
errorCode: data.errorCode,
errorCodeName: data.errorCodeName,
errorMsg: data.errorMsg
});
}
if (data.status === false) {
throw new ApiError(data.errorMsg || data.message || `API Error: ${data.errorCode}`, {
type: "business",
status: response.status,
response: data,
errorCode: data.errorCode,
errorCodeName: data.errorCodeName,
errorMsg: data.errorMsg
});
}
return data;
}
function apiGet(path) {
return apiRequest(path);
}
function apiPost(path, body) {
return apiRequest(path, { method: "POST", body });
}
async function fetchUserProfile() {
const result = await apiGet("/team/member/profile");
if (result.status && result.body) {
localStorage.setItem("userProfile", JSON.stringify(result.body));
}
}
async function fetchApplicationRecord() {
try {
const result = await apiGet("/team/wait-apply/profile");
state.records = result.status && result.body ? [result.body] : [];
} catch (error) {
console.error("获取申请记录失败:", error);
state.records = [];
showToast(error.errorMsg || error.message || "");
}
render();
}
async function cancelApplication(record) {
try {
const result = await apiPost("/team/user/join-apply-cancel", { id: record.messageId });
if (result.status) {
showToast(t("application_cancelled"));
state.records = [];
await fetchApplicationRecord();
} else {
showToast(result.message || t("cancellation_failed"));
}
} catch (error) {
console.error("撤销申请失败:", error);
if (error instanceof ApiError) {
if (error.type === "business") {
switch (error.errorCode) {
case 6404:
showToast(t("application_record_not_found"));
break;
case 6003:
showToast(t("application_processed_cannot_cancel"));
break;
default:
showToast(error.errorMsg || error.message || "Cancellation failed, please try again");
}
} else {
showToast(error.type === "http" ? t("network_error") : t("cancellation_failed"));
}
return;
}
showToast(t("cancellation_failed_try_again"));
}
}
async function searchTeamAccount(account) {
try {
console.debug("🔍 Searching team account:", account);
const result = await apiGet(`/team/search-account?account=${encodeURIComponent(account)}`);
if (result.status && result.body && result.body.teamProfile) {
state.selectedTeam = result.body;
console.debug("✅ Team found:", result.body.teamProfile);
return result.body.teamProfile.id;
}
state.selectedTeam = null;
showToast(result.errorMsg || "");
} catch (error) {
state.selectedTeam = null;
showToast(error.errorMsg || error.message || "");
}
return null;
}
async function submitApplication() {
if (!state.agencyId.trim()) {
state.inlineError = "Please enter agency ID";
return;
}
state.loading = true;
state.inlineError = "";
render();
try {
const teamId = await searchTeamAccount(state.agencyId.trim());
if (!teamId) {
state.inlineError = "Account not found or not an agency";
return;
}
console.debug("✅ Team ID found:", teamId);
console.debug("📝 Step 2: Submitting application...");
const result = await apiPost("/team/user/apply", { teamId, reason: "JOIN" });
if (result.status) {
showToast(t("application_submitted"));
await fetchApplicationRecord();
state.agencyId = "";
state.selectedTeam = null;
} else {
state.inlineError = result.message || "Application failed. Please try again.";
}
} catch (error) {
if (error instanceof ApiError) {
if (error.type === "business") {
showToast(`business error!${error.errorMsg || ""}`);
} else if (error.type === "http") {
showToast(error.status === 406 ? error.errorMsg : error.message || "Unknown error, please try again");
} else {
showToast("Unknown error, please try again");
}
}
} finally {
state.loading = false;
render();
}
}
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}`;
}
function avatarText(profile) {
const name = profile?.userNickname || profile?.account || "U";
return name.trim().slice(0, 1).toUpperCase() || "U";
}
function userAccount(profile) {
return profile?.ownSpecialId?.account || profile?.account || "";
}
function renderRecords() {
const recordsSection = document.getElementById("recordsSection");
const recordList = document.getElementById("recordList");
recordList.innerHTML = "";
recordsSection.hidden = state.records.length === 0;
document.querySelector("[data-action='apply']").hidden = state.records.length > 0;
state.records.forEach((record) => {
const profile = record.ownUserProfile || {};
const card = document.createElement("article");
card.className = "surface-card record-card";
card.innerHTML = `
<div class="record-status"></div>
<div class="record-user">
<div class="record-avatar"><img alt=""><span></span></div>
<div class="record-info">
<div class="record-name"></div>
<div class="record-id"></div>
</div>
</div>
<button class="record-cancel" type="button"></button>
`;
card.querySelector(".record-status").textContent = t("pending");
const img = card.querySelector(".record-avatar img");
const fallback = card.querySelector(".record-avatar span");
img.src = profile.userAvatar || "";
fallback.textContent = avatarText(profile);
img.addEventListener("error", () => {
img.hidden = true;
fallback.hidden = false;
});
if (!profile.userAvatar) {
img.hidden = true;
fallback.hidden = false;
} else {
fallback.hidden = true;
}
card.querySelector(".record-name").textContent = profile.userNickname || "";
card.querySelector(".record-id").textContent = `${t("user_id_prefix")} ${userAccount(profile)}`;
const cancelButton = card.querySelector(".record-cancel");
cancelButton.textContent = t("cancel");
cancelButton.addEventListener("click", () => cancelApplication(record));
recordList.appendChild(card);
});
}
function render() {
const input = document.getElementById("agencyInput");
if (document.activeElement !== input) input.value = state.agencyId;
document.querySelector("[data-action='apply']").textContent = state.loading ? "..." : t("apply");
document.getElementById("helpModal").hidden = !state.helpOpen;
renderRecords();
}
function showToast(message) {
if (!message) return;
const toast = document.getElementById("toast");
toast.textContent = message;
toast.hidden = false;
window.clearTimeout(showToast.timer);
showToast.timer = window.setTimeout(() => {
toast.hidden = true;
}, 2500);
}
function bindEvents() {
const input = document.getElementById("agencyInput");
input.addEventListener("input", () => {
state.agencyId = input.value;
render();
});
document.querySelector("[data-action='apply']").addEventListener("click", submitApplication);
document.querySelector("[data-action='help']").addEventListener("click", () => {
state.helpOpen = true;
render();
});
document.querySelector("[data-action='close-help']").addEventListener("click", () => {
state.helpOpen = false;
render();
});
document.getElementById("helpModal").addEventListener("click", (event) => {
if (event.target.id === "helpModal") {
state.helpOpen = false;
render();
}
});
}
async function initializePageData() {
try {
await fetchUserProfile();
} catch (error) {
console.error("❌ Data initialization failed:", error);
}
await fetchApplicationRecord();
}
async function init() {
setLanguage();
setIcons();
bindEvents();
render();
try {
const connection = await connectToApp();
if (connection.success) await initializePageData();
} catch (error) {
console.error("❌ Page initialization failed:", error);
}
}
init();

View File

@ -13,23 +13,51 @@
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Yumi H5</title>
<style>
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-webkit-overflow-scrolling: touch;
}
</style>
<script>
window.__H5_ROUTE_PATH__ = "/apply";
</script>
<script src="/assets/yumi-route-entry.js"></script>
<script type="module" crossorigin src="/js/index-CIAVq8iD-1776148658686.js"></script>
<link rel="stylesheet" crossorigin href="/css/index-VRlNQ74k-1776148661679.css">
<title>Apply</title>
<link rel="stylesheet" href="./apply.css">
</head>
<body>
<div id="app"></div>
<main class="apply-view">
<header class="page-header">
<div></div>
<h1 data-i18n="apply_join_team">Apply to join the team</h1>
<button class="help-button" type="button" data-action="help" aria-label="Help">
<img id="helpIcon" alt="">
</button>
</header>
<section class="content">
<section class="reminder-section">
<h3 data-i18n="apply_reminder">Reminder before application:</h3>
<p data-i18n="apply_reminder_detail">
Enter the ID of your agent, and you will become the host after the host apply is approved. After becoming an hoster,your income will be entrusted to your agent
</p>
</section>
<section class="apply-row">
<div class="surface-card input-card">
<label for="agencyInput" data-i18n="apply_id">Apply ID:</label>
<input id="agencyInput" type="text" data-i18n-placeholder="enter_agent_id" placeholder="Please enter the agent ID" autocomplete="off">
</div>
<button class="apply-button" type="button" data-action="apply" data-i18n="apply">Apply</button>
</section>
<section class="records-section" id="recordsSection" hidden>
<h3 data-i18n="application_records">Application records</h3>
<div class="record-list" id="recordList"></div>
</section>
</section>
</main>
<div class="modal-layer" id="helpModal" hidden>
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="helpTitle">
<h3 id="helpTitle" data-i18n="application_help">Application help</h3>
<p data-i18n="application_help_detail">Enter your agent ID and wait for approval.</p>
<button class="modal-primary" type="button" data-action="close-help" data-i18n="got_it">Got it</button>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
<script type="module" src="./apply.js"></script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 131 KiB

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Available Income</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Available Income","mode":"walletAction"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

13
h5/bank-card/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Bank Card</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Bank Card","mode":"bankCards"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

13
h5/bank-cards/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Bank Cards</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Bank Cards","mode":"bankCards"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

View File

@ -248,7 +248,7 @@ button {
.menu-label span {
min-width: 0;
font-size: 1em;
font-size: 1.6em;
font-weight: 600;
color: #244735;
overflow: hidden;

View File

@ -69,7 +69,12 @@ let commonHeaders = {
const state = {
lang: resolveLanguage(),
balance: 0,
hasGiftPermission: false
hasGiftPermission: false,
appConnected: false,
userInfo: null,
teamInfo: null,
identityInfo: null,
identity: "BD"
};
function resolveLanguage() {
@ -140,7 +145,7 @@ function isMobileDevice() {
}
function isAppEnvironment() {
return Boolean(window.app || window.webkit || window.FlutterPageControl);
return Boolean(window.app || window.webkit || window.FlutterPageControl || window.FlutterApp);
}
function splitHeaderPairs(value) {
@ -166,9 +171,9 @@ function parseAccessOrigin(raw) {
buildVersion: appIntel.build,
appVersion: appIntel.version,
appChannel: appIntel.channel,
reqImei: appIntel["Req-Imei"],
reqImei: data["Req-Imei"] || appIntel["Req-Imei"],
sysOrigin: sysOrigin.origin,
sysOriginChild: sysOrigin.child
sysOriginChild: sysOrigin.originChild || sysOrigin.child
};
}
@ -186,7 +191,7 @@ function setHeadersFromApp(headers) {
if (headers.reqImei) next["req-imei"] = headers.reqImei;
if (headers.sysOrigin) {
const origin = [`origin=${headers.sysOrigin}`];
if (headers.sysOriginChild) origin.push(`child=${headers.sysOriginChild}`);
if (headers.sysOriginChild) origin.push(`originChild=${headers.sysOriginChild}`);
next["req-sys-origin"] = origin.join(";");
}
commonHeaders = { ...commonHeaders, ...next };
@ -229,7 +234,7 @@ function requestAccessOrigin() {
}
};
if (isMobileDevice()) {
if (isMobileDevice() || isAppEnvironment()) {
poll();
} else {
resolve(null);
@ -239,8 +244,12 @@ function requestAccessOrigin() {
async function connectToApp() {
const raw = await requestAccessOrigin();
if (!raw) return;
if (!raw) {
state.appConnected = true;
return;
}
setHeadersFromApp(parseAccessOrigin(raw));
state.appConnected = true;
}
async function apiGet(path) {
@ -272,6 +281,59 @@ async function fetchGiftPermission() {
}
}
async function fetchUserProfile() {
const result = await apiGet("/team/member/profile");
if (result.status && result.body) {
state.userInfo = result.body;
}
}
async function fetchTeamInfo() {
const result = await apiGet("/team/entry");
if (result.status && result.body) {
state.teamInfo = result.body.teamProfile || result.body;
}
}
async function fetchIdentity() {
const result = await apiGet("/app/h5/identity");
if (result.status && result.body) {
state.identityInfo = result.body;
}
}
async function initializePageConfig() {
const results = await Promise.allSettled([fetchUserProfile(), fetchTeamInfo(), fetchIdentity()]);
results.forEach((result) => {
if (result.status === "rejected") {
console.warn("BD center page config request failed:", result.reason);
}
});
}
function clearPayeeStore() {
try {
sessionStorage.removeItem("payee");
localStorage.removeItem("payee");
} catch (error) {
console.debug("BD center payee cache clear skipped:", error);
}
}
function setIdentity(identity) {
state.identity = identity;
localStorage.setItem("identity", JSON.stringify({ identity }));
}
async function loadBDCenterData() {
const results = await Promise.allSettled([fetchBalance(), fetchGiftPermission()]);
results.forEach((result) => {
if (result.status === "rejected") {
console.warn("BD center data request failed:", result.reason);
}
});
}
function navigateTo(target) {
const url = new URL(target, window.location.origin);
const currentLang = new URLSearchParams(window.location.search).get("lang");
@ -310,16 +372,13 @@ async function init() {
setIcons();
bindEvents();
render();
localStorage.setItem("identity", JSON.stringify({ identity: "BD" }));
clearPayeeStore();
setIdentity("BD");
try {
await connectToApp();
const results = await Promise.allSettled([fetchBalance(), fetchGiftPermission()]);
results.forEach((result) => {
if (result.status === "rejected") {
console.warn("BD center data request failed:", result.reason);
}
});
await initializePageConfig();
await loadBDCenterData();
} catch (error) {
console.warn("BD center initialization failed:", error);
showToast(error.response?.errorMsg || error.message || t("failed_to_load_balance"));

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>BD Item Distribution</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"BD Item Distribution","mode":"itemDistribution","admin":false};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

View File

@ -0,0 +1,482 @@
@font-face {
font-family: "Baloo 2";
font-weight: 400 700;
font-style: normal;
font-display: swap;
}
* {
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%;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-webkit-overflow-scrolling: touch;
-webkit-tap-highlight-color: transparent;
overflow-x: hidden;
}
body {
color: #1f2937;
background: #f4fbf6;
}
button {
border: 0;
background: transparent;
color: inherit;
font: inherit;
}
.bd-center {
min-height: 100vh;
overflow-y: auto;
background: #f4fbf6;
}
.general-header {
position: sticky;
top: 0;
z-index: 10;
display: grid;
grid-template-columns: 44px 1fr 58px;
align-items: center;
width: 100%;
min-height: calc(48px + env(safe-area-inset-top));
padding: env(safe-area-inset-top) 10px 0;
background: #ffffff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.general-header h1 {
min-width: 0;
color: #111827;
font-size: 1.35em;
font-weight: 700;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.header-button,
.language-button {
min-width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
color: #111827;
font-weight: 700;
}
.header-button span {
display: block;
font-size: 2em;
line-height: 1;
}
.language-button {
justify-self: end;
padding: 0 10px;
border-radius: 999px;
background: #e9f7ee;
color: #276344;
font-size: 0.86em;
}
.content {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
}
.gradient-border {
position: relative;
overflow: hidden;
border-radius: 20px;
background: #ffffff;
border: 1px solid #d7eadf;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.salary-card {
margin-bottom: 12px;
}
.salary-inner {
padding: 16px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.salary-row {
display: flex;
justify-content: space-between;
gap: 10px;
margin-bottom: 5px;
}
.salary-title {
flex: 1;
min-width: 0;
font-weight: 600;
color: #1f2937;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.salary-link {
flex-shrink: 0;
max-width: 48%;
color: rgba(0, 0, 0, 0.4);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.salary-amount {
color: #1f2937;
font-size: 1.5em;
font-weight: 600;
direction: ltr;
display: flex;
align-items: center;
gap: 8px;
}
.salary-icon {
display: block;
width: 1.35em;
aspect-ratio: 1 / 1;
object-fit: contain;
flex-shrink: 0;
}
.menu-card {
margin: 0 1% 12px;
}
.menu-inner {
padding: 16px;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 4vw;
}
.menu-row {
width: 100%;
min-height: 1.8em;
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
text-align: left;
}
.menu-label {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 1vw;
}
.menu-label img {
display: block;
width: 1.6em;
aspect-ratio: 1 / 1;
object-fit: contain;
flex-shrink: 0;
}
.menu-label span {
min-width: 0;
color: #1f2937;
font-size: 1.4em;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.arrow {
display: block;
width: 1.5em;
aspect-ratio: 1 / 1;
object-fit: contain;
flex-shrink: 0;
}
.divider {
border-bottom: 1px solid #e6e6e6;
}
.mask-layer {
position: fixed;
inset: 0;
z-index: 40;
background: rgba(0, 0, 0, 0.35);
}
.mask-hit {
position: absolute;
inset: 0;
}
.sheet {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 41;
max-height: 80vh;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 10px;
padding: 20px;
border-radius: 16px 16px 0 0;
background: #ffffff;
}
.details-sheet {
max-height: 60vh;
}
.sheet-title {
display: flex;
justify-content: center;
align-items: center;
color: #333333;
font-size: 1em;
font-weight: 600;
}
.summary-box,
.details-summary {
display: grid;
gap: 4px;
margin: 0 4px;
padding: 12px;
border-radius: 8px;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.25);
font-size: 0.9em;
font-weight: 600;
}
.details-summary {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.7em;
}
.salary-list,
.member-list {
display: flex;
flex-direction: column;
gap: 8px;
overflow-y: auto;
}
.salary-item {
position: relative;
width: 100%;
border-radius: 8px;
background: #ebf1fd;
}
.salary-item.is-pending {
background: #feebef;
}
.salary-item.is-completed {
background: #e8faed;
}
.salary-item.is-out {
background: #e9e9e9;
}
.status-background {
position: absolute;
top: 0;
left: 0;
min-width: 7.5em;
max-width: 9em;
padding: 2px 8px;
border-radius: 8px 0 8px 0;
background: #3d73ff;
color: #ffffff;
font-size: 0.9em;
font-weight: 600;
text-align: center;
}
.is-pending .status-background {
background: #ff3d40;
}
.is-completed .status-background {
background: #3dff54;
}
.is-out .status-background {
background: #aeaeae;
font-size: 0.7em;
}
.salary-item-content {
display: grid;
gap: 7px;
padding: 32px 8px 30px;
font-size: 0.9em;
}
.pair-line {
display: flex;
justify-content: space-between;
gap: 8px;
}
.pair-line > div {
min-width: 0;
}
.more-bt {
position: absolute;
right: 8px;
bottom: 6px;
color: #4f46e5;
font-size: 0.9em;
font-weight: 700;
}
.member-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
padding: 10px 16px;
border-radius: 12px;
background: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.member-item img {
width: 3.5em;
aspect-ratio: 1 / 1;
border-radius: 50%;
object-fit: cover;
background: #eef2f7;
}
.member-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.member-info div:first-child {
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.member-info div:last-child {
color: rgba(0, 0, 0, 0.4);
font-size: 0.8em;
font-weight: 500;
}
.member-metrics {
width: auto;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: space-around;
font-size: 0.8em;
font-weight: 500;
}
.toast {
position: fixed;
left: 50%;
bottom: calc(28px + env(safe-area-inset-bottom));
z-index: 50;
max-width: 86vw;
transform: translateX(-50%);
padding: 10px 14px;
border-radius: 8px;
color: #ffffff;
background: rgba(0, 0, 0, 0.75);
font-size: 13px;
font-weight: 500;
text-align: center;
}
[hidden] {
display: none !important;
}
[dir="rtl"] .flip-img {
transform: scaleX(-1);
}
[dir="rtl"] .menu-row {
text-align: right;
}
[dir="rtl"] .status-background {
left: auto;
right: 0;
border-radius: 0 8px 0 8px;
}
[dir="rtl"] .more-bt {
right: auto;
left: 8px;
}
@media screen and (max-width: 360px) {
.bd-center {
font-size: 10px;
}
}
@media screen and (min-width: 360px) {
.bd-center {
font-size: 12px;
}
}
@media screen and (min-width: 768px) {
.bd-center {
font-size: 24px;
}
}
@media screen and (min-width: 1024px) {
.bd-center {
font-size: 32px;
}
}

View File

@ -0,0 +1,558 @@
const API_BASE_URL = "https://jvapi.haiyihy.com";
const labels = {
en: {
bd_leader_center: "BD Leader Center",
my_available_salary: "My available salary",
my_salary_link: "Details",
bd_policy_link: "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",
send_welcome_gift: "Send welcome gift",
bd_salary: "BD salary",
bd_leader_history: "BD Leader history",
total_income: "Total income",
previous_period_income: "Previous period income",
agency_number: "Agency number",
bd_number: "BD number",
team_salary: "Team salary",
team_top_up: "Team top up",
bd_income_salary: "BD income salary",
bd_income_top_up: "BD income top up",
user_id_prefix: "ID: ",
salary: "Salary",
recharge: "Recharge",
host: "Host",
agencies: "Agencies",
total_team_salary: "Total team salary",
more: "More",
in_progress: "In Progress",
pending: "Pending",
completed: "Completed",
out_of_account: "Out of account",
failed_to_load_balance: "Failed to load balance. Please try again.",
failed_to_load_history: "Failed to load history. Please try again.",
request_not_available: "Request failed. Please try again."
},
ar: {},
tr: {},
bn: {}
};
["ar", "tr", "bn"].forEach((lang) => {
labels[lang] = { ...labels.en };
});
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 state = {
lang: resolveLanguage(),
balanceTotal: {},
userInfo: null,
teamInfo: null,
identityInfo: null,
identity: "BD_LEADER",
modalMode: "",
historyType: "bd",
bdHistory: {},
bdLeaderHistory: {},
historyMore: {}
};
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 t(key) {
return labels[state.lang]?.[key] || labels.en[key] || key;
}
function setLanguage() {
document.documentElement.lang = state.lang;
document.documentElement.dir = state.lang === "ar" ? "rtl" : "ltr";
document.querySelector("[data-action='language']").textContent = state.lang.toUpperCase();
document.querySelectorAll("[data-i18n]").forEach((node) => {
node.textContent = t(node.dataset.i18n);
});
}
function setIcons() {
const icon = (body) => `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
${body}
</svg>
`)}`;
const iconMap = {
policy: icon(`
<path d="M13 8h15l7 7v25H13V8Z" fill="#E9F8EF" stroke="#22945D" stroke-width="3" stroke-linejoin="round"/>
<path d="M28 8v8h7" fill="#BDEBD0" stroke="#22945D" stroke-width="3" stroke-linejoin="round"/>
<path d="M18 32h4l2-9 4 14 2-5h4" stroke="#1766C2" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
`),
bank: icon(`
<path d="M8 20h32L24 10 8 20Z" fill="#FFF2C7" stroke="#C08212" stroke-width="3" stroke-linejoin="round"/>
<path d="M12 22v14M20 22v14M28 22v14M36 22v14" stroke="#2C7A4B" stroke-width="3" stroke-linecap="round"/>
<path d="M9 38h30" stroke="#C08212" stroke-width="3" stroke-linecap="round"/>
`),
gift: icon(`
<path d="M11 22h26v18H11V22Z" fill="#FFF0B8" stroke="#C47B13" stroke-width="3" stroke-linejoin="round"/>
<path d="M8 15h32v7H8v-7Z" fill="#FFE4E8" stroke="#D94B63" stroke-width="3" stroke-linejoin="round"/>
<path d="M24 15v25" stroke="#D94B63" stroke-width="3" stroke-linecap="round"/>
<path d="M24 15c-7-1-10-3-10-6 0-2 2-4 4-4 4 0 6 6 6 10ZM24 15c7-1 10-3 10-6 0-2-2-4-4-4-4 0-6 6-6 10Z" fill="#FFFFFF" stroke="#D94B63" stroke-width="3" stroke-linejoin="round"/>
`),
invite: icon(`
<circle cx="18" cy="17" r="6" fill="#E8F3FF" stroke="#2878D8" stroke-width="3"/>
<path d="M7 37c1.5-7 6-11 11-11 2.8 0 5.4 1.2 7.3 3.4" stroke="#2878D8" stroke-width="3" stroke-linecap="round"/>
<path d="M34 19v14M27 26h14" stroke="#2C9B63" stroke-width="3" stroke-linecap="round"/>
`),
arrow: icon(`
<path d="M19 12l12 12-12 12" stroke="#62A87C" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
`),
salary: icon(`
<circle cx="24" cy="24" r="17" fill="#E8F8EF" stroke="#2C9B63" stroke-width="3"/>
<path d="M24 13v22" stroke="#2C9B63" stroke-width="3" stroke-linecap="round"/>
<path d="M30 17.5c-1.4-1.4-3.4-2.2-6-2.2-3.5 0-6 1.8-6 4.7 0 6.7 12 2.8 12 9.1 0 2.7-2.5 4.6-6.2 4.6-2.8 0-5.3-.9-7-2.5" stroke="#D69E17" stroke-width="3" stroke-linecap="round"/>
`)
};
document.querySelectorAll("[data-icon]").forEach((img) => {
img.src = iconMap[img.dataset.icon] || "";
});
}
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 isMobileDevice() {
return /Android|iPad|iPhone|iPod/.test(navigator.userAgent);
}
function isAppEnvironment() {
return Boolean(window.app || window.webkit || window.FlutterPageControl || window.FlutterApp);
}
function requestAccessOrigin() {
return new Promise((resolve) => {
window.renderData = resolve;
window.getIosAccessOriginParam = resolve;
if (!isAppEnvironment()) {
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");
}
};
if (isMobileDevice() || isAppEnvironment()) {
poll();
} else {
resolve(null);
}
});
}
async function connectToApp() {
const raw = await requestAccessOrigin();
if (!raw) return { success: true, environment: "browser" };
setHeadersFromApp(parseAccessOrigin(raw));
return { success: true, environment: "app" };
}
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 result = await apiGet("/team/member/profile");
if (result.status && result.body) state.userInfo = result.body;
}
async function fetchTeamInfo() {
try {
const result = await apiGet("/team/entry");
if (result.status && result.body) state.teamInfo = result.body.teamProfile || result.body;
} catch (error) {
state.teamInfo = null;
console.debug("BD leader team info unavailable:", error);
}
}
async function fetchIdentity() {
const result = await apiGet("/app/h5/identity");
if (result.status && result.body) state.identityInfo = result.body;
}
async function fetchBalanceTotal() {
const result = await apiGet("/wallet/salary-account/balance/total");
if (result.status && result.body) state.balanceTotal = result.body || {};
}
async function fetchBDHistoryMore(billBelong) {
const result = await apiGet(`/team/bd/history/more?billBelong=${encodeURIComponent(billBelong)}`);
if (result.status && result.body) state.historyMore = result.body || {};
}
async function fetchBDLeaderHistoryMore(billBelong) {
const result = await apiGet(`/team/bd/leader/history/more?billBelong=${encodeURIComponent(billBelong)}`);
if (result.status && result.body) state.historyMore = result.body || {};
}
function setIdentity(identity) {
state.identity = identity;
localStorage.setItem("identity", JSON.stringify({ identity }));
}
function clearPayee() {
try {
sessionStorage.removeItem("payee");
localStorage.removeItem("payee");
} catch (error) {
console.debug("Payee cache clear skipped:", error);
}
}
function formatMoney(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);
}
function normalizeStatus(status) {
if (status === "In Progress" || status === t("in_progress")) return t("in_progress");
if (status === "Pending" || status === t("pending")) return t("pending");
if (status === "Completed" || status === t("completed")) return t("completed");
if (status === "Out of account" || status === t("out_of_account")) return t("out_of_account");
return status || "";
}
function statusClass(status) {
const value = normalizeStatus(status);
if (value === t("completed")) return "is-completed";
if (value === t("pending")) return "is-pending";
if (value === t("out_of_account")) return "is-out";
return "is-progress";
}
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}`;
}
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 renderBalance() {
document.getElementById("availableBalance").textContent = `$${formatMoney(state.balanceTotal.availableBalance || 0)}`;
}
function historySource() {
return state.historyType === "bd" ? state.bdHistory : state.bdLeaderHistory;
}
function historyTitle() {
return state.historyType === "bd" ? t("bd_salary") : t("bd_leader_history");
}
function renderHistorySheet() {
const data = historySource();
document.getElementById("historyTitle").textContent = historyTitle();
document.getElementById("historyTotalIncome").textContent = formatMoney(data.totalIncome || 0);
document.getElementById("historyPreviousIncome").textContent = formatMoney(data.previousPeriodIncome || 0);
const list = document.getElementById("historyList");
list.innerHTML = "";
const items = data.incomeDetails || [];
items.forEach((item) => list.appendChild(createHistoryItem(item)));
}
function createHistoryItem(item) {
const card = document.createElement("article");
card.className = `salary-item ${statusClass(item.statusText)}`;
const status = document.createElement("div");
status.className = "status-background";
status.textContent = normalizeStatus(item.statusText);
card.appendChild(status);
const content = document.createElement("div");
content.className = "salary-item-content";
if (state.historyType === "bd") {
content.append(
textLine(item.billTitle || ""),
textLine(`${t("agency_number")}: ${item.agencyNumber || 0}`),
pairLine(`${t("team_salary")}: $${item.teamSalaryAmount || 0}`, `${t("bd_income_salary")}: $${item.bdIncomeSalary || item.settlementSalary || 0} (${item.bdRatioSalary || item.commissionRate || 0}%)`),
pairLine(`${t("team_top_up")}: $${item.teamRechargeAmount || 0}`, `${t("bd_income_top_up")}: $${item.bdIncomeRecharge || item.settlementSalary || 0} (${item.bdRatioRecharge || item.commissionRate || 0}%)`)
);
} else {
content.append(
textLine(item.billTitle || ""),
pairLine(`${t("bd_number")}: ${item.bdNumber || 0}`, `${t("team_salary")}: $${item.teamSalaryAmount || 0}`)
);
}
card.appendChild(content);
if (normalizeStatus(item.statusText) === t("completed")) {
const more = document.createElement("button");
more.type = "button";
more.className = "more-bt";
more.textContent = t("more");
more.addEventListener("click", () => showHistoryMore(item.billBelong));
card.appendChild(more);
}
return card;
}
function textLine(text) {
const node = document.createElement("div");
node.textContent = text;
return node;
}
function pairLine(left, right) {
const node = document.createElement("div");
node.className = "pair-line";
const leftNode = document.createElement("div");
const rightNode = document.createElement("div");
leftNode.textContent = left;
rightNode.textContent = right;
node.append(leftNode, rightNode);
return node;
}
async function showHistoryMore(billBelong) {
try {
if (state.historyType === "bd") {
await fetchBDHistoryMore(billBelong);
} else {
await fetchBDLeaderHistoryMore(billBelong);
}
state.modalMode = "details";
renderModal();
} catch (error) {
console.warn("BD leader history more failed:", error);
showToast(error.response?.errorMsg || error.message || t("failed_to_load_history"));
}
}
function renderDetailsSheet() {
const data = state.historyMore || {};
document.getElementById("detailsPeriod").textContent = data.period || "";
const summary = document.getElementById("detailsSummary");
summary.innerHTML = "";
if (state.historyType === "bd") {
summary.append(
textLine(`${t("team_top_up")}: $${data.teamTotalRecharge || 0}`),
textLine(`${t("total_team_salary")}: $${data.teamTotalSalary || 0}`)
);
} else {
summary.append(
textLine(`${t("team_salary")}: $${data.teamSalary || 0}`),
textLine(`${t("bd_number")}: ${data.bdNumber || 0}`)
);
}
const list = document.getElementById("memberDetails");
list.innerHTML = "";
(data.memberDetails || []).forEach((item) => {
const card = document.createElement("article");
card.className = "member-item";
const img = document.createElement("img");
img.src = state.historyType === "bd" ? item.userAvatar || "" : item.bdUserAvatar || "";
img.alt = "";
img.addEventListener("error", () => {
img.removeAttribute("src");
});
const info = document.createElement("div");
info.className = "member-info";
info.append(
textLine(state.historyType === "bd" ? item.userName || "" : item.bdUserName || ""),
textLine(`${t("user_id_prefix")}${item.account || ""}`)
);
const metrics = document.createElement("div");
metrics.className = "member-metrics";
metrics.append(textLine(`${t("salary")}: $${item.salary || 0}`));
if (state.historyType === "bd") {
metrics.append(textLine(`${t("recharge")}: $${item.recharge || 0}`), textLine(`${t("host")}: ${item.hostCount || 0}`));
} else {
metrics.append(textLine(`${t("agencies")}: ${item.agencyCount || 0}`));
}
card.append(img, info, metrics);
list.appendChild(card);
});
}
function renderModal() {
const mask = document.getElementById("maskLayer");
const history = document.getElementById("historySheet");
const details = document.getElementById("detailsSheet");
const showHistory = state.modalMode === "history";
const showDetails = state.modalMode === "details";
mask.hidden = !showHistory && !showDetails;
history.hidden = !showHistory;
details.hidden = !showDetails;
if (showHistory) renderHistorySheet();
if (showDetails) renderDetailsSheet();
}
function closeModal() {
state.modalMode = "";
state.historyMore = {};
renderModal();
}
function showToast(message) {
const toast = document.getElementById("toast");
toast.textContent = message;
toast.hidden = false;
window.clearTimeout(showToast.timer);
showToast.timer = window.setTimeout(() => {
toast.hidden = true;
}, 2500);
}
function bindEvents() {
document.querySelector("[data-action='back']").addEventListener("click", closePage);
document.querySelector("[data-action='language']").addEventListener("click", () => navigateTo("/language"));
document.querySelectorAll("[data-route]").forEach((node) => {
node.addEventListener("click", () => navigateTo(node.dataset.route));
});
document.querySelector("[data-action='sendGift']").addEventListener("click", () => navigateTo("/bd-item-distribution"));
document.querySelector("[data-action='closeModal']").addEventListener("click", closeModal);
}
async function initializePageData() {
const tasks = [fetchUserProfile(), fetchTeamInfo(), fetchIdentity()];
await Promise.allSettled(tasks);
}
async function init() {
setLanguage();
setIcons();
bindEvents();
renderBalance();
clearPayee();
setIdentity("BD_LEADER");
try {
await connectToApp();
await initializePageData();
await fetchBalanceTotal();
} catch (error) {
console.warn("BD leader center initialization failed:", error);
showToast(error.response?.errorMsg || error.message || t("failed_to_load_balance"));
} finally {
renderBalance();
}
}
init();

View File

@ -13,23 +13,128 @@
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Yumi H5</title>
<style>
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-webkit-overflow-scrolling: touch;
}
</style>
<script>
window.__H5_ROUTE_PATH__ = "/bd-leader-center";
</script>
<script src="/assets/yumi-route-entry.js"></script>
<script type="module" crossorigin src="/js/index-CIAVq8iD-1776148658686.js"></script>
<link rel="stylesheet" crossorigin href="/css/index-VRlNQ74k-1776148661679.css">
<title>BD Leader Center</title>
<link rel="stylesheet" href="./bd-leader-center.css?v=bdleader-static-1">
</head>
<body>
<div id="app"></div>
<main class="bd-center">
<header class="general-header">
<button class="header-button" type="button" data-action="back" aria-label="Back">
<span aria-hidden="true"></span>
</button>
<h1 data-i18n="bd_leader_center">BD Leader Center</h1>
<button class="language-button" type="button" data-action="language" aria-label="Language">EN</button>
</header>
<section class="content">
<article class="gradient-border salary-card">
<div class="salary-inner">
<div class="salary-row">
<div class="salary-title" data-i18n="my_available_salary">My available salary</div>
<button class="salary-link" type="button" data-route="/available-income" data-i18n="my_salary_link">
Details
</button>
</div>
<div class="salary-amount">
<img data-icon="salary" class="salary-icon" alt="">
<span id="availableBalance">$0</span>
</div>
</div>
</article>
<article class="gradient-border menu-card">
<div class="menu-inner">
<button type="button" class="menu-row" data-route="/policy?from=BDLead">
<span class="menu-label">
<img data-icon="policy" alt="">
<span data-i18n="bd_policy_link">BD policy</span>
</span>
<img data-icon="arrow" class="flip-img arrow" alt="">
</button>
<div class="divider"></div>
<button type="button" class="menu-row" data-route="/agency-list">
<span class="menu-label">
<img data-icon="policy" alt="">
<span data-i18n="agency_list_link">Agency list</span>
</span>
<img data-icon="arrow" class="flip-img arrow" alt="">
</button>
<div class="divider"></div>
<button type="button" class="menu-row" data-route="/bd-list">
<span class="menu-label">
<img data-icon="policy" alt="">
<span data-i18n="bd_list_link">BD List</span>
</span>
<img data-icon="arrow" class="flip-img arrow" alt="">
</button>
<div class="divider"></div>
<button type="button" class="menu-row" data-route="/invite-agency">
<span class="menu-label">
<img data-icon="invite" alt="">
<span data-i18n="invite_become_agent">Invite to become agent</span>
</span>
<img data-icon="arrow" class="flip-img arrow" alt="">
</button>
<div class="divider"></div>
<button type="button" class="menu-row" data-route="/invite-bd">
<span class="menu-label">
<img data-icon="invite" alt="">
<span data-i18n="invite_become_BD">Invite to become BD</span>
</span>
<img data-icon="arrow" class="flip-img arrow" alt="">
</button>
<div class="divider"></div>
<button type="button" class="menu-row" data-route="/bank-cards">
<span class="menu-label">
<img data-icon="bank" alt="">
<span data-i18n="bank_cards_link">Bank cards</span>
</span>
<img data-icon="arrow" class="flip-img arrow" alt="">
</button>
<div class="divider send-row" hidden></div>
<button type="button" class="menu-row send-row" data-action="sendGift" hidden>
<span class="menu-label">
<img data-icon="gift" alt="">
<span data-i18n="send_welcome_gift">Send welcome gift</span>
</span>
<img data-icon="arrow" class="flip-img arrow" alt="">
</button>
</div>
</article>
</section>
</main>
<div class="mask-layer" id="maskLayer" hidden>
<div class="mask-hit" data-action="closeModal"></div>
<section class="sheet history-sheet" id="historySheet" hidden>
<div class="sheet-title" id="historyTitle">BD salary</div>
<div class="summary-box">
<div>
<span data-i18n="total_income">Total income</span>
<span> $</span><span id="historyTotalIncome">0</span>
</div>
<div>
<span data-i18n="previous_period_income">Previous period income</span>
<span>: $</span><span id="historyPreviousIncome">0</span>
</div>
</div>
<div class="salary-list" id="historyList"></div>
</section>
<section class="sheet details-sheet" id="detailsSheet" hidden>
<div class="sheet-title" id="detailsPeriod"></div>
<div class="details-summary" id="detailsSummary"></div>
<div class="member-list" id="memberDetails"></div>
</section>
</div>
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
<script type="module" src="./bd-leader-center.js?v=bdleader-static-3"></script>
</body>
</html>

13
h5/bd-list/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>BD List</title>
<link rel="stylesheet" href="../logic-pages/app.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>
</body>
</html>

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Cash Out Details</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Cash Out Details","mode":"policy"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

13
h5/cash-out/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Cash Out</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Cash Out","mode":"walletAction","action":"Cash out"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Cash Withdraw</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Cash Withdraw","mode":"walletAction","action":"Cash out"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

View File

@ -0,0 +1,318 @@
* {
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%;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-webkit-overflow-scrolling: touch;
-webkit-tap-highlight-color: transparent;
overflow-x: hidden;
}
body {
color: #1f3528;
background: #edf8f1;
}
button {
border: 0;
background: transparent;
color: inherit;
font: inherit;
}
.center-page {
min-height: 100vh;
overflow-y: auto;
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;
font-size: 1.35em;
font-weight: 700;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #153d28;
}
.icon-button,
.language-button {
min-width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
color: #1f6f46;
}
.icon-button span {
display: block;
font-size: 2em;
line-height: 1;
}
.language-button {
justify-self: end;
padding: 0 10px;
border-radius: 999px;
background: #dff5e7;
color: #24724a;
font-size: 0.86em;
}
.content {
display: flex;
flex-direction: column;
gap: 14px;
padding: 16px;
}
.surface-card {
border: 1px solid #b7e0c6;
border-radius: 18px;
background: #ffffff;
box-shadow: 0 8px 20px rgba(69, 129, 89, 0.12);
}
.hero-card {
position: relative;
padding: 18px;
background: #f8fff9;
border-color: #94d9ae;
overflow: hidden;
}
.hero-card::after {
content: "";
position: absolute;
right: 16px;
bottom: 16px;
width: 64px;
height: 64px;
border-radius: 20px;
background: #d7f5e2;
border: 10px solid #ecfbf1;
}
.hero-row {
position: relative;
z-index: 1;
display: flex;
justify-content: space-between;
gap: 10px;
}
.hero-title {
font-weight: 700;
color: #244735;
}
.hero-link {
flex-shrink: 0;
padding: 4px 10px;
border-radius: 999px;
background: #fff3c8;
color: #846216;
font-weight: 700;
}
.hero-value {
position: relative;
z-index: 1;
margin-top: 6px;
font-size: 1.65em;
font-weight: 800;
color: #105c36;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.summary-item {
padding: 12px;
border-radius: 14px;
border: 1px solid #cce9d7;
background: #fbfffc;
}
.summary-item:nth-child(2n) {
background: #f7fbec;
}
.summary-label {
font-size: 0.83em;
font-weight: 700;
color: #587060;
}
.summary-value {
margin-top: 4px;
font-size: 1.12em;
font-weight: 800;
color: #153d28;
}
.task-card {
padding: 14px 16px 16px;
background: #fffaf0;
border-color: #e7c777;
}
.task-title {
margin-bottom: 10px;
font-weight: 800;
color: #3f3826;
}
.task-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
text-align: center;
}
.task-label {
margin-bottom: 6px;
font-size: 0.9em;
font-weight: 800;
color: #4b4536;
}
.task-value {
font-size: 0.95em;
font-weight: 800;
color: #1f3528;
}
.menu-card {
padding: 10px;
background: #fbfffc;
}
.section-title {
padding: 4px 4px 10px;
font-size: 1.02em;
font-weight: 800;
color: #1f5134;
}
.section-title:empty {
display: none;
}
.menu-row {
width: 100%;
min-height: 4.5em;
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 14px;
text-align: left;
}
.menu-row:nth-of-type(4n + 2) {
background: #eefbf3;
}
.menu-row:nth-of-type(4n + 3) {
background: #f7fbec;
}
.menu-row:nth-of-type(4n + 4) {
background: #eef7fb;
}
.menu-row:nth-of-type(4n + 5) {
background: #fff5ed;
}
.menu-label {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 12px;
}
.menu-icon {
width: 2.65em;
height: 2.65em;
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
border-radius: 14px;
border: 1px solid #bce5ca;
background: #d7f5e2;
color: #1e6b43;
font-size: 1em;
font-weight: 800;
}
.menu-text {
min-width: 0;
font-size: 1em;
font-weight: 800;
color: #173a28;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.arrow {
flex-shrink: 0;
color: #7fa48d;
font-size: 1.35em;
font-weight: 800;
}
.toast {
position: fixed;
left: 50%;
bottom: calc(22px + env(safe-area-inset-bottom));
z-index: 20;
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;
}

View File

@ -0,0 +1,297 @@
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 fallbackConfig = {
title: "Center",
identity: "USER",
heroTitle: "Available salary",
heroLink: "Details",
heroRoute: "/history-salary",
balanceType: "",
summaries: [],
sections: []
};
const config = { ...fallbackConfig, ...(window.CENTER_PAGE || {}) };
const state = {
lang: resolveLanguage(),
heroValue: "0",
summaries: [...(config.summaries || [])]
};
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 formatMoney(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);
}
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 requestAccessOrigin() {
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");
}
};
poll();
});
}
async function connectToApp() {
const raw = await requestAccessOrigin();
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;
}
async function fetchHeroValue() {
if (config.balanceType) {
const result = await apiGet(`/wallet/salary-account/balance?salaryType=${config.balanceType}`);
if (result.status && result.body) state.heroValue = formatMoney(result.body.availableBalance || 0);
return;
}
if (config.balanceEndpoint) {
const result = await apiGet(config.balanceEndpoint);
const body = result.body || {};
state.heroValue = formatMoney(body.currentSalary ?? body.availableBalance ?? body.balance ?? 0);
}
}
function showToast(message) {
const toast = document.getElementById("toast");
toast.textContent = message;
toast.hidden = false;
window.clearTimeout(showToast.timer);
showToast.timer = window.setTimeout(() => {
toast.hidden = true;
}, 2500);
}
function normalizeRoute(target) {
const alias = {
"/invite-agency": "/invite-agency/",
"/invite-bd": "/invite-bd/",
"/invite-bd-leader": "/invite-bd-leader/",
"/invite-recharge-agency": "/invite-recharge-agency/"
};
const url = new URL(alias[target] || 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 menuIcon(text) {
return (text || "?").trim().slice(0, 1).toUpperCase();
}
function render() {
document.documentElement.lang = state.lang;
document.documentElement.dir = state.lang === "ar" ? "rtl" : "ltr";
document.getElementById("pageTitle").textContent = config.title;
document.querySelector("[data-action='language']").textContent = state.lang.toUpperCase();
document.getElementById("heroTitle").textContent = config.heroTitle;
document.getElementById("heroLink").textContent = config.heroLink || "Details";
document.getElementById("heroValue").textContent = `$${state.heroValue}`;
const summaryRoot = document.getElementById("summaryGrid");
summaryRoot.innerHTML = "";
state.summaries.forEach((item) => {
const node = document.createElement("div");
node.className = "summary-item";
node.innerHTML = `<div class="summary-label"></div><div class="summary-value"></div>`;
node.querySelector(".summary-label").textContent = item.label;
node.querySelector(".summary-value").textContent = item.value;
summaryRoot.appendChild(node);
});
summaryRoot.hidden = !state.summaries.length;
const oldTask = document.getElementById("taskCard");
if (oldTask) oldTask.remove();
if (config.task) {
const taskCard = document.createElement("section");
taskCard.className = "surface-card task-card";
taskCard.id = "taskCard";
taskCard.innerHTML = `<div class="task-title"></div><div class="task-grid"></div>`;
taskCard.querySelector(".task-title").textContent = config.task.title || "";
const taskGrid = taskCard.querySelector(".task-grid");
(config.task.items || []).forEach((item) => {
const node = document.createElement("div");
node.innerHTML = `<div class="task-label"></div><div class="task-value"></div>`;
node.querySelector(".task-label").textContent = item.label;
node.querySelector(".task-value").textContent = item.value;
taskGrid.appendChild(node);
});
const sections = document.getElementById("sections");
sections.parentNode.insertBefore(taskCard, sections);
}
const sectionRoot = document.getElementById("sections");
sectionRoot.innerHTML = "";
(config.sections || []).forEach((section) => {
const card = document.createElement("section");
card.className = "surface-card menu-card";
const title = document.createElement("div");
title.className = "section-title";
title.textContent = section.title || "";
card.appendChild(title);
(section.items || []).forEach((item) => {
const row = document.createElement("button");
row.type = "button";
row.className = "menu-row";
row.dataset.route = item.route;
row.innerHTML = `<div class="menu-label"><span class="menu-icon"></span><span class="menu-text"></span></div><span class="arrow"></span>`;
row.querySelector(".menu-icon").textContent = item.icon || menuIcon(item.label);
row.querySelector(".menu-text").textContent = item.label;
row.addEventListener("click", () => navigateTo(item.route));
card.appendChild(row);
});
sectionRoot.appendChild(card);
});
}
function bindEvents() {
document.querySelector("[data-action='back']").addEventListener("click", () => {
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");
}
});
document.querySelector("[data-action='language']").addEventListener("click", () => navigateTo("/language"));
document.getElementById("heroLink").addEventListener("click", () => navigateTo(config.heroRoute || "/history-salary"));
}
async function init() {
localStorage.setItem("identity", JSON.stringify({ identity: config.identity }));
render();
bindEvents();
try {
await connectToApp();
await fetchHeroValue();
} catch (error) {
console.warn(`${config.title} data request failed:`, error);
showToast(error.response?.errorMsg || error.message || "Failed to load data.");
} finally {
render();
}
}
init();

View File

@ -0,0 +1,177 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: "Baloo 2", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
html,
body {
width: 100%;
min-height: 100%;
background: #edf8f1;
color: #1f3528;
-webkit-text-size-adjust: 100%;
}
button,
input {
font: inherit;
}
button {
border: 0;
background: transparent;
color: inherit;
}
.page {
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.32em;
font-weight: 800;
text-align: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.back-button,
.lang-button {
min-width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
color: #1f6f46;
}
.back-button span {
font-size: 2em;
line-height: 1;
}
.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-card,
.list-card,
.empty-card {
border: 1px solid #b7e0c6;
border-radius: 18px;
background: #ffffff;
box-shadow: 0 8px 20px rgba(69, 129, 89, 0.12);
}
.search-card {
display: flex;
gap: 8px;
padding: 10px;
background: #fbfffc;
}
.search-card input {
flex: 1;
min-width: 0;
border: 1px solid #cce9d7;
border-radius: 12px;
background: #f8fff9;
padding: 10px 12px;
outline: none;
color: #1f3528;
font-weight: 700;
}
.search-card button,
.primary-button {
flex-shrink: 0;
border-radius: 12px;
background: #dff5e7;
padding: 0 14px;
color: #1f6f46;
font-weight: 800;
}
.list-card {
overflow: hidden;
}
.row {
width: 100%;
min-height: 4.2em;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 12px 14px;
border-bottom: 1px solid #e2f1e7;
}
.row:last-child {
border-bottom: 0;
}
.row:nth-child(2n) {
background: #fbfffc;
}
.row-title {
font-weight: 800;
color: #173a28;
}
.row-subtitle {
margin-top: 4px;
color: #6d7e72;
font-size: 0.86em;
font-weight: 700;
}
.tag {
flex-shrink: 0;
border-radius: 999px;
background: #fff3c8;
padding: 5px 9px;
color: #846216;
font-size: 0.82em;
font-weight: 800;
}
.empty-card {
padding: 24px 16px;
text-align: center;
color: #6d7e72;
font-weight: 700;
}

View File

@ -0,0 +1,56 @@
const config = window.STATIC_ROUTE || {};
function lang() {
const value = new URLSearchParams(window.location.search).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 goBack() {
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 renderRows(rows) {
const list = document.getElementById("list");
list.innerHTML = "";
rows.forEach((row) => {
const node = document.createElement("div");
node.className = "row";
node.innerHTML = `<div><div class="row-title"></div><div class="row-subtitle"></div></div><div class="tag"></div>`;
node.querySelector(".row-title").textContent = row.title;
node.querySelector(".row-subtitle").textContent = row.subtitle || "";
node.querySelector(".tag").textContent = row.tag || "Ready";
list.appendChild(node);
});
}
function init() {
document.title = config.title || "Yumi H5";
document.getElementById("title").textContent = config.title || "Yumi H5";
document.getElementById("lang").textContent = lang();
document.getElementById("keyword").placeholder = config.placeholder || "Search";
document.getElementById("empty").textContent = config.empty || "No data";
document.querySelector("[data-action='back']").addEventListener("click", goBack);
document.getElementById("search").addEventListener("click", () => {
const value = document.getElementById("keyword").value.trim();
const base = config.rows || [];
renderRows(value ? base.filter((row) => JSON.stringify(row).toLowerCase().includes(value.toLowerCase())) : base);
});
renderRows(config.rows || []);
document.getElementById("list").hidden = !(config.rows || []).length;
document.getElementById("empty").hidden = (config.rows || []).length > 0;
}
init();

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Exchange Coins</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Exchange Coins","mode":"walletAction","action":"Exchange"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>History Salary</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"History Salary","mode":"teamBill"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

View File

@ -0,0 +1,431 @@
* {
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-center {
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;
z-index: 2;
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
}
.section-title {
display: flex;
align-items: center;
margin: 0 1%;
color: #173a28;
font-size: 1.25em;
font-weight: 700;
}
.surface-card {
margin: 0 1%;
border: 1px solid #b7e0c6;
border-radius: 20px;
background: #fbfffc;
box-shadow: 0 8px 20px rgba(69, 129, 89, 0.12);
}
.agency-card {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
}
.avatar {
position: relative;
width: 50px;
height: 50px;
flex: 0 0 50px;
overflow: hidden;
border-radius: 25px;
background: #91d9aa;
color: #fff;
font-size: 20px;
font-weight: 700;
}
.avatar img,
.avatar span {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.avatar img {
display: block;
object-fit: cover;
}
.avatar span {
display: flex;
align-items: center;
justify-content: center;
}
.user-info {
flex: 1;
min-width: 0;
}
.agency-name {
width: 100%;
margin-bottom: 4px;
color: #244735;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.agency-id {
color: #647468;
font-size: 0.9em;
}
.leave-button {
min-width: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 4px 12px;
border: 1px solid #ff9500;
border-radius: 16px;
color: #ff9500;
font-weight: 700;
}
.salary-card,
.task-card,
.menu-card {
padding: 16px;
}
.card-row,
.menu-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.card-title,
.task-title {
color: #244735;
font-size: 1.08em;
font-weight: 700;
}
.text-link {
color: rgba(0, 0, 0, 0.42);
cursor: pointer;
font-weight: 600;
}
.salary-value {
margin-top: 8px;
display: flex;
align-items: center;
gap: 6px;
color: #105c36;
font-size: 1.45em;
font-weight: 700;
}
.money-icon {
width: 1.05em;
height: 1.05em;
display: block;
flex: 0 0 auto;
}
.task-card {
background: #fffaf0;
border-color: #e7c777;
}
.task-title {
margin-bottom: 12px;
}
.task-grid {
display: flex;
justify-content: space-between;
}
.task-item {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
}
.task-label,
.task-value {
font-weight: 700;
}
.task-label {
margin-bottom: 5px;
color: #4b4536;
}
.task-value {
color: #1f3528;
}
.menu-card {
display: flex;
flex-direction: column;
gap: 4vw;
}
.menu-row {
width: 100%;
min-height: 32px;
text-align: left;
}
.menu-main {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 1vw;
}
.menu-main span {
min-width: 0;
color: #173a28;
font-size: 1.4em;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.menu-icon {
display: block;
width: 1.6em;
flex: 0 0 auto;
}
.arrow-icon {
display: block;
width: 1.5em;
flex: 0 0 auto;
}
.divider {
border-bottom: 1px solid #d9eadf;
}
.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;
}
.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: 2vw;
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.8);
font-weight: 700;
text-align: center;
}
.modal-submessage {
color: rgba(0, 0, 0, 0.4);
font-size: 0.8em;
font-weight: 700;
text-align: center;
}
.modal-actions {
width: 80%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.modal-primary,
.modal-secondary {
display: flex;
align-items: center;
justify-content: center;
min-height: 38px;
padding: 8px 12px;
border-radius: 70px;
font-weight: 800;
}
.modal-primary {
width: 50%;
background: #ff9500;
color: #fff;
}
.modal-actions .modal-primary,
.modal-secondary {
width: 45%;
}
.modal-secondary {
border: 1px solid #ffb805;
background: #fff;
color: #ff9500;
}
[hidden] {
display: none !important;
}
[dir="rtl"] .menu-row,
[dir="rtl"] .page-content {
text-align: right;
}
@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,653 @@
const API_BASE_URL = "https://jvapi.haiyihy.com";
const DEFAULT_AVATAR = "/assets/defaultAvatar-CdxWBK1k-1776148661448.png";
const icons = {
policy: "./icons/policy.svg",
exchange: "./icons/exchange.svg",
transfer: "./icons/transfer.svg",
withdraw: "./icons/withdraw.svg",
bank: "./icons/bank.svg",
money: "./icons/money.svg",
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_center: "Host Center",
my_agency: "My agency",
user_id_prefix: "ID:",
leave: "Leave",
my_available_salary: "My available salary",
my_salary_details: "My salary details",
todays_task: "Today's task",
anchor_type: "Anchor type",
minutes: "Minutes",
gift_task: "Gift Task",
platform_policy: "Platform policy",
exchange_coins: "Exchange coins",
transfer: "Transfer",
cash_withdraw: "Cash withdraw",
bank_accounts: "Bank accounts",
prompt: "Prompt",
tips: "Tips",
fill_info_tips: "Please complete your information first.",
card_issue_tips: "Please make sure your bank card and KYC information are available.",
to_improve: "To improve",
leave_agent_tips: "Are you sure you want to leave?",
cancel: "Cancel",
confirm: "Confirm",
failed_to_load_balance: "Failed to load balance. Please try again.",
something_went_wrong: "Something went wrong."
},
ar: {
host_center: "مركز المضيف",
my_agency: "وكالتي",
user_id_prefix: "ID:",
leave: "مغادرة",
my_available_salary: "راتبي المتاح",
my_salary_details: "تفاصيل الراتب",
todays_task: "مهمة اليوم",
anchor_type: "نوع المضيف",
minutes: "الدقائق",
gift_task: "مهمة الهدايا",
platform_policy: "سياسة المنصة",
exchange_coins: "استبدال العملات",
transfer: "تحويل",
cash_withdraw: "سحب نقدي",
bank_accounts: "الحسابات البنكية",
prompt: "تنبيه",
tips: "نصائح",
fill_info_tips: "يرجى إكمال معلوماتك أولا.",
card_issue_tips: "يرجى التأكد من توفر البطاقة البنكية ومعلومات KYC.",
to_improve: "إكمال",
leave_agent_tips: "هل أنت متأكد أنك تريد المغادرة؟",
cancel: "إلغاء",
confirm: "تأكيد",
failed_to_load_balance: "فشل تحميل الرصيد. يرجى المحاولة مرة أخرى.",
something_went_wrong: "حدث خطأ ما."
},
tr: {
host_center: "Host Merkezi",
my_agency: "Ajansım",
user_id_prefix: "ID:",
leave: "Ayrıl",
my_available_salary: "Kullanılabilir maaşım",
my_salary_details: "Maaş detayları",
todays_task: "Bugünkü görev",
anchor_type: "Host türü",
minutes: "Dakika",
gift_task: "Hediye görevi",
platform_policy: "Platform politikası",
exchange_coins: "Coin değiştir",
transfer: "Transfer",
cash_withdraw: "Para çek",
bank_accounts: "Banka hesapları",
prompt: "Uyarı",
tips: "İpuçları",
fill_info_tips: "Lütfen önce bilgilerinizi tamamlayın.",
card_issue_tips: "Lütfen banka kartı ve KYC bilgilerinizin mevcut olduğundan emin olun.",
to_improve: "Tamamla",
leave_agent_tips: "Ayrılmak istediğinizden emin misiniz?",
cancel: "İptal",
confirm: "Onayla",
failed_to_load_balance: "Bakiye yüklenemedi. Lütfen tekrar deneyin.",
something_went_wrong: "Bir şeyler ters gitti."
},
bn: {
host_center: "হোস্ট সেন্টার",
my_agency: "আমার এজেন্সি",
user_id_prefix: "ID:",
leave: "ছাড়ুন",
my_available_salary: "আমার উপলভ্য বেতন",
my_salary_details: "বেতনের বিবরণ",
todays_task: "আজকের কাজ",
anchor_type: "হোস্ট ধরন",
minutes: "মিনিট",
gift_task: "গিফট টাস্ক",
platform_policy: "প্ল্যাটফর্ম নীতি",
exchange_coins: "কয়েন এক্সচেঞ্জ",
transfer: "ট্রান্সফার",
cash_withdraw: "ক্যাশ উত্তোলন",
bank_accounts: "ব্যাংক অ্যাকাউন্ট",
prompt: "প্রম্পট",
tips: "টিপস",
fill_info_tips: "প্রথমে আপনার তথ্য সম্পূর্ণ করুন।",
card_issue_tips: "আপনার ব্যাংক কার্ড এবং KYC তথ্য আছে কিনা নিশ্চিত করুন।",
to_improve: "সম্পূর্ণ করুন",
leave_agent_tips: "আপনি কি নিশ্চিত যে আপনি ছাড়তে চান?",
cancel: "বাতিল",
confirm: "নিশ্চিত",
failed_to_load_balance: "ব্যালেন্স লোড করতে ব্যর্থ। আবার চেষ্টা করুন।",
something_went_wrong: "কিছু ভুল হয়েছে।"
}
};
const state = {
lang: resolveLanguage(),
profile: {},
teamInfo: null,
identityInfo: null,
agency: {},
salaryInfo: { currentSalary: "0" },
taskInfo: { anchorType: "Chat", minutesProgress: 0, minutesTotal: 120, giftTask: 0 },
bankCards: [],
activeBankCard: null,
withdrawInfo: null,
withdrawEnabled: false
};
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)) {
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;
}
let attempt = 0;
const poll = () => {
attempt += 1;
if (window.app && typeof window.app.getAccessOrigin === "function") {
try {
const raw = window.app.getAccessOrigin();
if (raw) setHeadersFromApp(parseAccessOrigin(raw));
} catch (error) {
console.error("Failed to parse access origin:", error);
}
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
});
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 || `Request failed: ${response.status}`);
error.response = data;
throw error;
}
return data;
}
function formatMoney(value) {
if (value === null || value === undefined || value === "") return "0";
const number = Number(value);
if (Number.isNaN(number)) return String(value);
return number.toFixed(2);
}
function getMemberId() {
return state.profile.id || state.profile.userId || state.profile.account || null;
}
function getTeamId() {
if (state.teamInfo && state.teamInfo.id) return state.teamInfo.id;
const cached = localStorage.getItem("teamInfo");
if (cached) {
try {
const data = JSON.parse(cached);
if (data && data.id) return data.id;
} catch (error) {
console.error("Failed to parse teamInfo:", error);
}
}
return state.agency.teamId || state.agency.id || null;
}
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);
}
}
const result = await apiGet("/team/member/profile");
if (result.status && result.body) {
state.profile = result.body;
localStorage.setItem("userProfile", JSON.stringify(result.body));
}
}
async function fetchAgencyProfile() {
const result = await apiGet("/team/team-own/profile");
if (result.status && result.body) state.agency = result.body;
}
async function fetchTeamEntry() {
const cached = localStorage.getItem("teamInfo");
if (cached) {
try {
state.teamInfo = JSON.parse(cached);
} catch (error) {
console.error("Failed to parse teamInfo:", error);
}
}
const result = await apiGet("/team/entry");
if (result.status && result.body) {
state.teamInfo = result.body.teamProfile || result.body;
localStorage.setItem("teamInfo", JSON.stringify(state.teamInfo));
}
}
async function fetchBankBalance() {
const result = await apiGet("/wallet/bank/balance");
if (result.status && typeof result.body === "number") {
state.salaryInfo.currentSalary = result.body.toFixed(2);
} else if (result.status && result.body) {
state.salaryInfo.currentSalary = formatMoney(result.body.currentSalary ?? result.body.availableBalance ?? result.body.balance ?? 0);
} else {
state.salaryInfo.currentSalary = "0";
}
}
async function fetchWorkStatistics() {
const memberId = getMemberId();
if (!memberId) {
console.warn("No user ID available");
return;
}
const result = await apiGet(`/team/member/work/statistics-now?dataType=DAILY&memberId=${encodeURIComponent(memberId)}`);
if (result.status && result.body) {
const ownTime = Number(result.body.ownTime) || 0;
const otherTime = Number(result.body.otherTime) || 0;
const total = ownTime + otherTime;
state.taskInfo.anchorType = "Chat";
state.taskInfo.minutesProgress = total >= state.taskInfo.minutesTotal ? state.taskInfo.minutesTotal : total;
state.taskInfo.giftTask = result.body.acceptGiftValue || 0;
}
}
async function fetchMemberWork() {
const memberId = getMemberId();
if (!memberId) return;
const result = await apiGet(`/team/member-work?userId=${encodeURIComponent(memberId)}`);
if (result.status && result.body) console.debug("member work data:", result.body);
}
async function fetchBankCards() {
const result = await apiGet("/wallet/bank-card/list");
if (result.status && result.body) {
state.bankCards = result.body;
state.activeBankCard = state.bankCards.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 };
}
}
async function fetchWithdrawEnabled() {
const result = await apiGet("/wallet/bank/host-salary/withdraw/enabled");
if (result.status) state.withdrawEnabled = Boolean(result.body);
}
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;
state.identityInfo = result.body;
const roles = calculateRoles(result.body);
const primaryRole = getPrimaryRole(roles);
if (!roles.includes("anchor")) {
const defaultPage = getDefaultPage(primaryRole);
if (defaultPage !== "/host-center") navigateTo(defaultPage);
}
}
async function leaveAgency() {
await apiPost("/team/user/apply", {
reason: "QUIT",
teamId: getTeamId()
});
}
async function runTask(task, label) {
try {
await task();
} catch (error) {
console.error(`${label} failed:`, error);
if (label === "bank balance") showToast(t("failed_to_load_balance"));
if (label === "withdraw info" || label === "withdraw enabled") showToast(error.response?.errorMsg || error.message || t("something_went_wrong"));
}
}
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.app && typeof window.app.closePage === "function") {
window.app.closePage();
return;
}
if (window.FlutterPageControl && typeof window.FlutterPageControl.postMessage === "function") {
window.FlutterPageControl.postMessage("close_page");
return;
}
if (window.history.length > 1) {
window.history.back();
}
}
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 showModal(id) {
const modal = $(`#${id}`);
if (modal) modal.hidden = false;
}
function hideModal(id) {
const modal = $(`#${id}`);
if (modal) modal.hidden = true;
}
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() {
$("#policyIcon").src = icons.policy;
$("#exchangeIcon").src = icons.exchange;
$("#transferIcon").src = icons.transfer;
$("#withdrawIcon").src = icons.withdraw;
$("#bankIcon").src = icons.bank;
$("#moneyIcon").src = icons.money;
$("#backIcon").src = icons.back;
["#arrowIcon1", "#arrowIcon2", "#arrowIcon3", "#arrowIcon4", "#arrowIcon5"].forEach((selector) => {
$(selector).src = icons.arrow;
});
}
function renderAgency() {
const avatar = $("#agencyAvatar");
const fallback = $("#avatarFallback");
const displayName = state.agency.userNickname || state.agency.name || "-";
const account = state.agency.ownSpecialId?.account || state.agency.account || "-";
$("#agencyName").textContent = displayName;
$("#agencyId").textContent = account;
fallback.textContent = (displayName || "U").slice(0, 1).toUpperCase();
avatar.alt = displayName;
avatar.src = state.agency.userAvatar || "";
avatar.hidden = !state.agency.userAvatar;
}
function renderData() {
$("#currentSalary").textContent = state.salaryInfo.currentSalary;
$("#anchorType").textContent = state.taskInfo.anchorType;
$("#minutesProgress").textContent = state.taskInfo.minutesProgress;
$("#minutesTotal").textContent = state.taskInfo.minutesTotal;
$("#giftTask").textContent = state.taskInfo.giftTask;
$("#cashWithdrawRow").hidden = !state.withdrawEnabled;
$("#cashDivider").hidden = !state.withdrawEnabled;
}
function render() {
renderI18n();
renderIcons();
renderAgency();
renderData();
}
function bindEvents() {
$("[data-action='back']").addEventListener("click", closePage);
$("[data-action='language']").addEventListener("click", () => navigateTo("/language"));
$("[data-action='leave']").addEventListener("click", () => showModal("leaveModal"));
$$("[data-route]").forEach((node) => {
node.addEventListener("click", () => navigateTo(node.dataset.route));
});
$("[data-action='cash-withdraw']").addEventListener("click", () => {
if (!state.withdrawInfo || !state.activeBankCard) {
showModal("infoMissingModal");
return;
}
navigateTo("/cash-withdraw");
});
$("#agencyAvatar").addEventListener("error", (event) => {
event.target.onerror = null;
event.target.src = DEFAULT_AVATAR;
event.target.hidden = false;
});
$("#infoMissingModal").addEventListener("click", () => hideModal("infoMissingModal"));
$("#leaveModal").addEventListener("click", () => hideModal("leaveModal"));
$$(".modal-card").forEach((node) => node.addEventListener("click", (event) => event.stopPropagation()));
$("[data-action='improve']").addEventListener("click", () => navigateTo("/host-setting"));
$("[data-action='cancel-leave']").addEventListener("click", () => hideModal("leaveModal"));
$("[data-action='confirm-leave']").addEventListener("click", async () => {
try {
await leaveAgency();
} catch (error) {
showToast(error.response?.errorMsg || error.errorMsg || t("something_went_wrong"));
} finally {
hideModal("leaveModal");
}
});
}
async function init() {
localStorage.setItem("identity", JSON.stringify({ identity: "HOST" }));
localStorage.removeItem("payee");
bindEvents();
render();
try {
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")
]);
} catch (error) {
console.error("Host center data request failed:", error);
showToast(error.response?.errorMsg || error.message || t("failed_to_load_balance"));
} 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

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<rect x="4" y="4" width="40" height="40" rx="14" fill="#EDF8F1"/>
<path d="M14 21h20M16 21v11M22 21v11M28 21v11M34 34H14M12 18l12-7 12 7H12Z" fill="#FFFFFF"/>
<path d="M14 21h20M16 21v11M22 21v11M28 21v11M34 34H14M12 18l12-7 12 7H12Z" stroke="#1F6F46" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 406 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<rect x="4" y="4" width="40" height="40" rx="14" fill="#FFF3C8"/>
<circle cx="24" cy="24" r="11" fill="#FFFFFF" stroke="#A07100" stroke-width="2.4"/>
<path d="M18 21h11l-3-3M30 27H19l3 3" stroke="#A07100" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M24 16v16" stroke="#A07100" stroke-width="1.8" stroke-linecap="round" opacity=".45"/>
</svg>

After

Width:  |  Height:  |  Size: 456 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" fill="none">
<circle cx="20" cy="20" r="18" fill="#DFF5E7"/>
<circle cx="20" cy="20" r="13" fill="#FFFFFF" stroke="#1F6F46" stroke-width="2.4"/>
<path d="M20 11.5v17M24.5 15.5h-6.2a3 3 0 0 0 0 6h3.4a3 3 0 0 1 0 6h-6.2" stroke="#1F6F46" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 377 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<rect x="4" y="4" width="40" height="40" rx="14" fill="#DFF5E7"/>
<path d="M17 12h10l5 5v19H17V12Z" fill="#FFFFFF" stroke="#1F6F46" stroke-width="2.4" stroke-linejoin="round"/>
<path d="M27 12v6h6" stroke="#1F6F46" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 24h8M21 29h8M21 34h5" stroke="#1F6F46" stroke-width="2.4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 467 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<rect x="4" y="4" width="40" height="40" rx="14" fill="#EAF4FF"/>
<path d="M14 18h17l-4-4M34 30H17l4 4" stroke="#266C9F" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="17" cy="30" r="3" fill="#FFFFFF" stroke="#266C9F" stroke-width="2"/>
<circle cx="31" cy="18" r="3" fill="#FFFFFF" stroke="#266C9F" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 439 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<rect x="4" y="4" width="40" height="40" rx="14" fill="#FFF0E6"/>
<path d="M15 18h18a3 3 0 0 1 3 3v12H15a3 3 0 0 1-3-3v-9a3 3 0 0 1 3-3Z" fill="#FFFFFF" stroke="#B9652D" stroke-width="2.4" stroke-linejoin="round"/>
<path d="M16 18v-3h14v3" stroke="#B9652D" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="29" cy="26" r="2.5" fill="#B9652D"/>
<path d="M18 26h6" stroke="#B9652D" stroke-width="2.4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 544 B

View File

@ -1,35 +1,111 @@
<!doctype html>
<html lang="zh-CN">
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
>
<meta name="format-detection" content="telephone=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Yumi H5</title>
<style>
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-webkit-overflow-scrolling: touch;
}
</style>
<script>
window.__H5_ROUTE_PATH__ = "/host-center";
</script>
<script src="/assets/yumi-route-entry.js"></script>
<script type="module" crossorigin src="/js/index-CIAVq8iD-1776148658686.js"></script>
<link rel="stylesheet" crossorigin href="/css/index-VRlNQ74k-1776148661679.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>Host Center</title>
<link rel="stylesheet" href="./host-center.css" />
</head>
<body>
<div id="app"></div>
<div class="host-center">
<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_center">Host Center</h1>
<button class="lang-button" type="button" data-action="language">EN</button>
</header>
<main class="page-content">
<section class="section-title" data-i18n="my_agency">My agency</section>
<section class="surface-card agency-card">
<div class="avatar">
<img id="agencyAvatar" src="" alt="" />
<span id="avatarFallback">U</span>
</div>
<div class="user-info">
<div id="agencyName" class="agency-name">-</div>
<div class="agency-id"><span data-i18n="user_id_prefix">ID:</span> <span id="agencyId">-</span></div>
</div>
<button class="leave-button" type="button" data-action="leave" data-i18n="leave">Leave</button>
</section>
<section class="surface-card salary-card">
<div class="card-row">
<div class="card-title" data-i18n="my_available_salary">My available salary</div>
<button class="text-link" type="button" data-route="/history-salary" data-i18n="my_salary_details">My salary details</button>
</div>
<div class="salary-value"><img id="moneyIcon" class="money-icon" alt="" /><span id="currentSalary">0</span></div>
</section>
<section class="surface-card task-card">
<div class="task-title" data-i18n="todays_task">Today's task</div>
<div class="task-grid">
<div class="task-item">
<div class="task-label" data-i18n="anchor_type">Anchor type</div>
<div class="task-value" id="anchorType">Chat</div>
</div>
<div class="task-item">
<div class="task-label" data-i18n="minutes">Minutes</div>
<div class="task-value"><span id="minutesProgress">0</span>/<span id="minutesTotal">120</span></div>
</div>
<div class="task-item">
<div class="task-label" data-i18n="gift_task">Gift Task</div>
<div class="task-value" id="giftTask">0</div>
</div>
</div>
</section>
<section class="surface-card menu-card">
<button class="menu-row" type="button" data-route="/platform-policy?from=hostcenter">
<span class="menu-main"><img class="menu-icon" id="policyIcon" alt="" /><span data-i18n="platform_policy">Platform policy</span></span>
<img class="arrow-icon" id="arrowIcon1" alt="" />
</button>
<div class="divider"></div>
<button class="menu-row" type="button" data-route="/exchange-gold-coins">
<span class="menu-main"><img class="menu-icon" id="exchangeIcon" alt="" /><span data-i18n="exchange_coins">Exchange coins</span></span>
<img class="arrow-icon" id="arrowIcon2" alt="" />
</button>
<div class="divider"></div>
<button class="menu-row" type="button" data-route="/transfer">
<span class="menu-main"><img class="menu-icon" id="transferIcon" alt="" /><span data-i18n="transfer">Transfer</span></span>
<img class="arrow-icon" id="arrowIcon3" alt="" />
</button>
<div class="divider" id="cashDivider" hidden></div>
<button class="menu-row" type="button" data-action="cash-withdraw" id="cashWithdrawRow" hidden>
<span class="menu-main"><img class="menu-icon" id="withdrawIcon" alt="" /><span data-i18n="cash_withdraw">Cash withdraw</span></span>
<img class="arrow-icon" id="arrowIcon4" alt="" />
</button>
<div class="divider"></div>
<button class="menu-row" type="button" data-route="/host-setting">
<span class="menu-main"><img class="menu-icon" id="bankIcon" alt="" /><span data-i18n="bank_accounts">Bank accounts</span></span>
<img class="arrow-icon" id="arrowIcon5" alt="" />
</button>
</section>
</main>
<div class="toast" id="toast" hidden></div>
<div class="modal-layer" id="infoMissingModal" hidden>
<div class="modal-card">
<div class="modal-title" data-i18n="prompt">Prompt</div>
<div class="modal-message" data-i18n="fill_info_tips">Please complete your information first.</div>
<div class="modal-submessage" data-i18n="card_issue_tips">Please make sure your bank card and KYC information are available.</div>
<button class="modal-primary" type="button" data-action="improve" data-i18n="to_improve">To improve</button>
</div>
</div>
<div class="modal-layer" id="leaveModal" hidden>
<div class="modal-card">
<div class="modal-title" data-i18n="tips">Tips</div>
<div class="modal-message" data-i18n="leave_agent_tips">Are you sure you want to leave?</div>
<div class="modal-actions">
<button class="modal-secondary" type="button" data-action="cancel-leave" data-i18n="cancel">Cancel</button>
<button class="modal-primary" type="button" data-action="confirm-leave" data-i18n="confirm">Confirm</button>
</div>
</div>
</div>
</div>
<script type="module" src="./host-center.js"></script>
</body>
</html>

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Host Setting</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Host Setting","mode":"bankCards"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Income Details</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Income Details","mode":"policy"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Information Details</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Information Details","mode":"policy"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

110
h5/invite-agency/index.html Normal file
View File

@ -0,0 +1,110 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
/>
<title>Invite User To Become Agent</title>
<link rel="stylesheet" href="./yumi-invite-agency.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>
</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-agency.js"></script>
</body>
</html>

View File

@ -0,0 +1,177 @@
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

@ -0,0 +1,694 @@
(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

@ -0,0 +1,109 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
/>
<title>Invite User To Become BD Leader</title>
<link rel="stylesheet" href="./yumi-invite-bd-leader.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 Leader</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"
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-leader.js"></script>
</body>
</html>

View File

@ -0,0 +1,177 @@
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

@ -0,0 +1,699 @@
(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 ENDPOINTS = {
invitedList: "/team/bd/invite-bdleader-message-own",
invite: "/team/bd/invite-bdleader",
cancelPrefix: "/team/bd/invite-bdleader-cancel/",
};
const TEXT = {
pageTitle: "Invite User To Become BD Leader",
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", ENDPOINTS.invitedList);
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", ENDPOINTS.invite, {
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", ENDPOINTS.cancelPrefix + 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);
}
})();

109
h5/invite-bd/index.html Normal file
View File

@ -0,0 +1,109 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
/>
<title>Invite User To Become BD</title>
<link rel="stylesheet" href="./yumi-invite-bd.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>
</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"
placeholder="Enter User ID"
autocomplete="off"
style="
width: 100%;
color: rgba(0, 0, 0, 0.4);
font-weight: 600;
outline: none;
border: none;
background-color: transparent;
padding: 8px;
"
/>
<button
id="clearButton"
type="button"
hidden
style="background: none; border: none; color: #9ca3af; cursor: pointer; padding: 0 4px"
>
×
</button>
</div>
</div>
</div>
<button style="font-weight: 600" class="invite-btn" id="searchButton" type="button" disabled>Search</button>
</div>
<div id="resultList" style="margin: 16px 0; display: flex; flex-direction: column; gap: 12px"></div>
<div id="noData" class="no-data">please input user id</div>
</div>
</div>
<template id="userCardTemplate">
<div
class="surface-border-wrapper user-card"
style="
position: relative;
z-index: 0;
border-radius: 20px;
"
>
<div class="surface-border-content">
<div style="padding: 10px; transition: all 0.2s">
<div class="status-badge" style="position: absolute; z-index: 3; top: -2px; right: -2px"></div>
<div style="font-weight: 600">Information:</div>
<div style="display: flex; align-items: center; justify-content: space-between; gap: 4px">
<div style="flex: 1; min-width: 0; align-self: stretch; display: flex; align-items: center; gap: 4px">
<div style="width: 3em; display: flex; align-items: center; justify-content: center">
<img class="avatar-image" src="" alt="" style="width: 100%; aspect-ratio: 1/1; object-fit: cover; border-radius: 50%" />
</div>
<div style="flex: 1; min-width: 0; align-self: stretch">
<div class="nickname" style="margin-bottom: 4px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap"></div>
<div class="account" style="font-size: 0.9em; color: rgba(0, 0, 0, 0.4); font-weight: 500"></div>
</div>
</div>
<div style="width: auto; min-width: 0; align-self: stretch; display: flex; justify-content: center; align-items: center">
<div class="pending-actions"></div>
</div>
</div>
</div>
</div>
</div>
</template>
<script src="./yumi-invite-bd.js"></script>
</body>
</html>

View File

@ -0,0 +1,177 @@
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

@ -0,0 +1,699 @@
(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 ENDPOINTS = {
invitedList: "/team/bd/invite-bd-message-own",
invite: "/team/bd/invite-bd",
cancelPrefix: "/team/bd/invite-bd-cancel/",
};
const TEXT = {
pageTitle: "Invite User To Become BD",
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", ENDPOINTS.invitedList);
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", ENDPOINTS.invite, {
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", ENDPOINTS.cancelPrefix + 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

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Invite Members</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Invite Members","mode":"inviteProcess"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

View File

@ -0,0 +1,109 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
/>
<title>Invite User To Become Recharge Agent</title>
<link rel="stylesheet" href="./yumi-invite-recharge-agency.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 Recharge 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"
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-recharge-agency.js"></script>
</body>
</html>

View File

@ -0,0 +1,177 @@
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

@ -0,0 +1,699 @@
(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 ENDPOINTS = {
invitedList: "/team/bd/invite-recharge-agent-message-own",
invite: "/team/bd/invite-recharge-agent",
cancelPrefix: "/team/bd/invite-recharge-agent-cancel/",
};
const TEXT = {
pageTitle: "Invite User To Become Recharge 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", ENDPOINTS.invitedList);
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", ENDPOINTS.invite, {
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", ENDPOINTS.cancelPrefix + 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

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Item Distribution</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Item Distribution","mode":"itemDistribution","admin":true};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

35
h5/language/index.html Normal file
View File

@ -0,0 +1,35 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
>
<meta name="format-detection" content="telephone=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Yumi H5</title>
<style>
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-webkit-overflow-scrolling: touch;
}
</style>
<script>
window.__H5_ROUTE_PATH__ = "/language";
</script>
<script src="/assets/yumi-route-entry.js"></script>
<script type="module" crossorigin src="/js/index-CIAVq8iD-1776148658686.js"></script>
<link rel="stylesheet" crossorigin href="/css/index-VRlNQ74k-1776148661679.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

355
h5/logic-pages/app.css Normal file
View File

@ -0,0 +1,355 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: "Baloo 2", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
html,
body {
width: 100%;
min-height: 100%;
background: #edf8f1;
color: #1f3528;
-webkit-text-size-adjust: 100%;
}
button,
input {
font: inherit;
}
button {
border: 0;
background: transparent;
color: inherit;
}
.page {
min-height: 100vh;
background: #edf8f1;
}
.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: #f8fff9;
border-bottom: 1px solid #cce9d7;
box-shadow: 0 4px 14px rgba(75, 137, 95, 0.14);
}
.header h1 {
min-width: 0;
color: #153d28;
font-size: 1.28em;
font-weight: 800;
text-align: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.back,
.lang {
min-width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
color: #1f6f46;
}
.back span {
font-size: 2em;
line-height: 1;
}
.lang {
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;
}
.card {
border: 1px solid #b7e0c6;
border-radius: 18px;
background: #fff;
box-shadow: 0 8px 20px rgba(69, 129, 89, 0.12);
overflow: hidden;
}
.card-pad {
padding: 14px;
}
.metric-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.metric {
padding: 12px;
border-radius: 14px;
border: 1px solid #cce9d7;
background: #fbfffc;
}
.metric:nth-child(2n) {
background: #f7fbec;
}
.label {
font-size: 0.84em;
color: #5f7468;
font-weight: 800;
}
.value {
margin-top: 4px;
color: #153d28;
font-size: 1.12em;
font-weight: 900;
}
.section-title {
color: #1f5134;
font-size: 1.04em;
font-weight: 900;
}
.row {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 13px 14px;
border-bottom: 1px solid #e2f1e7;
}
.row:last-child {
border-bottom: 0;
}
.avatar {
width: 46px;
height: 46px;
flex-shrink: 0;
border-radius: 50%;
background: #dff5e7;
object-fit: cover;
}
.main {
flex: 1;
min-width: 0;
}
.title {
color: #173a28;
font-weight: 900;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.sub {
margin-top: 4px;
color: #6d7e72;
font-size: 0.84em;
font-weight: 700;
}
.side {
flex-shrink: 0;
text-align: right;
font-size: 0.84em;
font-weight: 800;
color: #375744;
}
.tag {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
background: #fff3c8;
padding: 5px 9px;
color: #846216;
font-size: 0.82em;
font-weight: 900;
}
.toolbar,
.tabs,
.form-row {
display: flex;
align-items: center;
gap: 8px;
}
.toolbar input,
.form-row input,
.input {
width: 100%;
border: 1px solid #cce9d7;
border-radius: 12px;
background: #f8fff9;
padding: 10px 12px;
outline: none;
color: #1f3528;
font-weight: 800;
}
.button,
.primary,
.ghost {
min-height: 38px;
border-radius: 12px;
padding: 8px 12px;
font-weight: 900;
}
.primary {
background: #dff5e7;
border: 1px solid #b7e0c6;
color: #1f6f46;
}
.ghost {
border: 1px solid #cce9d7;
color: #375744;
background: #fff;
}
.tabs button {
flex: 1;
border: 1px solid #cce9d7;
background: #fff;
color: #375744;
border-radius: 999px;
padding: 9px 8px;
font-weight: 900;
}
.tabs button.active {
background: #dff5e7;
color: #1f6f46;
border-color: #94d9ae;
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.grid.three {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.tile {
min-height: 96px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
border: 1px solid #cce9d7;
border-radius: 14px;
background: #fbfffc;
color: #173a28;
font-weight: 900;
text-align: center;
}
.tile.selected {
background: #dff5e7;
border-color: #74c993;
}
.tile img {
width: 54%;
max-height: 64px;
object-fit: contain;
}
.empty,
.loading {
padding: 28px 16px;
text-align: center;
color: #6d7e72;
font-weight: 800;
}
.modal-mask {
position: fixed;
inset: 0;
z-index: 50;
display: none;
align-items: flex-end;
background: rgba(0, 0, 0, 0.45);
}
.modal-mask.show {
display: flex;
}
.modal {
width: 100%;
max-height: 82vh;
overflow: auto;
border-radius: 18px 18px 0 0;
background: #fff;
padding: 16px;
}
.modal.center {
width: calc(100% - 32px);
margin: auto;
border-radius: 18px;
}
.modal-title {
margin-bottom: 12px;
color: #153d28;
font-weight: 900;
text-align: center;
}
.toast {
position: fixed;
left: 50%;
bottom: calc(22px + env(safe-area-inset-bottom));
z-index: 80;
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: 800;
text-align: center;
}
[hidden] {
display: none !important;
}

662
h5/logic-pages/app.js Normal file
View File

@ -0,0 +1,662 @@
const API_BASE_URL = "https://jvapi.haiyihy.com";
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"
};
const cfg = window.PAGE_CONFIG || {};
const $ = (selector, root = document) => root.querySelector(selector);
const money = (v) => {
const n = Number(v || 0);
return Number.isFinite(n) ? n.toFixed(2).replace(/\.00$/, "") : String(v || 0);
};
function lang() {
const value = new URLSearchParams(location.search).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 splitPairs(value) {
const result = {};
String(value || "").split(";").forEach((item) => {
const index = item.indexOf("=");
if (index > -1) result[item.slice(0, index)] = item.slice(index + 1);
});
return result;
}
function setAppHeaders(raw) {
if (!raw) return;
const data = typeof raw === "string" ? JSON.parse(raw) : raw;
const appIntel = splitPairs(data["Req-App-Intel"]);
const sysOrigin = splitPairs(data["Req-Sys-Origin"]);
headers = {
...headers,
authorization: data.Authorization || headers.authorization,
"req-lang": data["Req-Lang"] || headers["req-lang"],
"req-app-intel": [`version=${appIntel.version || "1.0.0"}`, `build=${appIntel.build || "1"}`, `channel=${appIntel.channel || "Web"}`].join(";"),
"req-imei": appIntel["Req-Imei"] || headers["req-imei"],
"req-sys-origin": [`origin=${sysOrigin.origin || "LIKEI"}`, `child=${sysOrigin.child || "LIKEI"}`].join(";")
};
}
async function connectApp() {
if (!(window.app || window.webkit || window.FlutterPageControl)) {
headers["req-lang"] = lang().toLowerCase();
return;
}
const raw = await new Promise((resolve) => {
window.renderData = resolve;
window.getIosAccessOriginParam = resolve;
let attempt = 0;
const poll = () => {
attempt += 1;
if (window.app && typeof window.app.getAccessOrigin === "function") return resolve(window.app.getAccessOrigin());
if (attempt < 50) return setTimeout(poll, 100);
if (window.FlutterApp && typeof window.FlutterApp.postMessage === "function") window.FlutterApp.postMessage("requestAccessOrigin");
resolve(null);
};
poll();
});
if (raw) setAppHeaders(raw);
}
async function request(method, path, body) {
const response = await fetch(`${API_BASE_URL}${path}`, {
method,
headers: { ...headers, "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 get = (path) => request("GET", path);
const post = (path, body) => request("POST", path, body);
function getTeamId() {
const keys = ["teamId", "TEAM_ID", "team_id"];
for (const key of keys) {
const value = localStorage.getItem(key);
if (value) return value.replace(/^"|"$/g, "");
}
for (const key of ["teamInfo", "userInfo", "identity"]) {
try {
const item = JSON.parse(localStorage.getItem(key) || "{}");
if (item.teamId || item.id) return item.teamId || item.id;
} catch {}
}
return "";
}
function getIdentity() {
try {
const value = JSON.parse(localStorage.getItem("identity") || "{}");
return value.identity || value.role || "BD";
} catch {
return "BD";
}
}
function toast(message) {
const node = $("#toast");
node.textContent = message || "Request failed";
node.hidden = false;
clearTimeout(toast.timer);
toast.timer = setTimeout(() => (node.hidden = true), 2500);
}
function setLoading(text = "Loading...") {
$("#app").innerHTML = `<div class="loading">${text}</div>`;
}
function row({ avatar, title, sub, side, tag, onClick }) {
const node = document.createElement("button");
node.type = "button";
node.className = "row";
node.innerHTML = `${avatar !== undefined ? `<img class="avatar" src="${avatar || ""}" alt="">` : ""}<div class="main"><div class="title"></div><div class="sub"></div></div><div class="side"></div>${tag ? `<span class="tag"></span>` : ""}`;
$(".title", node).textContent = title || "-";
$(".sub", node).textContent = sub || "";
$(".side", node).textContent = side || "";
if (tag) $(".tag", node).textContent = tag;
if (onClick) node.addEventListener("click", onClick);
return node;
}
function metrics(items) {
return `<section class="metric-grid">${items.map((item) => `<div class="metric"><div class="label">${item.label}</div><div class="value">${item.value}</div></div>`).join("")}</section>`;
}
function showModal(title, content, center = false) {
const mask = $("#modalMask");
mask.innerHTML = `<div class="modal ${center ? "center" : ""}"><div class="modal-title"></div><div class="modal-body"></div></div>`;
$(".modal-title", mask).textContent = title || "";
$(".modal-body", mask).append(content);
mask.classList.add("show");
}
function closeModal() {
$("#modalMask").classList.remove("show");
}
function navigate(route) {
const url = new URL(route, location.origin);
const langParam = new URLSearchParams(location.search).get("lang");
if (langParam && !url.searchParams.has("lang")) url.searchParams.set("lang", langParam);
location.href = `${url.pathname}${url.search}${url.hash}`;
}
function renderShell() {
document.title = cfg.title || "Yumi H5";
document.body.innerHTML = `
<div class="page">
<header class="header">
<button class="back" type="button" data-back aria-label="Back"><span></span></button>
<h1>${cfg.title || "Yumi H5"}</h1>
<button class="lang" type="button">${lang()}</button>
</header>
<main class="content" id="app"></main>
<div class="modal-mask" id="modalMask"></div>
<div class="toast" id="toast" hidden></div>
</div>`;
$("[data-back]").addEventListener("click", () => history.length > 1 ? history.back() : window.app?.closePage?.());
$("#modalMask").addEventListener("click", (event) => {
if (event.target.id === "modalMask") closeModal();
});
}
async function renderTeamHistory() {
setLoading();
const [listRes, historyRes] = await Promise.allSettled([get(cfg.listEndpoint), get(cfg.historyEndpoint)]);
const bill = listRes.value?.body || {};
const history = historyRes.value?.body || {};
$("#app").innerHTML = `
${metrics([
{ label: cfg.countLabel, value: bill.memberCount || 0 },
{ label: cfg.totalLabel, value: `$${money(bill.totalSalary || bill.teamSalary || bill.totalRecharge || 0)}` }
])}
<section class="card">
<div class="row">
<div class="main"><div class="title">${bill.billTitle || cfg.title}</div><div class="sub">${cfg.totalLabel}: $${money(bill.totalSalary || 0)}</div></div>
<button class="primary" id="historyBtn" type="button">History</button>
</div>
<div id="list"></div>
</section>`;
const list = $("#list");
(bill.memberBillList || []).forEach((item) => {
list.appendChild(row({
avatar: item.agentAvatar || item.userAvatar,
title: item.agentName || item.userName,
sub: `ID: ${item.agentAccount || item.account || "-"}`,
side: `${cfg.memberSide1}: $${money(item.teamSalaryAmount || item.salary || 0)}\n${cfg.memberSide2}: ${item.teamMemberCount || item.hostCount || item.agencyCount || 0}`
}));
});
if (!list.children.length) list.innerHTML = `<div class="empty">No data</div>`;
$("#historyBtn").addEventListener("click", () => {
const body = document.createElement("div");
body.innerHTML = metrics([
{ label: "Total income", value: `$${money(history.totalIncome || 0)}` },
{ label: "Previous period", value: `$${money(history.previousPeriodIncome || 0)}` }
]);
const wrap = document.createElement("div");
wrap.className = "card";
(history.incomeDetails || []).forEach((item) => {
wrap.appendChild(row({
title: item.billTitle || item.settlementPeriod,
sub: `${cfg.countLabel}: ${item.agencyNumber || item.bdNumber || item.memberCount || 0}`,
side: `$${money(item.teamSalaryAmount || item.teamTotalIncome || item.settlementSalary || 0)}`,
tag: item.statusText || item.status,
onClick: async () => {
if (!cfg.moreEndpoint || !item.billBelong) return;
try {
const more = await get(`${cfg.moreEndpoint}${item.billBelong}`);
const detail = document.createElement("div");
const card = document.createElement("div");
card.className = "card";
(more.body?.memberDetails || more.body?.members || []).forEach((member) => {
card.appendChild(row({
avatar: member.userAvatar || member.bdUserAvatar || member.avatar,
title: member.userName || member.bdUserName || member.account,
sub: `ID: ${member.account || "-"}`,
side: `$${money(member.salary || member.income || 0)}`
}));
});
detail.appendChild(card);
showModal(more.body?.period || item.billTitle, detail);
} catch (error) {
toast(error.message);
}
}
}));
});
if (!wrap.children.length) wrap.innerHTML = `<div class="empty">No data</div>`;
body.appendChild(wrap);
showModal("History", body);
});
}
async function renderAdminTeam() {
setLoading();
const res = await post("/bd/team/overview", { type: cfg.teamType });
const data = res.body || {};
$("#app").innerHTML = `
${metrics([
{ label: "Total income", value: `$${money(data.totalIncome || 0)}` },
{ label: "Previous period", value: `$${money(data.previousPeriodIncome || 0)}` },
{ label: cfg.memberTitle, value: data.memberCount || 0 },
{ label: "Team total income", value: `$${money(data.teamTotalIncome || 0)}` }
])}
<section class="card" id="members"></section>`;
const members = $("#members");
(data.members || []).forEach((item) => {
members.appendChild(row({
avatar: item.avatar,
title: item.userName || "-",
sub: `ID: ${item.account || "-"}`,
side: `${cfg.subMemberLabel}: ${item.subMemberCount || 0}\n$${money(item.totalIncome || 0)}`,
tag: "More",
onClick: () => showAdminTeamDetail(item)
}));
});
if (!members.children.length) members.innerHTML = `<div class="empty">No data</div>`;
}
async function showAdminTeamDetail(item) {
try {
const res = await post("/bd/team/member/detail", { targetId: item.userId, type: cfg.detailType || cfg.teamType });
const data = res.body || {};
const body = document.createElement("div");
body.innerHTML = metrics([
{ label: "Total income", value: `$${money(data.totalIncome || 0)}` },
{ label: "Previous period", value: `$${money(data.previousPeriodIncome || 0)}` }
]);
const card = document.createElement("div");
card.className = "card";
(data.incomeDetails || []).forEach((detail) => {
card.appendChild(row({
title: detail.settlementPeriod,
sub: `${cfg.subMemberLabel}: ${detail.memberCount || 0}`,
side: `$${money(detail.teamTotalIncome || 0)}`,
tag: detail.status || "More",
onClick: () => {
const detailBody = document.createElement("div");
const detailCard = document.createElement("div");
detailCard.className = "card";
(detail.members || []).forEach((member) => detailCard.appendChild(row({
avatar: member.avatar,
title: member.userName,
sub: `ID: ${member.account || "-"}`,
side: `$${money(member.income || 0)}`
})));
detailBody.appendChild(detailCard);
showModal(detail.settlementPeriod, detailBody);
}
}));
});
if (!card.children.length) card.innerHTML = `<div class="empty">No data</div>`;
body.appendChild(card);
showModal(`${cfg.memberTitle} ${item.account || ""}`, body);
} catch (error) {
toast(error.message);
}
}
async function renderRechargeAgency() {
setLoading();
const [summary, list] = await Promise.allSettled([
get("/app/h5/recharge-summary"),
get("/app/h5/recharge-page?cursor=1&limit=20")
]);
const data = summary.value?.body || {};
const records = list.value?.body?.records || [];
$("#app").innerHTML = `${metrics([
{ label: "My total income", value: `$${money(data.totalIncome || 0)}` },
{ label: "This period recharge", value: `$${money(data.currentMonthRecharge || 0)}` }
])}<div class="section-title">Recharge agency list</div><section class="card" id="records"></section>`;
const root = $("#records");
records.forEach((item) => root.appendChild(row({
avatar: item.userAvatar,
title: item.userNickname,
sub: `ID: ${item.account || "-"}`,
side: `This: $${money(item.thisMonth || 0)}\nLast: $${money(item.lastMonth || 0)}`
})));
if (!root.children.length) root.innerHTML = `<div class="empty">No data</div>`;
}
async function renderMessage() {
const teamId = getTeamId();
setLoading();
const res = await get(`/team/user/apply/record?teamId=${teamId}&status=WAIT`);
const records = (res.body || []).map((item) => ({
id: item.id,
account: item.createUserProfile?.account,
name: item.createUserProfile?.userNickname,
avatar: item.createUserProfile?.userAvatar
}));
$("#app").innerHTML = `<section class="card" id="messages"></section>`;
const root = $("#messages");
records.forEach((item) => {
const node = row({ avatar: item.avatar, title: item.name || "Unknown user", sub: `ID: ${item.account || "-"}` });
const actions = document.createElement("div");
actions.className = "toolbar";
actions.innerHTML = `<button class="primary" type="button">Agree</button><button class="ghost" type="button">Refuse</button>`;
actions.children[0].addEventListener("click", async () => updateApply(item.id, "AGREE", node));
actions.children[1].addEventListener("click", async () => updateApply(item.id, "REJECT", node));
node.appendChild(actions);
root.appendChild(node);
});
if (!root.children.length) root.innerHTML = `<div class="empty">No data</div>`;
}
async function updateApply(id, status, node) {
try {
await post("/team/user/apply/record/update", { id, status });
node.remove();
toast(status === "AGREE" ? "Approved" : "Rejected");
} catch (error) {
toast(error.message);
}
}
async function renderTeamMembers() {
const teamId = getTeamId();
setLoading();
const res = await get(`/team/members?id=${teamId}`);
const records = res.body || [];
$("#app").innerHTML = `
<div class="toolbar"><input id="search" placeholder="Enter host ID"><button class="primary" id="searchBtn">Search</button><button class="ghost" id="cancelBtn">Cancel</button></div>
<section class="card" id="members"></section>`;
const root = $("#members");
const draw = (items) => {
root.innerHTML = "";
items.forEach((member, index) => root.appendChild(row({
avatar: member.userProfile?.userAvatar,
title: `${index + 1}. ${member.userProfile?.userNickname || "-"}`,
sub: `ID: ${member.userProfile?.ownSpecialId?.account || member.userProfile?.account || "-"}`,
side: `${member.targetFormat || "-"}\n${member.timeHours || "-"}`
})));
if (!root.children.length) root.innerHTML = `<div class="empty">No data</div>`;
};
draw(records);
$("#searchBtn").addEventListener("click", async () => {
const account = $("#search").value.trim();
if (!account) return draw(records);
try {
const searched = await get(`/team/search-members?account=${account}`);
draw(searched.body || []);
} catch (error) {
toast(error.message);
}
});
$("#cancelBtn").addEventListener("click", () => {
$("#search").value = "";
draw(records);
});
}
async function renderTeamBill() {
const teamId = getTeamId();
setLoading();
const res = await get(`/team/bill?id=${teamId}`);
const records = res.body || [];
let total = 0;
records.forEach((item) => {
if (item.status === "SETTLED") total += Number(item.settleResult?.totalSalary || 0);
});
$("#app").innerHTML = `${metrics([{ label: "History team income", value: `$${money(total)}` }])}<div class="section-title">Work report</div><section class="card" id="bills"></section>`;
const root = $("#bills");
records.forEach((item) => root.appendChild(row({
title: item.billTitle,
sub: `Host salary: $${money(item.settleResult?.memberSalary || 0)} | Agent salary: $${money(item.settleResult?.ownSalary || 0)}`,
side: `$${money(item.settleResult?.totalSalary || 0)}`,
tag: ({ SETTLED: "Completed", UNPAID: "In Progress", HANG_UP: "Out of account", PAY_OUT: "Completed" })[item.status] || item.status,
onClick: async () => {
try {
const detail = await get(`/team/bill/work-members?id=${item.id}`);
const body = document.createElement("div");
const card = document.createElement("div");
card.className = "card";
(detail.body || []).forEach((member) => card.appendChild(row({
avatar: member.memberProfile?.userAvatar,
title: member.memberProfile?.userNickname,
sub: `ID: ${member.memberProfile?.account || "-"}`,
side: `Target: ${member.target?.acceptGiftValue || 0}\nDays: ${member.target?.effectiveDay || 0}`
})));
body.appendChild(card);
showModal(item.billTitle, body);
} catch (error) {
toast(error.message);
}
}
})));
if (!root.children.length) root.innerHTML = `<div class="empty">No data</div>`;
}
function renderInviteProcess() {
$("#app").innerHTML = `<section class="card card-pad">
<div class="row"><div class="tag">1</div><div class="main"><div class="title">Invite users to download the app.</div></div></div>
<div class="row"><div class="tag">2</div><div class="main"><div class="title">Invited users log in.</div></div></div>
<div class="row"><div class="tag">3</div><div class="main"><div class="title">Users join your team.</div></div></div>
<div class="row"><div class="tag">4</div><div class="main"><div class="title">Enter team ID when joining.</div></div></div>
</section>`;
}
async function renderBankCards() {
setLoading();
const [cards, kyc] = await Promise.allSettled([get("/wallet/bank-card/list"), get("/wallet/withdraw-info/list")]);
const current = (cards.value?.body || []).find((item) => item.use);
const info = kyc.value?.body?.[0];
$("#app").innerHTML = `
<div class="section-title">Bank card</div>
<section class="card" id="cardBox"></section>
<div class="section-title">KYC</div>
<section class="card" id="kycBox"></section>`;
$("#cardBox").appendChild(current ? row({ title: current.cardType, sub: `Card: ${current.cardNo}`, side: current.payee }) : row({ title: "Add payment method", sub: "No bank cards in use", tag: "Add", onClick: () => navigate("/bank-card") }));
$("#kycBox").appendChild(info ? row({ title: `Status: ${info.status}`, sub: `Contact: ${info.contactNumber || "-"}`, side: info.otherDescription || "", tag: "KYC", onClick: () => navigate("/KYC") }) : row({ title: "Complete information", sub: "No KYC info", tag: "KYC", onClick: () => navigate("/KYC") }));
}
async function renderWalletAction() {
setLoading();
const action = new URLSearchParams(location.search).get("activeAction") || cfg.action || "Exchange";
const balanceEndpoint = cfg.salaryType ? `/wallet/salary-account/balance?salaryType=${cfg.salaryType}` : "/wallet/bank/balance";
const balanceRes = await get(balanceEndpoint).catch(() => ({ body: 0 }));
const balance = typeof balanceRes.body === "number" ? balanceRes.body : balanceRes.body?.availableBalance || balanceRes.body?.balance || 0;
const amounts = [1, 5, 10, 50, 100, 200];
$("#app").innerHTML = `
${metrics([{ label: "Available salaries", value: `$${money(balance)}` }])}
<div class="tabs">
<button class="${action === "Exchange" ? "active" : ""}" data-act="Exchange">Exchange</button>
<button class="${action === "Transfer" ? "active" : ""}" data-act="Transfer">Transfer</button>
<button class="${action === "Cash out" ? "active" : ""}" data-act="Cash out">Cash out</button>
</div>
<section class="card card-pad" id="walletPanel"></section>`;
document.querySelectorAll("[data-act]").forEach((btn) => btn.addEventListener("click", () => navigate(`${location.pathname}?activeAction=${encodeURIComponent(btn.dataset.act)}&lang=${lang().toLowerCase()}`)));
const panel = $("#walletPanel");
if (action === "Cash out") return renderCashOutPanel(panel);
if (action === "Transfer") return renderTransferPanel(panel, amounts);
renderExchangePanel(panel, amounts);
}
function renderExchangePanel(panel, amounts) {
panel.innerHTML = `<div class="grid three">${amounts.map((amount) => `<button class="tile" data-amount="${amount}">$${amount}</button>`).join("")}</div><button class="primary" id="submit">Exchange</button>`;
let selected = null;
panel.querySelectorAll("[data-amount]").forEach((node) => node.addEventListener("click", () => {
selected = Number(node.dataset.amount);
panel.querySelectorAll(".tile").forEach((tile) => tile.classList.remove("selected"));
node.classList.add("selected");
}));
$("#submit").addEventListener("click", async () => {
if (!selected) return toast("Please select amount");
try {
const path = cfg.salaryType ? "/wallet/salary-account/exchange/gold" : "/wallet/bank/exchange/gold";
await post(path, cfg.salaryType ? { amount: selected, salaryType: cfg.salaryType } : { amount: selected });
toast("Exchange success");
} catch (error) {
toast(error.message);
}
});
}
function renderTransferPanel(panel, amounts) {
panel.innerHTML = `<div class="form-row"><input id="payee" placeholder="Enter user ID"><button class="primary" id="findPayee">Search</button></div><div id="payeeInfo"></div><div class="grid three">${amounts.map((amount) => `<button class="tile" data-amount="${amount}">$${amount}</button>`).join("")}</div><button class="primary" id="submit">Transfer</button>`;
let payee = null;
let selected = null;
$("#findPayee").addEventListener("click", async () => {
try {
const res = await get(`/user/user-profile/open-search?account=${encodeURIComponent($("#payee").value.trim())}`);
payee = res.body;
$("#payeeInfo").innerHTML = "";
$("#payeeInfo").appendChild(row({ avatar: payee.userAvatar, title: payee.userNickname || payee.name, sub: `ID: ${payee.account}`, side: "" }));
} catch (error) {
toast(error.message);
}
});
panel.querySelectorAll("[data-amount]").forEach((node) => node.addEventListener("click", () => {
selected = Number(node.dataset.amount);
panel.querySelectorAll(".tile").forEach((tile) => tile.classList.remove("selected"));
node.classList.add("selected");
}));
$("#submit").addEventListener("click", async () => {
if (!selected || !payee?.id) return toast("Please select amount and payee");
try {
const path = cfg.salaryType ? "/wallet/salary-account/transferGold" : "/wallet/bank/transferGold";
await post(path, cfg.salaryType ? { amount: selected, acceptUserId: payee.id, salaryType: cfg.salaryType } : { amount: selected, acceptUserId: payee.id, acceptMethod: "BANK_TRANSFER" });
toast("Transfer success");
} catch (error) {
toast(error.message);
}
});
}
async function renderCashOutPanel(panel) {
const [cards, kyc] = await Promise.allSettled([get("/wallet/bank-card/list"), get("/wallet/withdraw-info/list")]);
const card = (cards.value?.body || []).find((item) => item.use);
const info = kyc.value?.body?.[0];
panel.innerHTML = `<div class="form-row"><input id="amount" placeholder="50" inputmode="decimal"></div><div id="card"></div><div id="kyc"></div><button class="primary" id="submit">Cash out</button>`;
$("#card").appendChild(card ? row({ title: card.cardType, sub: `Card: ${card.cardNo}`, side: card.payee }) : row({ title: "No bank cards in use", sub: "Add bank card", tag: "Add", onClick: () => navigate("/bank-card") }));
$("#kyc").appendChild(info ? row({ title: `KYC: ${info.status}`, sub: `Contact: ${info.contactNumber || "-"}`, tag: "KYC" }) : row({ title: "Complete KYC", sub: "No KYC information", tag: "KYC", onClick: () => navigate("/KYC") }));
$("#submit").addEventListener("click", async () => {
const amount = Number($("#amount").value);
if (!card?.id || !amount || amount < 50) return toast("Please complete information and enter at least 50");
try {
await post("/wallet/bank/withdraw", { amount, acceptBankCardId: card.id, salaryType: cfg.salaryType || (getIdentity() === "HOST" ? "HOST_SALARY" : "BD_SALARY") });
toast("Cash withdrawal submitted");
} catch (error) {
toast(error.message);
}
});
}
async function renderItemDistribution() {
setLoading();
const permissionEndpoint = cfg.admin ? "/app/h5/own-permission?propsType=NOBLE_VIP" : "/sys/bd/list-permission";
const propsBase = cfg.admin ? "/sys/manager/props" : "/sys/bd/props";
const sendEndpoint = cfg.admin ? "/sys/manager/send" : "/sys/bd/send";
let tabs = cfg.admin ? [{ name: "frames", type: "AVATAR_FRAME" }, { name: "vehicles", type: "RIDE" }, { name: "vip", type: "NOBLE_VIP" }] : [];
if (!cfg.admin) {
const res = await get(permissionEndpoint);
const body = res.body || [];
if (body.includes("AVATAR_FRAME")) tabs.push({ name: "frames", type: "AVATAR_FRAME" });
if (body.includes("RIDE")) tabs.push({ name: "vehicles", type: "RIDE" });
if (body.some((x) => String(x).startsWith("NOBLE_VIP"))) tabs.push({ name: "vip", type: "NOBLE_VIP" });
}
if (!tabs.length) tabs = [{ name: "frames", type: "AVATAR_FRAME" }];
$("#app").innerHTML = `<div class="tabs">${tabs.map((tab, index) => `<button data-tab="${index}" class="${index === 0 ? "active" : ""}">${tab.name}</button>`).join("")}</div><section class="grid" id="props"></section>`;
const load = async (tab) => {
const res = await get(`${propsBase}?propsType=${tab.type}&currencyType=GOLD`);
const root = $("#props");
root.innerHTML = "";
(res.body || []).forEach((item) => {
const tile = document.createElement("button");
tile.type = "button";
tile.className = "tile";
tile.innerHTML = `<img src="${item.cover || ""}" alt=""><span>${item.name || "Item"}</span><span class="tag">Send</span>`;
tile.addEventListener("click", () => openSendProps(item, sendEndpoint));
root.appendChild(tile);
});
if (!root.children.length) root.innerHTML = `<div class="empty">No data</div>`;
};
document.querySelectorAll("[data-tab]").forEach((btn) => btn.addEventListener("click", () => {
document.querySelectorAll("[data-tab]").forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
load(tabs[Number(btn.dataset.tab)]);
}));
await load(tabs[0]);
}
function openSendProps(item, endpoint) {
const body = document.createElement("div");
body.innerHTML = `<div class="form-row"><input id="targetUser" placeholder="Enter user ID"><button class="primary" id="confirmSend">Confirm</button></div>`;
$("#confirmSend", body).addEventListener("click", async () => {
try {
const user = await get(`/user/user-profile/open-search?account=${encodeURIComponent($("#targetUser", body).value.trim())}`);
await post(endpoint, { propsId: item.id, acceptUserId: user.body.id });
closeModal();
toast("Gift delivery successful");
} catch (error) {
toast(error.message);
}
});
showModal(item.name || "Send", body, true);
}
function renderPolicy() {
$("#app").innerHTML = "";
}
async function renderPlatformPolicy() {
const teamId = getTeamId();
setLoading();
const res = await get(`/team/release/policy?id=${teamId}`);
const policies = res.body?.policy || [];
$("#app").innerHTML = `<section class="card" id="policyList"></section>`;
const root = $("#policyList");
policies.forEach((item) => root.appendChild(row({
title: `Lv.${item.level}`,
sub: `Time: ${item.onlineTime || 0}h | Target: ${item.target || 0}`,
side: `Host: $${money(item.memberSalary || 0)}\nAgent: $${money(item.ownSalary || 0)}`
})));
if (!root.children.length) root.innerHTML = `<div class="empty">No policy data available</div>`;
}
async function init() {
renderShell();
try {
await connectApp();
const handlers = {
teamHistory: renderTeamHistory,
adminTeam: renderAdminTeam,
rechargeAgency: renderRechargeAgency,
message: renderMessage,
teamMembers: renderTeamMembers,
teamBill: renderTeamBill,
inviteProcess: renderInviteProcess,
bankCards: renderBankCards,
walletAction: renderWalletAction,
itemDistribution: renderItemDistribution,
platformPolicy: renderPlatformPolicy,
policy: renderPolicy
};
await (handlers[cfg.mode] || renderPolicy)();
} catch (error) {
console.error(error);
$("#app").innerHTML = `<div class="empty">${error.message || "Failed to load data"}</div>`;
}
}
init();

13
h5/message/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Message</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Message","mode":"message"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

13
h5/my-BD-teams/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>My BD Teams</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"My BD Teams","mode":"adminTeam","teamType":"BD","detailType":"BD","memberTitle":"BDs","subMemberLabel":"Agencies"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>My BD Leader Teams</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"My BD Leader Teams","mode":"adminTeam","teamType":"BD_LEADER","detailType":"BD","memberTitle":"BD leaders","subMemberLabel":"Agencies"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>My Agency Teams</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"My Agency Teams","mode":"adminTeam","teamType":"AGENCY","detailType":"AGENCY","memberTitle":"Agencies","subMemberLabel":"Hosts"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>My Recharge Agency</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"My Recharge Agency","mode":"rechargeAgency"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Platform Policy</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Platform Policy","mode":"platformPolicy"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

13
h5/policy/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>BD Policy</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"BD Policy","mode":"policy"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Search Payee</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Search Payee","mode":"teamMembers"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

13
h5/team-bill/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Team Bill</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Team Bill","mode":"teamBill"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

13
h5/team-member/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Team Member</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Team Member","mode":"teamMembers"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>

13
h5/transfer/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Transfer</title>
<link rel="stylesheet" href="../logic-pages/app.css">
</head>
<body>
<script>window.PAGE_CONFIG = {"title":"Transfer","mode":"walletAction","action":"Transfer"};</script>
<script src="../logic-pages/app.js"></script>
</body>
</html>