币商页面

This commit is contained in:
170-carry 2026-04-30 13:29:00 +08:00
parent 1fc21860a2
commit 1a3984fb63
8 changed files with 2054 additions and 158 deletions

View File

@ -36,7 +36,7 @@
width: min(100%, var(--page-width));
aspect-ratio: 1080 / 2410;
margin: 0 auto;
overflow: hidden;
overflow: visible;
background: var(--brown-990);
}
@ -45,7 +45,7 @@
width: 100%;
height: 100%;
background: var(--brown-990);
overflow: hidden;
overflow: visible;
}
.sr-only {
@ -118,7 +118,141 @@
left: 5.3704%;
top: 63.0705%;
width: 89.1667%;
height: 11.4108%;
height: auto;
overflow: visible;
}
.agency-list {
width: 100%;
display: flex;
flex-direction: column;
gap: 12px;
overflow: visible;
}
.agency-row {
width: 100%;
aspect-ratio: 963 / 275;
display: flex;
align-items: center;
gap: 8px;
padding: 0 24px;
border: 1px solid var(--gold-line);
border-radius: 2.9076% / 10.1818%;
background: var(--panel-brown);
box-shadow:
inset 0 0 0 1px rgba(255, 214, 109, 0.16),
0 6px 18px rgba(38, 11, 0, 0.35);
}
.agency-avatar {
width: 52px;
height: 52px;
flex: 0 0 52px;
border-radius: 50%;
border: 1px solid rgba(255, 220, 136, 0.78);
background: rgba(46, 18, 0, 0.55);
object-fit: cover;
}
.agency-main {
min-width: 0;
flex: 1;
}
.agency-name {
display: block;
color: #fff3cf;
font-size: 13px;
font-weight: 700;
line-height: 1.15;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.agency-meta {
margin-top: 4px;
display: flex;
align-items: center;
gap: 5px;
color: rgba(255, 238, 195, 0.74);
font-size: 10px;
line-height: 1.15;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.agency-flags {
display: flex;
align-items: center;
gap: 3px;
flex: 0 0 auto;
}
.agency-flag {
width: 16px;
height: 16px;
border-radius: 50%;
border: 1px solid rgba(255, 238, 195, 0.62);
object-fit: cover;
background: rgba(46, 18, 0, 0.6);
}
.agency-actions {
flex: 0 0 78px;
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: center;
gap: 5px;
}
.agency-action {
appearance: none;
width: 78px;
height: 24px;
margin: 0;
padding: 0 7px;
border: 1px solid rgba(255, 222, 126, 0.72);
border-radius: 999px;
font-size: 10px;
font-weight: 800;
line-height: 1;
letter-spacing: 0;
white-space: nowrap;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
box-shadow: 0 2px 8px rgba(38, 11, 0, 0.3);
}
.agency-action:active {
transform: translateY(1px);
}
.agency-action.whatsapp {
border-color: rgba(159, 255, 185, 0.84);
background: linear-gradient(180deg, #27c66a, #168a49);
color: #ffffff;
}
.agency-action.profile {
background: linear-gradient(180deg, #fff2a8, #efb43d);
color: #552300;
}
.agency-state {
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 238, 195, 0.78);
font-size: 13px;
font-weight: 650;
text-align: center;
padding: 0 18px;
aspect-ratio: 963 / 275;
border: 1px solid var(--gold-line);
border-radius: 2.9076% / 10.1818%;
background: var(--panel-brown);
@ -127,13 +261,15 @@
.bottom-frame {
position: absolute;
left: 1.8056%;
top: 91%;
top: var(--bottom-frame-top, 91%);
width: 96.1574%;
height: 2.4066%;
display: block;
max-width: none;
pointer-events: none;
user-select: none;
transform: scaleY(-1);
transform-origin: center;
}
</style>
</head>
@ -151,7 +287,11 @@
<img class="title-ribbon" src="../../assets/recharge-agency-list/title-ribbon.png" alt="Recharge Agency" />
<section class="content-panel" aria-label="Recharge agency list"></section>
<section class="content-panel" aria-label="Recharge agency list">
<div class="agency-list" data-agency-list>
<div class="agency-state">Loading agencies...</div>
</div>
</section>
<svg class="bottom-frame" aria-hidden="true" viewBox="0 0 1038.5 58" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 58L9 49V28L34 8H193.5L200 17.5H840.5L845.5 8H1005.5L1031 28.5V49L1038.5 58V25.5L1010 0H840.5L834.5 11.5H204L197 0H30L0 25.5V58Z" fill="url(#bottom-frame-gradient)" />
@ -166,6 +306,307 @@
</svg>
</div>
</main>
<script>
(function () {
const DEFAULT_API_BASE = "https://jvapi.haiyihy.com/";
const DEFAULT_API_PREFIX = "/go";
const ENDPOINT = "/app/h5/recharge-agency-list/sellers";
const DEFAULT_AVATAR = "../../assets/defaultAvatar-CdxWBK1k.png";
const query = new URLSearchParams(window.location.search || "");
const hashQuery = new URLSearchParams((window.location.hash.split("?")[1] || "").split("#")[0]);
const listNode = document.querySelector("[data-agency-list]");
function trimValue(value) {
return String(value || "").trim();
}
function readParam(names) {
for (const name of names) {
const fromQuery = trimValue(query.get(name));
if (fromQuery) return fromQuery;
const fromHash = trimValue(hashQuery.get(name));
if (fromHash) return fromHash;
}
return "";
}
function normalizeBaseURL(base) {
const value = trimValue(base) || DEFAULT_API_BASE;
return value.endsWith("/") ? value : value + "/";
}
function resolveAPIBase() {
const override = readParam(["apiBase", "api_base"]);
if (override) return normalizeBaseURL(override);
if (typeof window.__RECHARGE_AGENCY_API_BASE__ === "string") {
return normalizeBaseURL(window.__RECHARGE_AGENCY_API_BASE__);
}
return normalizeBaseURL(DEFAULT_API_BASE);
}
function resolveAPIPrefix(apiBase) {
const override = readParam(["apiPrefix", "api_prefix"]);
if (override) return override;
if (typeof window.__RECHARGE_AGENCY_API_PREFIX__ === "string") {
return trimValue(window.__RECHARGE_AGENCY_API_PREFIX__);
}
try {
return new URL(apiBase).origin === new URL(DEFAULT_API_BASE).origin ? DEFAULT_API_PREFIX : "";
} catch (_) {
return "";
}
}
function joinPath() {
return "/" + Array.prototype.slice.call(arguments)
.map((part) => trimValue(part).replace(/^\/+|\/+$/g, ""))
.filter(Boolean)
.join("/");
}
function buildURL(path) {
const apiBase = resolveAPIBase();
const apiPrefix = resolveAPIPrefix(apiBase);
const url = new URL(joinPath(apiPrefix, path).replace(/^\/+/, ""), apiBase);
url.searchParams.set("cursor", readParam(["cursor"]) || "1");
url.searchParams.set("limit", readParam(["limit"]) || "20");
return url;
}
function normalizeAuthorization(value) {
const token = trimValue(value);
if (!token) return "";
return /^bearer\s+/i.test(token) ? token : "Bearer " + token;
}
function readAuthorization() {
const direct = readParam(["Authorization", "authorization"]);
if (direct) return normalizeAuthorization(direct);
const token = readParam(["token", "accessToken", "access_token"]);
if (token) return normalizeAuthorization(token);
if (typeof window.__RECHARGE_AGENCY_AUTHORIZATION__ === "string") {
return normalizeAuthorization(window.__RECHARGE_AGENCY_AUTHORIZATION__);
}
try {
return normalizeAuthorization(
window.localStorage.getItem("Authorization") ||
window.localStorage.getItem("authorization") ||
window.localStorage.getItem("token") ||
window.localStorage.getItem("accessToken")
);
} catch (_) {
return "";
}
}
function clearList() {
while (listNode.firstChild) listNode.removeChild(listNode.firstChild);
}
function syncListLayout(count) {
const safeCount = Math.max(1, Number(count) || 1);
const listTop = 63.0705;
const cardHeight = 11.4108;
const cardGap = 1.16;
const bottomGap = 3.95;
const listHeight = safeCount * cardHeight + Math.max(0, safeCount - 1) * cardGap;
const bottomTop = Math.max(91, listTop + listHeight + bottomGap);
document.documentElement.style.setProperty("--bottom-frame-top", bottomTop.toFixed(4) + "%");
}
function renderState(message) {
syncListLayout(1);
clearList();
const node = document.createElement("div");
node.className = "agency-state";
node.textContent = message;
listNode.appendChild(node);
}
function compactNumber(value) {
const number = Number(value || 0);
if (!Number.isFinite(number) || number <= 0) return "";
if (number >= 1000000) return (number / 1000000).toFixed(1).replace(/\.0$/, "") + "M";
if (number >= 1000) return (number / 1000).toFixed(1).replace(/\.0$/, "") + "K";
return String(Math.trunc(number));
}
function createImage(className, src, fallback) {
const img = document.createElement("img");
img.className = className;
img.alt = "";
img.decoding = "async";
img.loading = "lazy";
img.src = trimValue(src) || fallback || "";
img.onerror = function () {
if (fallback && img.src.indexOf(fallback) === -1) {
img.src = fallback;
}
};
return img;
}
function isInAppWebView() {
return !!(
window.FlutterPageControl &&
typeof window.FlutterPageControl.postMessage === "function"
);
}
function sendToApp(message) {
if (!isInAppWebView()) {
console.warn("[AppBridge] FlutterPageControl is not available", message);
return false;
}
window.FlutterPageControl.postMessage(String(message));
return true;
}
function openUserProfile(uid) {
const safeUID = trimValue(uid).replace(/:/g, "");
if (!safeUID) return false;
return sendToApp("view_user_info:" + safeUID);
}
function normalizeWhatsapp(value) {
const raw = trimValue(value);
if (!raw) return "";
const linkMatch = raw.match(/(?:wa\.me\/|phone=)(\+?\d[\d\s().-]*)/i);
const phoneSource = linkMatch ? linkMatch[1] : ((raw.match(/\+?\d[\d\s().-]{5,}\d/) || [raw])[0]);
const digits = phoneSource.replace(/\D/g, "");
return digits.length >= 6 ? digits : "";
}
function buildWhatsappURL(value) {
const raw = trimValue(value);
if (/^https?:\/\/(?:wa\.me|api\.whatsapp\.com|www\.whatsapp\.com)\//i.test(raw)) {
return raw;
}
const phone = normalizeWhatsapp(raw);
return phone ? "https://wa.me/" + encodeURIComponent(phone) : "";
}
function openWhatsapp(value) {
const url = buildWhatsappURL(value);
if (!url) return false;
let opened = null;
if (typeof window.open === "function") {
opened = window.open(url, "_blank", "noopener");
}
if (!opened) {
window.location.href = url;
}
return true;
}
function createRow(item) {
const row = document.createElement("article");
row.className = "agency-row";
row.dataset.userId = trimValue(item.userId);
row.appendChild(createImage("agency-avatar", item.userAvatar, DEFAULT_AVATAR));
const main = document.createElement("div");
main.className = "agency-main";
const name = document.createElement("span");
name.className = "agency-name";
name.textContent = item.userNickname || item.account || ("ID " + item.userId);
main.appendChild(name);
const meta = document.createElement("div");
meta.className = "agency-meta";
const place = [item.countryName, item.regionCode].filter(Boolean).join(" / ");
const count = compactNumber(item.transactionCount);
const metaParts = [];
if (place) metaParts.push(place);
metaParts.push("ID " + (item.account || item.userId));
if (count) metaParts.push(count + " orders");
meta.textContent = metaParts.join(" · ");
main.appendChild(meta);
row.appendChild(main);
const flags = document.createElement("div");
flags.className = "agency-flags";
const flagValues = (Array.isArray(item.nationalFlag) ? item.nationalFlag : []).slice(0, 3).filter(Boolean);
flagValues.forEach(function (flag) {
flags.appendChild(createImage("agency-flag", flag, ""));
});
if (flagValues.length) row.appendChild(flags);
const actions = document.createElement("div");
actions.className = "agency-actions";
const whatsapp = item.whatsapp || item.whatsApp || item.contactInfo;
if (normalizeWhatsapp(whatsapp)) {
const whatsappButton = document.createElement("button");
whatsappButton.type = "button";
whatsappButton.className = "agency-action whatsapp";
whatsappButton.textContent = "WhatsApp";
whatsappButton.setAttribute("aria-label", "Open WhatsApp");
whatsappButton.addEventListener("click", function (event) {
event.stopPropagation();
openWhatsapp(whatsapp);
});
actions.appendChild(whatsappButton);
}
const profileButton = document.createElement("button");
profileButton.type = "button";
profileButton.className = "agency-action profile";
profileButton.textContent = "Profile";
profileButton.setAttribute("aria-label", "Open user profile");
profileButton.addEventListener("click", function (event) {
event.stopPropagation();
openUserProfile(item.userId);
});
actions.appendChild(profileButton);
row.appendChild(actions);
return row;
}
function normalizePayload(json) {
const body = json && (json.body || json.data || json);
return body && Array.isArray(body.records) ? body.records : [];
}
async function fetchAgencies() {
if (!listNode) return;
const authorization = readAuthorization();
if (!authorization) {
renderState("Open in app to view agencies.");
return;
}
try {
const response = await fetch(buildURL(ENDPOINT).toString(), {
cache: "no-store",
headers: {
Accept: "application/json",
Authorization: authorization,
},
});
const json = await response.json().catch(function () { return {}; });
if (!response.ok || json.status === false) {
throw new Error(json.errorMsg || json.message || "Unable to load agencies.");
}
const records = normalizePayload(json);
if (!records.length) {
renderState("No agencies available.");
return;
}
syncListLayout(records.length);
clearList();
records.forEach(function (item) {
listNode.appendChild(createRow(item || {}));
});
} catch (error) {
renderState(error && error.message ? error.message : "Unable to load agencies.");
}
}
fetchAgencies();
})();
</script>
</body>
</html>

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__ = "/coin-seller";
</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>

View File

@ -1,35 +1,19 @@
<!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__ = "/coin-seller";
</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>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var target = new URL("https://h5.haiyihy.com/hyapp/recharge-center/index.html")
var source = new URL(window.location.href)
source.searchParams.forEach(function (value, key) {
target.searchParams.set(key, value)
})
target.searchParams.set("h5v", "20260429-0335")
window.location.replace(target.toString())
</script>
</body>
</html>

View File

@ -4,7 +4,7 @@
<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>Recharge Center</title>
<link rel="stylesheet" href="./style.css?v=20260429-0100" />
<link rel="stylesheet" href="./style.css?v=20260430-0108" />
</head>
<body>
<div class="recharge-center" aria-label="Recharge Center" data-i18n-aria="page_label" data-loading="true">
@ -28,6 +28,49 @@
</nav>
<main class="content">
<section class="card profile-card self-profile-card" aria-label="User profile" data-i18n-aria="profile_card">
<div class="avatar-shell">
<img class="avatar-image" id="selfAvatar" alt="" hidden />
<span class="avatar-fallback" id="selfAvatarFallback">U</span>
</div>
<div class="profile-copy">
<div class="name" id="selfName">-</div>
<div class="meta" id="selfUid">UID: -</div>
<div class="meta whatsapp-meta" id="selfWhatsappRow">
<span class="whatsapp-icon" id="selfWhatsappIcon" aria-hidden="true" hidden>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6.2 18.2 4.8 21l3.2-1.1a8.2 8.2 0 1 0-1.8-1.7Z" />
<path d="M9.4 8.4c.3-.3.8-.3 1 .1l.7 1.2c.2.4.1.8-.2 1.1l-.4.3c.5 1 1.4 1.8 2.4 2.3l.4-.5c.3-.3.7-.4 1.1-.2l1.2.7c.4.2.5.7.2 1-.5.8-1.2 1.1-2.1.9-2.8-.6-5.1-2.8-5.8-5.6-.2-.8.2-1.5 1.5-1.3Z" />
</svg>
</span>
<span id="selfWhatsappText"></span>
<button class="whatsapp-add-button" id="addWhatsappButton" type="button" data-whatsapp-open data-i18n="add_whatsapp">+ add whatsapp</button>
</div>
</div>
</section>
<section class="card balance-card" aria-label="Coins" data-i18n-aria="balance_title">
<div class="balance-head">
<div>
<div class="balance-label" data-i18n="balance_title">Coins</div>
</div>
<button class="history-button" type="button" data-history-open aria-label="History" data-i18n-aria="history">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 8v5l3 2" />
<path d="M3.5 12a8.5 8.5 0 1 0 2.2-5.7" />
<path d="M3.5 4.5v5h5" />
</svg>
<span data-i18n="history">History</span>
</button>
</div>
<div class="balance-value">
<span class="balance-coin gold-coin-icon" aria-hidden="true">
<span class="gold-coin-shine"></span>
</span>
<strong id="selfBalance">0</strong>
</div>
</section>
<section class="card search-card">
<div class="section-head">
<div>
@ -59,28 +102,26 @@
<div class="name" id="targetName">-</div>
<div class="meta" id="targetUid">UID: -</div>
</div>
<button class="mini-action" type="button" data-change-target data-i18n="change">Change</button>
<button class="mini-action mini-action-icon" type="button" data-change-target aria-label="Change" data-i18n-aria="change">X</button>
</section>
<section class="card country-card" id="countryCard" hidden>
<section class="card coin-recharge-card" id="coinRechargeCard" hidden>
<div class="section-head">
<div>
<div class="section-title" data-i18n="select_country">Select a country</div>
<div class="section-subtitle" id="countrySubtitle">-</div>
<div class="section-title" data-i18n="coin_recharge_title">Recharge coins</div>
<div class="section-subtitle" data-i18n="coin_recharge_subtitle">Enter coins amount</div>
</div>
</div>
<div class="country-list" id="countryList"></div>
</section>
<section class="card payment-card" id="paymentCard" hidden>
<div class="section-head">
<div>
<div class="section-title" data-i18n="payment_methods">Payment methods</div>
<div class="section-subtitle" data-i18n="select_package">Select a package</div>
<form class="coin-recharge-form" id="coinRechargeForm" autocomplete="off">
<div class="coin-input-shell">
<span class="input-coin gold-coin-icon" aria-hidden="true">
<span class="gold-coin-shine"></span>
</span>
<input id="coinAmountInput" type="text" inputmode="numeric" data-i18n-placeholder="coin_amount_placeholder" placeholder="Enter coins amount" />
</div>
</div>
<div class="payment-status" id="paymentStatus" role="status" hidden></div>
<div class="channel-list" id="channelList"></div>
<button id="confirmRechargeButton" type="submit" data-i18n="confirm_recharge">Confirm recharge</button>
</form>
<div class="form-status" id="coinRechargeStatus" role="status" hidden></div>
</section>
</main>
@ -94,13 +135,60 @@
<div class="help-content">
<h3 data-i18n="faq_title">FAQ</h3>
<p data-i18n="faq_question_1">How to recharge?</p>
<p data-i18n="faq_answer_1">Enter the user's ID, choose a package, then select a payment method.</p>
<p data-i18n="faq_answer_1">Enter the user's ID, enter the coins amount, then confirm recharge.</p>
<p data-i18n="faq_question_2">How to check recharge result?</p>
<p data-i18n="faq_answer_2">After payment, return to the app and check the user's wallet balance.</p>
</div>
</section>
</div>
<div class="history-modal" id="historyModal" hidden>
<div class="modal-backdrop" data-history-close></div>
<section class="modal-dialog history-dialog" role="dialog" aria-modal="true" aria-labelledby="historyTitle">
<div class="modal-head">
<h2 id="historyTitle" data-i18n="transaction_records">Transaction records</h2>
<button class="modal-close" type="button" aria-label="Close" data-i18n-aria="close" data-history-close>&times;</button>
</div>
<div class="history-body">
<form class="history-search-form" id="historySearchForm" autocomplete="off">
<input id="historySearchInput" type="text" inputmode="text" data-i18n-placeholder="enter_user_id" placeholder="Enter user ID" />
<button type="submit" data-i18n="search">Search</button>
<button id="historyClearButton" class="history-clear-button" type="button" data-i18n="cancel">Cancel</button>
</form>
<div class="history-tabs" role="tablist" aria-label="Transaction type">
<button type="button" class="history-tab is-active" data-history-tab="0" data-i18n="income">Income</button>
<button type="button" class="history-tab" data-history-tab="1" data-i18n="expenditure">Expenditure</button>
</div>
<div class="history-status" id="historyStatus" role="status" hidden></div>
<div class="history-list" id="historyList"></div>
<button class="history-load-more" id="historyLoadMore" type="button" data-i18n="load_more" hidden>Load more</button>
</div>
</section>
</div>
<div class="whatsapp-modal" id="whatsappModal" hidden>
<div class="modal-backdrop" data-whatsapp-close></div>
<section class="modal-dialog whatsapp-dialog" role="dialog" aria-modal="true" aria-labelledby="whatsappTitle">
<div class="modal-head">
<h2 id="whatsappTitle" data-i18n="add_whatsapp_title">Add WhatsApp</h2>
<button class="modal-close" type="button" aria-label="Close" data-i18n-aria="close" data-whatsapp-close>&times;</button>
</div>
<form class="whatsapp-form" id="whatsappForm" autocomplete="off">
<div class="whatsapp-input-shell">
<span class="whatsapp-input-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6.2 18.2 4.8 21l3.2-1.1a8.2 8.2 0 1 0-1.8-1.7Z" />
<path d="M9.4 8.4c.3-.3.8-.3 1 .1l.7 1.2c.2.4.1.8-.2 1.1l-.4.3c.5 1 1.4 1.8 2.4 2.3l.4-.5c.3-.3.7-.4 1.1-.2l1.2.7c.4.2.5.7.2 1-.5.8-1.2 1.1-2.1.9-2.8-.6-5.1-2.8-5.8-5.6-.2-.8.2-1.5 1.5-1.3Z" />
</svg>
</span>
<input id="whatsappInput" type="tel" inputmode="tel" data-i18n-placeholder="whatsapp_placeholder" placeholder="Enter WhatsApp" />
</div>
<button id="saveWhatsappButton" type="submit" data-i18n="save">Save</button>
<div class="form-status" id="whatsappStatus" role="status" hidden></div>
</form>
</section>
</div>
<div class="home-indicator" aria-hidden="true"></div>
<div class="loading-mask" id="loadingMask" role="status" aria-live="polite" aria-busy="true">
<div class="runner-stick" aria-hidden="true">
@ -112,11 +200,11 @@
<span class="runner-leg runner-leg-back"></span>
<span class="runner-ground"></span>
</div>
<div class="loading-copy">loading....</div>
<div class="loading-copy" data-i18n="loading_mask">loading....</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
</div>
<script src="./script.js?v=20260429-0100" defer></script>
<script src="./script.js?v=20260430-0108" defer></script>
</body>
</html>

View File

@ -32,6 +32,44 @@
## APIs
### Get current user profile
`GET /team/member/profile`
Usage in new H5:
- Loads the current logged-in user's account card before the recharge flow.
- This is the same current-user profile source reused by the hyapp center pages.
Reusable fields:
- `id`
- `account`
- `userNickname`
- `name`
- `userAvatar`
- `ownSpecialId.account`
### Get current user coin balance
`GET /wallet/freight/balance`
Original Vue reference:
- The old recharge agency / coin seller page imports `a4` from the main bundle.
- `a4` maps to `dX()`, which calls `GET /wallet/freight/balance`.
- Original UI stores `body.toString()` and displays it under `my_coins`.
Usage in new H5:
- Displays the current user's Coins card.
- If the request fails, the page keeps showing `0` and the recharge flow still works.
Reusable fields:
- `body` as number
- `body.gold`
- `body.goldBalance`
- `body.coin`
- `body.coins`
- `body.balance`
- `body.amount`
### Search target user
`GET /user/user-profile/open-search?account=<account>`
@ -52,6 +90,55 @@ Reusable fields:
- `isFreightAgent`
- `isSuperFreightAgent`
### Recharge coins to target user
`POST /wallet/freight/ship`
Body:
```json
{
"acceptUserId": "<target user id>",
"quantity": 1000
}
```
Original Vue reference:
- The old coin seller page imports `a5` from the main bundle.
- `a5` maps to `mX(payload)`, which calls `POST /wallet/freight/ship`.
- Original submit logic sends `te({ acceptUserId: o, quantity: t })`.
- If the response has `body`, Vue uses it as the updated current coin balance.
Usage in new H5:
- After searching the target user, the page shows one coins input and a confirm recharge button.
- It no longer shows country selection, payment methods, or recharge packages for this direct transfer flow.
Reusable fields:
- `acceptUserId`: target user `id`
- `quantity`: entered coins amount
- response `body`: updated current user coin balance when returned
### Get transfer history
`GET /wallet/freight/balance/running/water/client/flowByUserId`
Query params:
- `userId=<current user id>`
- `type=0 | 1`
- `searchId=<searched user id or empty>`
- `lastId=<last record id for load more or empty>`
Original Vue reference:
- The seller records page imports `ad` as `Z` from the main bundle.
- `Z(params)` maps to `hX(params)`, which calls this endpoint.
- Original page uses `type=0` for `income` and `type=1` for `expenditure`.
- For each record, Vue displays `+quantity` when `record.type === 0`, otherwise `-quantity`.
- For user display, Vue reads `record.userAccount` for income records and `record.acceptUserAccount` for expenditure records.
Usage in new H5:
- The Coins card has a right-side History button.
- History opens in a center modal with Income / Expenditure tabs, user ID search, and load more using `lastId`.
### Get payment application
`GET /order/web/pay/application/{applicationId}`
@ -160,4 +247,5 @@ Reusable fields:
- New page lives at `h5/hyapp/recharge-center`.
- It keeps the existing hyapp center architecture: static `index.html`, `style.css`, `script.js`.
- It uses URL `token` for Java API authorization and sends the same H5 request headers used by the existing hyapp center pages.
- It merges the original `/recharge` and `/recharge-pay-way` flows into one page, so it does not depend on Vue router or Pinia `targetUser`.
- It uses the old coin seller direct-transfer API for the current UI: search target user, enter coins amount, and confirm recharge.
- The older `/recharge-pay-way` country / package / third-party payment flow is documented above for reference but is not rendered in the current H5 page.

File diff suppressed because it is too large Load Diff

View File

@ -189,8 +189,7 @@ button {
}
.search-card,
.country-card,
.payment-card {
.coin-recharge-card {
display: grid;
gap: 14px;
padding: 16px;
@ -238,6 +237,11 @@ button {
gap: 10px;
}
.coin-recharge-form {
display: grid;
gap: 12px;
}
.search-form input {
width: 100%;
min-width: 0;
@ -257,7 +261,46 @@ button {
box-shadow: 0 0 0 3px rgba(21, 189, 169, 0.12);
}
.coin-input-shell {
display: flex;
align-items: center;
gap: 10px;
min-height: 48px;
padding: 0 12px;
border: 1px solid #e4eeee;
border-radius: 8px;
background: #fbfdfd;
}
.coin-input-shell:focus-within {
border-color: rgba(21, 189, 169, 0.7);
box-shadow: 0 0 0 3px rgba(21, 189, 169, 0.12);
}
.input-coin {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
flex: 0 0 28px;
border-radius: 50%;
}
.coin-input-shell input {
width: 100%;
min-width: 0;
height: 46px;
border: 0;
outline: 0;
background: transparent;
color: #24282e;
font-size: 16px;
font-weight: 900;
}
.search-form button,
.coin-recharge-form button,
.package-button,
.mini-action {
min-height: 44px;
@ -274,7 +317,13 @@ button {
padding: 0 14px;
}
.coin-recharge-form button {
width: 100%;
padding: 0 14px;
}
.search-form button:disabled,
.coin-recharge-form button:disabled,
.package-button:disabled,
.mini-action:disabled {
opacity: 0.55;
@ -304,6 +353,121 @@ button {
padding: 16px;
}
.balance-card {
display: grid;
gap: 12px;
padding: 16px;
overflow: hidden;
background:
radial-gradient(circle at 88% 10%, rgba(75, 231, 216, 0.18), rgba(75, 231, 216, 0) 34%),
linear-gradient(145deg, #ffffff 0%, #f8fffd 100%);
}
.balance-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.balance-label {
color: #24282e;
font-size: 15px;
font-weight: 950;
line-height: 1.2;
}
.balance-icon {
display: flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
flex: 0 0 34px;
border-radius: 50%;
}
.history-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 34px;
padding: 0 10px;
border-radius: 999px;
background: #e7fbf8;
color: #0aa397;
font-size: 12px;
font-weight: 950;
}
.history-button svg {
width: 17px;
height: 17px;
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2.2;
}
.gold-coin-icon {
position: relative;
overflow: hidden;
border: 2px solid #f0ad17;
background:
radial-gradient(circle at 34% 28%, rgba(255, 255, 255, 0.72) 0 9%, rgba(255, 255, 255, 0) 25%),
linear-gradient(145deg, #ffe88a 0%, #ffc928 47%, #f4a800 100%);
box-shadow:
inset 0 -3px 0 rgba(174, 111, 0, 0.16),
inset 0 2px 0 rgba(255, 255, 255, 0.5);
}
.gold-coin-icon::before {
content: "";
position: absolute;
inset: 7px;
border: 2px solid rgba(174, 111, 0, 0.24);
border-radius: 50%;
}
.gold-coin-shine {
position: absolute;
top: 8px;
left: 10px;
width: 8px;
height: 4px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.72);
transform: rotate(-28deg);
}
.balance-value {
display: flex;
align-items: center;
gap: 9px;
min-width: 0;
color: #181d24;
}
.balance-coin {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
flex: 0 0 28px;
border-radius: 50%;
}
.balance-value strong {
min-width: 0;
overflow-wrap: anywhere;
font-size: 32px;
font-weight: 950;
line-height: 1.05;
}
.avatar-shell {
display: flex;
align-items: center;
@ -358,6 +522,10 @@ button {
white-space: nowrap;
}
.self-profile-card .name {
margin-top: 0;
}
.meta {
margin-top: 6px;
overflow: hidden;
@ -369,6 +537,50 @@ button {
white-space: nowrap;
}
.whatsapp-meta {
display: flex;
align-items: center;
gap: 8px;
}
.whatsapp-meta span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.whatsapp-icon,
.whatsapp-input-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
flex: 0 0 22px;
border-radius: 50%;
background: #e7fbf8;
color: #0aa397;
}
.whatsapp-icon svg,
.whatsapp-input-icon svg {
width: 16px;
height: 16px;
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 1.9;
}
.whatsapp-add-button {
color: #0aa397;
font-size: 13px;
font-weight: 950;
line-height: 1.2;
}
.mini-action {
min-width: 78px;
min-height: 36px;
@ -377,6 +589,14 @@ button {
font-size: 12px;
}
.mini-action-icon {
width: 36px;
min-width: 36px;
padding: 0;
font-size: 18px;
line-height: 1;
}
.country-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));
@ -513,9 +733,6 @@ button {
width: 32px;
height: 32px;
border-radius: 50%;
background: linear-gradient(145deg, #ffe694, #ffc93f);
color: #8b5d00;
font-weight: 950;
}
.package-content,
@ -548,7 +765,9 @@ button {
text-align: center;
}
.help-modal {
.help-modal,
.history-modal,
.whatsapp-modal {
position: fixed;
inset: 0;
z-index: 40;
@ -629,6 +848,280 @@ button {
line-height: 1.4;
}
.history-dialog {
width: min(100%, 390px);
}
.whatsapp-dialog {
width: min(100%, 360px);
}
.whatsapp-form {
display: grid;
gap: 12px;
padding: 0 16px 18px;
}
.whatsapp-input-shell {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
min-width: 0;
min-height: 44px;
border: 1px solid #e4eeee;
border-radius: 8px;
padding: 0 12px;
background: #fbfdfd;
}
.whatsapp-input-shell:focus-within {
border-color: rgba(21, 189, 169, 0.7);
box-shadow: 0 0 0 3px rgba(21, 189, 169, 0.12);
}
.whatsapp-form input {
width: 100%;
min-width: 0;
height: 42px;
border: 0;
outline: 0;
padding: 0;
background: transparent;
color: #24282e;
font-size: 14px;
font-weight: 850;
}
.whatsapp-form button {
min-height: 44px;
border-radius: 8px;
background: linear-gradient(135deg, #30dfb4, #15bda9);
color: #fff;
font-size: 14px;
font-weight: 950;
box-shadow: 0 8px 18px rgba(21, 189, 169, 0.24);
}
.whatsapp-form button:disabled {
opacity: 0.55;
}
.history-body {
display: grid;
gap: 12px;
overflow-y: auto;
padding: 0 16px 18px;
-webkit-overflow-scrolling: touch;
}
.history-search-form {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
gap: 8px;
}
.history-search-form input {
min-width: 0;
height: 40px;
border: 1px solid #e4eeee;
border-radius: 8px;
outline: 0;
padding: 0 10px;
background: #fbfdfd;
color: #24282e;
font-size: 13px;
font-weight: 800;
}
.history-search-form input:focus {
border-color: rgba(21, 189, 169, 0.7);
box-shadow: 0 0 0 3px rgba(21, 189, 169, 0.12);
}
.history-search-form button {
min-width: 56px;
min-height: 40px;
padding: 0 10px;
border-radius: 8px;
background: #e7fbf8;
color: #0aa397;
font-size: 12px;
font-weight: 950;
}
.history-search-form button[type="submit"] {
background: linear-gradient(135deg, #30dfb4, #15bda9);
color: #fff;
box-shadow: 0 8px 18px rgba(21, 189, 169, 0.18);
}
.history-tabs {
position: relative;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
padding: 4px;
border-radius: 8px;
background: #f3f7f7;
}
.history-tab {
min-height: 36px;
border-radius: 7px;
color: #7a7f87;
font-size: 13px;
font-weight: 950;
}
.history-tab.is-active {
background: #fff;
color: #0aa397;
box-shadow: 0 5px 14px rgba(22, 31, 42, 0.07);
}
.history-status {
padding: 10px 12px;
border-radius: 8px;
background: #f1fbf9;
color: #0b958a;
font-size: 12px;
font-weight: 850;
}
.history-status.is-error {
background: #fff3f3;
color: var(--danger);
}
.history-list {
display: grid;
gap: 10px;
}
.history-record {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
min-width: 0;
padding: 12px;
border: 1px solid #eef0f2;
border-radius: 8px;
background: #fbfcfc;
}
.history-record-user {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
flex: 1;
}
.history-avatar {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
width: 42px;
height: 42px;
flex: 0 0 42px;
border-radius: 50%;
background: linear-gradient(145deg, #d9fff5, #43e7d8);
color: #0b5f56;
font-size: 16px;
font-weight: 950;
}
.history-avatar img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.history-record-copy {
min-width: 0;
}
.history-record-name,
.history-record-account {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.history-record-name {
color: #25282e;
font-size: 14px;
font-weight: 950;
}
.history-record-account {
margin-top: 5px;
color: var(--muted);
font-size: 12px;
font-weight: 800;
}
.history-record-side {
display: grid;
justify-items: end;
gap: 5px;
min-width: 88px;
}
.history-record-amount {
color: #24282e;
font-size: 13px;
font-weight: 950;
text-align: right;
}
.history-record-amount.is-income {
color: #0aa397;
}
.history-record-amount.is-expense {
color: #e09a00;
}
.history-record-time {
color: var(--muted);
direction: ltr;
font-size: 11px;
font-weight: 750;
}
.history-empty {
display: grid;
justify-items: center;
gap: 6px;
padding: 32px 14px;
border-radius: 8px;
background: #f7fbfa;
color: #8b8d92;
font-size: 13px;
font-weight: 800;
text-align: center;
}
.history-empty strong {
color: #333840;
font-size: 15px;
font-weight: 950;
}
.history-load-more {
min-height: 40px;
border-radius: 8px;
background: #e7fbf8;
color: #0aa397;
font-size: 13px;
font-weight: 950;
}
.home-indicator {
width: 134px;
height: 5px;
@ -772,6 +1265,14 @@ body.modal-open {
text-align: right;
}
[dir="rtl"] .history-record-side {
justify-items: start;
}
[dir="rtl"] .history-record-amount {
text-align: left;
}
@keyframes runner-bob {
0%,
100% {

View File

@ -33,8 +33,10 @@ const deps = {
const state = {
applicationId: defaults.applicationId,
productType: defaults.productType,
application: null,
userProfile: null,
countries: [],
regionId: "",
selectedCountryId: "",
commodityCard: null,
selectedGoodsId: "",
@ -112,6 +114,20 @@ function currentCountryId() {
return state.selectedCountryId;
}
function currentRegionId() {
return state.regionId;
}
function currentUserAccount() {
return String(
state.userProfile?.account ||
state.userProfile?.actualAccount ||
state.userProfile?.ownSpecialId?.account ||
state.userProfile?.specialId?.account ||
""
).trim();
}
function getCountryLabel(item) {
return (
item?.label ||
@ -311,8 +327,36 @@ async function refreshMeta() {
persistSettings();
els.applicationId.value = state.applicationId;
const countriesResp = await deps.getApi("/order/web/pay/country");
state.countries = Array.isArray(countriesResp?.body) ? countriesResp.body : [];
const applicationResp = await deps.getApi(
`/order/web/pay/application/${encodeURIComponent(state.applicationId)}`
);
state.application = applicationResp?.body || {};
const profileQuery = new URLSearchParams({
sysOrigin: state.application?.appCode || state.userProfile?.originSys || "",
type: state.productType,
});
const account = currentUserAccount();
if (account) {
profileQuery.set("account", account);
}
if (currentUserId()) {
profileQuery.set("userId", currentUserId());
}
const profileResp = await deps.getApi(
`/order/web/pay/user-profile?${profileQuery.toString()}`
);
const payProfile = profileResp?.body || {};
if (payProfile.userProfile) {
state.userProfile = {
...state.userProfile,
...payProfile.userProfile,
};
}
state.regionId = payProfile.regionId || "";
state.countries = Array.isArray(payProfile.countryList) ? payProfile.countryList : [];
if (!state.countries.length) {
state.selectedCountryId = "";
state.commodityCard = null;
@ -335,7 +379,7 @@ async function refreshMeta() {
state.applicationId
)}&payCountryId=${encodeURIComponent(currentCountryId())}&type=${encodeURIComponent(
state.productType
)}`
)}&regionId=${encodeURIComponent(currentRegionId())}`
);
state.commodityCard = commodityResp?.body || {};