1556 lines
62 KiB
HTML
1556 lines
62 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta
|
||
name="viewport"
|
||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
||
/>
|
||
<title>Recharge</title>
|
||
<link rel="stylesheet" href="../common/theme.css" />
|
||
<style>
|
||
* {
|
||
box-sizing: border-box;
|
||
-webkit-tap-highlight-color: transparent;
|
||
}
|
||
|
||
html,
|
||
body {
|
||
width: 100%;
|
||
min-height: 100%;
|
||
margin: 0;
|
||
background: var(--hy-theme-bg);
|
||
color: var(--hy-theme-text);
|
||
font-family:
|
||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Arial,
|
||
sans-serif;
|
||
}
|
||
|
||
body {
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
button,
|
||
input {
|
||
font: inherit;
|
||
}
|
||
|
||
button {
|
||
border: 0;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.page {
|
||
width: min(100%, 520px);
|
||
min-height: 100vh;
|
||
margin: 0 auto;
|
||
padding: max(18px, env(safe-area-inset-top)) 16px
|
||
max(24px, env(safe-area-inset-bottom));
|
||
}
|
||
|
||
.hero {
|
||
position: relative;
|
||
display: grid;
|
||
min-height: 136px;
|
||
align-items: end;
|
||
padding: 22px;
|
||
border: 1px solid var(--hy-theme-line);
|
||
border-radius: 22px;
|
||
background: var(--hy-theme-hero);
|
||
box-shadow: var(--hy-theme-shadow);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.hero::after {
|
||
content: '';
|
||
position: absolute;
|
||
right: 22px;
|
||
top: 20px;
|
||
width: 68px;
|
||
height: 68px;
|
||
border-radius: 24px;
|
||
background:
|
||
linear-gradient(
|
||
145deg,
|
||
rgba(255, 255, 255, 0.86),
|
||
rgba(255, 255, 255, 0.18)
|
||
),
|
||
var(--hy-theme-primary);
|
||
box-shadow: 0 12px 28px rgba(125, 87, 199, 0.14);
|
||
transform: rotate(9deg);
|
||
}
|
||
|
||
.hero h1,
|
||
.hero p {
|
||
position: relative;
|
||
z-index: 2;
|
||
margin: 0;
|
||
}
|
||
|
||
.hero h1 {
|
||
max-width: 72%;
|
||
font-size: 30px;
|
||
line-height: 1.08;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.hero p {
|
||
max-width: 86%;
|
||
margin-top: 9px;
|
||
color: var(--hy-theme-muted);
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.language-switcher {
|
||
position: absolute;
|
||
right: 14px;
|
||
top: 14px;
|
||
z-index: 10;
|
||
}
|
||
|
||
.language-button {
|
||
min-width: 56px;
|
||
height: 34px;
|
||
padding: 0 12px;
|
||
border: 1px solid var(--hy-theme-line);
|
||
border-radius: 17px;
|
||
background: var(--hy-theme-surface);
|
||
color: var(--hy-theme-text);
|
||
font-weight: 800;
|
||
box-shadow: var(--hy-theme-shadow);
|
||
}
|
||
|
||
.language-menu {
|
||
position: absolute;
|
||
right: 0;
|
||
top: 40px;
|
||
width: 128px;
|
||
padding: 6px;
|
||
border: 1px solid var(--hy-theme-line);
|
||
border-radius: 16px;
|
||
background: var(--hy-theme-surface);
|
||
box-shadow: var(--hy-theme-shadow);
|
||
}
|
||
|
||
.language-menu button {
|
||
width: 100%;
|
||
min-height: 34px;
|
||
border-radius: 12px;
|
||
background: transparent;
|
||
color: var(--hy-theme-text);
|
||
text-align: left;
|
||
}
|
||
|
||
[dir='rtl'] .language-menu button {
|
||
text-align: right;
|
||
}
|
||
|
||
.language-menu button.is-active {
|
||
background: var(--hy-theme-primary-soft);
|
||
color: var(--hy-theme-primary-deep);
|
||
font-weight: 800;
|
||
}
|
||
|
||
.section {
|
||
margin-top: 16px;
|
||
padding: 16px;
|
||
border: 1px solid var(--hy-theme-line);
|
||
border-radius: 20px;
|
||
background: var(--hy-theme-surface);
|
||
box-shadow: var(--hy-theme-shadow);
|
||
}
|
||
|
||
.view {
|
||
display: none;
|
||
}
|
||
|
||
.view.is-active {
|
||
display: block;
|
||
}
|
||
|
||
.section-title {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.section-title h2 {
|
||
margin: 0;
|
||
font-size: 17px;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.section-title span {
|
||
color: var(--hy-theme-muted);
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.account-search {
|
||
display: grid;
|
||
gap: 10px;
|
||
grid-template-columns: minmax(0, 1fr) auto;
|
||
}
|
||
|
||
.input {
|
||
width: 100%;
|
||
height: 46px;
|
||
padding: 0 14px;
|
||
border: 1px solid var(--hy-theme-line);
|
||
border-radius: 14px;
|
||
background: var(--hy-theme-surface-soft);
|
||
color: var(--hy-theme-text);
|
||
outline: none;
|
||
}
|
||
|
||
.input:focus {
|
||
border-color: var(--hy-theme-primary-strong);
|
||
box-shadow: 0 0 0 3px rgba(197, 166, 246, 0.18);
|
||
}
|
||
|
||
.primary-button,
|
||
.secondary-button {
|
||
display: inline-flex;
|
||
min-height: 46px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0 16px;
|
||
border-radius: 14px;
|
||
font-weight: 850;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.primary-button {
|
||
background: var(--hy-theme-button);
|
||
color: #fff;
|
||
box-shadow: var(--hy-theme-button-shadow);
|
||
}
|
||
|
||
.secondary-button {
|
||
border: 1px solid var(--hy-theme-line);
|
||
background: var(--hy-theme-surface-soft);
|
||
color: var(--hy-theme-primary-deep);
|
||
}
|
||
|
||
.primary-button:disabled,
|
||
.secondary-button:disabled {
|
||
background: var(--hy-theme-button-disabled);
|
||
box-shadow: var(--hy-theme-button-disabled-shadow);
|
||
color: var(--hy-theme-button-disabled-text);
|
||
cursor: default;
|
||
}
|
||
|
||
.account-card {
|
||
display: none;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px;
|
||
border-radius: 16px;
|
||
background: var(--hy-theme-primary-soft);
|
||
}
|
||
|
||
.account-card.is-visible {
|
||
display: flex;
|
||
}
|
||
|
||
.avatar {
|
||
width: 48px;
|
||
height: 48px;
|
||
flex: 0 0 auto;
|
||
overflow: hidden;
|
||
border-radius: 16px;
|
||
background: var(--hy-theme-primary);
|
||
}
|
||
|
||
.avatar img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.avatar span {
|
||
display: grid;
|
||
width: 100%;
|
||
height: 100%;
|
||
place-items: center;
|
||
color: var(--hy-theme-primary-deep);
|
||
font-size: 22px;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.account-main {
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.account-name,
|
||
.account-meta {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.account-name {
|
||
font-weight: 850;
|
||
}
|
||
|
||
.account-meta {
|
||
margin-top: 4px;
|
||
color: var(--hy-theme-muted);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.role-pill {
|
||
min-width: 76px;
|
||
padding: 7px 10px;
|
||
border-radius: 999px;
|
||
background: var(--hy-theme-surface);
|
||
color: var(--hy-theme-primary-deep);
|
||
font-size: 12px;
|
||
font-weight: 850;
|
||
text-align: center;
|
||
}
|
||
|
||
.grid {
|
||
display: grid;
|
||
gap: 10px;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.choice {
|
||
position: relative;
|
||
min-height: 98px;
|
||
padding: 14px;
|
||
border: 1px solid var(--hy-theme-line);
|
||
border-radius: 18px;
|
||
background: var(--hy-theme-surface-soft);
|
||
color: var(--hy-theme-text);
|
||
text-align: left;
|
||
}
|
||
|
||
.choice.is-selected {
|
||
border-color: var(--hy-theme-primary-strong);
|
||
background: var(--hy-theme-primary-soft);
|
||
box-shadow: inset 0 0 0 1px var(--hy-theme-primary-strong);
|
||
}
|
||
|
||
.choice strong,
|
||
.choice span {
|
||
display: block;
|
||
}
|
||
|
||
.choice strong {
|
||
font-size: 18px;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.choice span {
|
||
margin-top: 7px;
|
||
color: var(--hy-theme-muted);
|
||
font-size: 13px;
|
||
line-height: 1.35;
|
||
}
|
||
|
||
.detail-top {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.back-button {
|
||
width: 46px;
|
||
min-width: 46px;
|
||
padding: 0;
|
||
font-size: 24px;
|
||
line-height: 1;
|
||
}
|
||
|
||
.detail-title {
|
||
min-width: 0;
|
||
margin: 0;
|
||
font-size: 20px;
|
||
line-height: 1.25;
|
||
}
|
||
|
||
.selected-summary {
|
||
min-height: auto;
|
||
margin-top: 14px;
|
||
cursor: default;
|
||
}
|
||
|
||
.method-list {
|
||
display: grid;
|
||
gap: 10px;
|
||
}
|
||
|
||
.method {
|
||
display: grid;
|
||
min-height: 78px;
|
||
align-items: center;
|
||
gap: 12px;
|
||
grid-template-columns: 44px minmax(0, 1fr) auto;
|
||
padding: 12px;
|
||
border: 1px solid var(--hy-theme-line);
|
||
border-radius: 18px;
|
||
background: var(--hy-theme-surface-soft);
|
||
color: var(--hy-theme-text);
|
||
text-align: left;
|
||
}
|
||
|
||
.method.is-selected {
|
||
border-color: var(--hy-theme-primary-strong);
|
||
background: var(--hy-theme-primary-soft);
|
||
}
|
||
|
||
.method-icon {
|
||
display: grid;
|
||
width: 44px;
|
||
height: 44px;
|
||
place-items: center;
|
||
border-radius: 15px;
|
||
background: var(--hy-theme-surface);
|
||
color: var(--hy-theme-primary-deep);
|
||
font-size: 14px;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.method-main {
|
||
min-width: 0;
|
||
}
|
||
|
||
.method-name,
|
||
.method-meta {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.method-name {
|
||
font-weight: 850;
|
||
}
|
||
|
||
.method-meta {
|
||
margin-top: 4px;
|
||
color: var(--hy-theme-muted);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.method-amount {
|
||
color: var(--hy-theme-primary-deep);
|
||
font-size: 13px;
|
||
font-weight: 850;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.order-panel {
|
||
display: none;
|
||
gap: 12px;
|
||
}
|
||
|
||
.order-panel.is-visible {
|
||
display: grid;
|
||
}
|
||
|
||
.order-row {
|
||
display: flex;
|
||
min-width: 0;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
color: var(--hy-theme-muted);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.order-row strong {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
color: var(--hy-theme-text);
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.address-box {
|
||
display: grid;
|
||
gap: 10px;
|
||
padding: 12px;
|
||
border-radius: 16px;
|
||
background: var(--hy-theme-primary-soft);
|
||
}
|
||
|
||
.address-value {
|
||
overflow-wrap: anywhere;
|
||
color: var(--hy-theme-text);
|
||
font-size: 13px;
|
||
font-weight: 800;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.tx-form {
|
||
display: grid;
|
||
gap: 10px;
|
||
}
|
||
|
||
.modal-backdrop {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 100;
|
||
display: grid;
|
||
align-items: end;
|
||
padding: 16px;
|
||
background: rgba(34, 29, 46, 0.5);
|
||
}
|
||
|
||
.modal {
|
||
width: min(100%, 520px);
|
||
max-height: min(82vh, 680px);
|
||
margin: 0 auto;
|
||
overflow: auto;
|
||
border: 1px solid var(--hy-theme-line);
|
||
border-radius: 22px;
|
||
background: var(--hy-theme-surface);
|
||
box-shadow: 0 22px 54px rgba(40, 25, 72, 0.26);
|
||
}
|
||
|
||
.modal-header {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
padding: 16px;
|
||
border-bottom: 1px solid var(--hy-theme-line);
|
||
background: var(--hy-theme-surface);
|
||
}
|
||
|
||
.modal-header h2 {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 16px;
|
||
}
|
||
|
||
.modal-actions {
|
||
display: grid;
|
||
gap: 10px;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.empty,
|
||
.status-line {
|
||
color: var(--hy-theme-muted);
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.status-line {
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.hidden {
|
||
display: none !important;
|
||
}
|
||
|
||
@media (max-width: 390px) {
|
||
.page {
|
||
padding-right: 12px;
|
||
padding-left: 12px;
|
||
}
|
||
|
||
.hero h1 {
|
||
font-size: 27px;
|
||
}
|
||
|
||
.grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.account-search {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main class="page" aria-labelledby="page-title">
|
||
<div id="package-view" class="view is-active">
|
||
<section class="hero">
|
||
<div class="language-switcher">
|
||
<button
|
||
class="language-button"
|
||
type="button"
|
||
data-language-toggle
|
||
data-i18n-aria="recharge.changeLanguage"
|
||
aria-expanded="false"
|
||
>
|
||
<span data-current-lang>EN</span>
|
||
</button>
|
||
<div class="language-menu" data-language-menu hidden>
|
||
<button type="button" data-lang-option="en">
|
||
English
|
||
</button>
|
||
<button type="button" data-lang-option="zh">
|
||
中文
|
||
</button>
|
||
<button type="button" data-lang-option="ar">
|
||
العربية
|
||
</button>
|
||
<button type="button" data-lang-option="tr">
|
||
Türkçe
|
||
</button>
|
||
<button type="button" data-lang-option="es">
|
||
Español
|
||
</button>
|
||
<button type="button" data-lang-option="id">
|
||
Indonesia
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<h1 id="page-title" data-i18n="recharge.pageTitle">
|
||
Recharge
|
||
</h1>
|
||
<p data-i18n="recharge.pageSubtitle">
|
||
Choose a coin package and pay with USDT-TRC20 or a
|
||
supported local payment method.
|
||
</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section" id="account-section">
|
||
<div class="section-title">
|
||
<h2 data-i18n="recharge.accountTitle">Account</h2>
|
||
<span id="auth-mode"></span>
|
||
</div>
|
||
<div id="account-search" class="account-search">
|
||
<input
|
||
id="display-user-id"
|
||
class="input"
|
||
autocomplete="off"
|
||
inputmode="numeric"
|
||
data-i18n-placeholder="recharge.userIdPlaceholder"
|
||
placeholder="Enter user ID"
|
||
/>
|
||
<button
|
||
id="search-account"
|
||
class="primary-button"
|
||
type="button"
|
||
data-i18n="recharge.searchAccount"
|
||
>
|
||
Confirm
|
||
</button>
|
||
</div>
|
||
<div id="account-card" class="account-card">
|
||
<div class="avatar" id="account-avatar"></div>
|
||
<div class="account-main">
|
||
<div class="account-name" id="account-name">-</div>
|
||
<div class="account-meta" id="account-meta">-</div>
|
||
</div>
|
||
<div class="role-pill" id="account-role">-</div>
|
||
</div>
|
||
<div
|
||
id="account-status"
|
||
class="status-line"
|
||
data-i18n="recharge.accountHint"
|
||
>
|
||
Confirm the account before selecting a package.
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section">
|
||
<div class="section-title">
|
||
<h2 data-i18n="recharge.productTitle">Package</h2>
|
||
<span id="product-count"></span>
|
||
</div>
|
||
<div id="product-list" class="grid"></div>
|
||
<div
|
||
id="product-empty"
|
||
class="empty"
|
||
data-i18n="recharge.productEmpty"
|
||
>
|
||
No package is available for this account.
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<div id="payment-view" class="view">
|
||
<div class="detail-top">
|
||
<button
|
||
id="back-to-products"
|
||
class="secondary-button back-button"
|
||
type="button"
|
||
data-i18n-aria="recharge.backToPackages"
|
||
aria-label="Back"
|
||
>
|
||
‹
|
||
</button>
|
||
<h1 class="detail-title" data-i18n="recharge.paymentTitle">
|
||
Payment
|
||
</h1>
|
||
</div>
|
||
|
||
<section class="section">
|
||
<div class="section-title">
|
||
<h2 data-i18n="recharge.selectedPackage">
|
||
Selected package
|
||
</h2>
|
||
</div>
|
||
<div id="selected-product-card" class="choice selected-summary"></div>
|
||
</section>
|
||
|
||
<section class="section">
|
||
<div class="section-title">
|
||
<h2 data-i18n="recharge.paymentTitle">Payment</h2>
|
||
<span id="method-count"></span>
|
||
</div>
|
||
<div id="method-list" class="method-list"></div>
|
||
<div
|
||
id="method-empty"
|
||
class="empty"
|
||
data-i18n="recharge.noPaymentMethods"
|
||
>
|
||
No payment method is available.
|
||
</div>
|
||
<button
|
||
id="create-order"
|
||
class="primary-button"
|
||
type="button"
|
||
disabled
|
||
data-i18n="recharge.createOrder"
|
||
style="width: 100%; margin-top: 12px"
|
||
>
|
||
Create order
|
||
</button>
|
||
</section>
|
||
</div>
|
||
|
||
<div id="order-modal" class="modal-backdrop hidden" role="dialog" aria-modal="true">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h2 data-i18n="recharge.paymentOperation">
|
||
Payment operation
|
||
</h2>
|
||
<button
|
||
id="close-order-modal"
|
||
class="secondary-button"
|
||
type="button"
|
||
data-i18n="recharge.close"
|
||
>
|
||
Close
|
||
</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<section id="order-panel" class="order-panel">
|
||
<div class="section-title">
|
||
<h2 data-i18n="recharge.orderTitle">Order</h2>
|
||
<span id="order-status"></span>
|
||
</div>
|
||
<div class="order-row">
|
||
<span data-i18n="recharge.orderId">Order ID</span>
|
||
<strong id="order-id">-</strong>
|
||
</div>
|
||
<div class="order-row">
|
||
<span data-i18n="recharge.orderStatus">Status</span>
|
||
<strong id="order-status-text">-</strong>
|
||
</div>
|
||
<div id="usdt-panel" class="hidden">
|
||
<div class="address-box">
|
||
<span data-i18n="recharge.usdtAddress">
|
||
USDT-TRC20 address
|
||
</span>
|
||
<div class="address-value" id="receive-address">-</div>
|
||
<button
|
||
id="copy-address"
|
||
class="secondary-button"
|
||
type="button"
|
||
data-i18n="recharge.copy"
|
||
>
|
||
Copy
|
||
</button>
|
||
</div>
|
||
<form id="tx-form" class="tx-form">
|
||
<input
|
||
id="tx-hash"
|
||
class="input"
|
||
autocomplete="off"
|
||
data-i18n-placeholder="recharge.txHashPlaceholder"
|
||
placeholder="Enter TXID after payment"
|
||
/>
|
||
<button
|
||
id="submit-tx"
|
||
class="primary-button"
|
||
type="submit"
|
||
data-i18n="recharge.submitTx"
|
||
>
|
||
Submit TXID
|
||
</button>
|
||
</form>
|
||
</div>
|
||
<div id="mifapay-panel" class="modal-actions hidden">
|
||
<button
|
||
id="open-payment-url"
|
||
class="primary-button"
|
||
type="button"
|
||
data-i18n="recharge.openPayment"
|
||
>
|
||
Open payment
|
||
</button>
|
||
</div>
|
||
<div id="order-message" class="status-line"></div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<script src="../common/api.js"></script>
|
||
<script src="../common/params.js"></script>
|
||
<script src="../common/i18n.js"></script>
|
||
<script src="../common/toast.js"></script>
|
||
<script>
|
||
(function () {
|
||
var state = {
|
||
context: null,
|
||
options: null,
|
||
activeView: 'packages',
|
||
selectedProductID: 0,
|
||
selectedMethodKey: '',
|
||
order: null,
|
||
orderModalOpen: false,
|
||
pollingTimer: 0,
|
||
loading: false,
|
||
};
|
||
|
||
var nodes = {};
|
||
|
||
// 页面只依赖公共 api/params/i18n/theme:token、app_code、env 都交给 common 层解析。
|
||
// 初始化阶段只决定入口模式,带 token 直接查本人,不带 token 才允许输入 display_user_id。
|
||
function init() {
|
||
nodes = {
|
||
packageView: document.getElementById('package-view'),
|
||
paymentView: document.getElementById('payment-view'),
|
||
backToProducts:
|
||
document.getElementById('back-to-products'),
|
||
accountSearch:
|
||
document.getElementById('account-search'),
|
||
accountCard: document.getElementById('account-card'),
|
||
accountAvatar:
|
||
document.getElementById('account-avatar'),
|
||
accountName: document.getElementById('account-name'),
|
||
accountMeta: document.getElementById('account-meta'),
|
||
accountRole: document.getElementById('account-role'),
|
||
accountStatus:
|
||
document.getElementById('account-status'),
|
||
authMode: document.getElementById('auth-mode'),
|
||
displayUserID:
|
||
document.getElementById('display-user-id'),
|
||
searchAccount:
|
||
document.getElementById('search-account'),
|
||
productList: document.getElementById('product-list'),
|
||
productEmpty: document.getElementById('product-empty'),
|
||
productCount: document.getElementById('product-count'),
|
||
selectedProductCard: document.getElementById(
|
||
'selected-product-card'
|
||
),
|
||
methodList: document.getElementById('method-list'),
|
||
methodEmpty: document.getElementById('method-empty'),
|
||
methodCount: document.getElementById('method-count'),
|
||
createOrder: document.getElementById('create-order'),
|
||
orderModal: document.getElementById('order-modal'),
|
||
closeOrderModal:
|
||
document.getElementById('close-order-modal'),
|
||
orderPanel: document.getElementById('order-panel'),
|
||
orderID: document.getElementById('order-id'),
|
||
orderStatus: document.getElementById('order-status'),
|
||
orderStatusText:
|
||
document.getElementById('order-status-text'),
|
||
orderMessage: document.getElementById('order-message'),
|
||
usdtPanel: document.getElementById('usdt-panel'),
|
||
mifapayPanel:
|
||
document.getElementById('mifapay-panel'),
|
||
openPaymentURL:
|
||
document.getElementById('open-payment-url'),
|
||
receiveAddress:
|
||
document.getElementById('receive-address'),
|
||
copyAddress: document.getElementById('copy-address'),
|
||
txForm: document.getElementById('tx-form'),
|
||
txHash: document.getElementById('tx-hash'),
|
||
submitTx: document.getElementById('submit-tx'),
|
||
};
|
||
|
||
bindEvents();
|
||
var current = window.HyAppParams
|
||
? window.HyAppParams.parse()
|
||
: { raw: new URLSearchParams(window.location.search) };
|
||
var raw = current.raw || new URLSearchParams();
|
||
var displayUserID = raw.get('display_user_id') || '';
|
||
nodes.displayUserID.value = displayUserID;
|
||
if (current.token) {
|
||
// token 模式必须隐藏账号输入,目标用户只能由服务端鉴权结果决定,不能让前端改充值对象。
|
||
nodes.accountSearch.classList.add('hidden');
|
||
loadContext('');
|
||
} else if (displayUserID) {
|
||
// 链接预填 display_user_id 时自动确认一次,适合客服或分享充值链接直接带目标账号。
|
||
loadContext(displayUserID);
|
||
} else {
|
||
render();
|
||
}
|
||
}
|
||
|
||
// 所有用户动作集中绑定,异步入口统一进入 load/create/submit,避免多个按钮各自拼接口参数。
|
||
function bindEvents() {
|
||
nodes.searchAccount.addEventListener('click', function () {
|
||
loadContext(nodes.displayUserID.value);
|
||
});
|
||
nodes.backToProducts.addEventListener('click', function () {
|
||
state.activeView = 'packages';
|
||
state.selectedMethodKey = '';
|
||
state.orderModalOpen = false;
|
||
render();
|
||
scrollToTop();
|
||
});
|
||
nodes.createOrder.addEventListener('click', createOrder);
|
||
nodes.closeOrderModal.addEventListener(
|
||
'click',
|
||
closeOrderModal
|
||
);
|
||
nodes.orderModal.addEventListener('click', function (event) {
|
||
if (event.target === nodes.orderModal) {
|
||
closeOrderModal();
|
||
}
|
||
});
|
||
nodes.openPaymentURL.addEventListener('click', function () {
|
||
if (state.order && state.order.pay_url) {
|
||
window.location.href = state.order.pay_url;
|
||
}
|
||
});
|
||
nodes.copyAddress.addEventListener('click', copyAddress);
|
||
nodes.txForm.addEventListener('submit', submitTx);
|
||
window.addEventListener('hyapp:i18n-ready', function () {
|
||
document.title = t('recharge.pageTitle', 'Recharge');
|
||
render();
|
||
});
|
||
}
|
||
|
||
// 账号确认是后续商品、支付方式、钱包入账目标的唯一前置事实。
|
||
// 无 token 时必须提供 display_user_id;有 token 时 display_user_id 置空,后端只认 token 用户。
|
||
function loadContext(displayUserID) {
|
||
displayUserID = String(displayUserID || '').trim();
|
||
if (!window.HyAppAPI.getAccessToken() && !displayUserID) {
|
||
toast(t('recharge.accountRequired', 'Enter user ID'));
|
||
return;
|
||
}
|
||
setBusy(true);
|
||
setStatus(
|
||
nodes.accountStatus,
|
||
t('recharge.loading', 'Loading...')
|
||
);
|
||
window.HyAppAPI.recharge
|
||
.context(displayUserID)
|
||
.then(function (context) {
|
||
// context 只返回安全账号信息和 audience_type;前端不读取手机号、邮箱、余额等敏感字段。
|
||
state.context = context;
|
||
return window.HyAppAPI.recharge.options(
|
||
context.auth_mode === 'token'
|
||
? ''
|
||
: context.account.display_user_id
|
||
);
|
||
})
|
||
.then(function (options) {
|
||
// 商品和支付方式必须跟随已确认账号重新加载,切换账号后清空旧选择和旧订单。
|
||
state.options = options;
|
||
state.activeView = 'packages';
|
||
state.selectedProductID = 0;
|
||
state.selectedMethodKey = '';
|
||
state.order = null;
|
||
state.orderModalOpen = false;
|
||
render();
|
||
})
|
||
.catch(function (error) {
|
||
toast(
|
||
error.message ||
|
||
t('recharge.loadFailed', 'Load failed')
|
||
);
|
||
setStatus(
|
||
nodes.accountStatus,
|
||
t('recharge.loadFailed', 'Load failed')
|
||
);
|
||
})
|
||
.finally(function () {
|
||
setBusy(false);
|
||
});
|
||
}
|
||
|
||
// render 只消费 state,不发请求;所有接口副作用集中在动作函数里,方便排查状态流转。
|
||
function render() {
|
||
renderAccount();
|
||
renderProducts();
|
||
renderViews();
|
||
renderSelectedProduct();
|
||
renderMethods();
|
||
renderOrder();
|
||
}
|
||
|
||
// 账号卡只展示后端允许公开的账号确认信息;受众文案直接来自 audience_type。
|
||
function renderAccount() {
|
||
var account = state.context && state.context.account;
|
||
nodes.authMode.textContent = state.context
|
||
? state.context.auth_mode === 'token'
|
||
? 'Token'
|
||
: 'ID'
|
||
: '';
|
||
nodes.accountCard.classList.toggle('is-visible', !!account);
|
||
if (!account) return;
|
||
|
||
nodes.accountAvatar.innerHTML = account.avatar
|
||
? '<img src="' +
|
||
escapeAttr(account.avatar) +
|
||
'" alt="" />'
|
||
: '<span>' +
|
||
escapeHTML(firstLetter(account.username)) +
|
||
'</span>';
|
||
nodes.accountName.textContent =
|
||
account.username ||
|
||
account.display_user_id ||
|
||
t('recharge.accountConfirmed', 'Confirmed account');
|
||
nodes.accountMeta.textContent = [
|
||
account.display_user_id,
|
||
account.country_name || account.country_code,
|
||
account.region_name || account.region_code,
|
||
]
|
||
.filter(Boolean)
|
||
.join(' · ');
|
||
nodes.accountRole.textContent =
|
||
account.audience_type === 'coin_seller'
|
||
? t('recharge.coinSeller', 'Coin seller')
|
||
: t('recharge.ordinaryUser', 'User');
|
||
setStatus(
|
||
nodes.accountStatus,
|
||
t('recharge.accountConfirmed', 'Account confirmed')
|
||
);
|
||
}
|
||
|
||
// 商品列表由后端按 normal / coin_seller 和用户区域过滤,前端只负责单选交互。
|
||
function renderProducts() {
|
||
var products =
|
||
(state.options && state.options.products) || [];
|
||
nodes.productList.innerHTML = '';
|
||
nodes.productCount.textContent = products.length
|
||
? String(products.length)
|
||
: '';
|
||
nodes.productEmpty.classList.toggle(
|
||
'hidden',
|
||
products.length > 0
|
||
);
|
||
products.forEach(function (product) {
|
||
var button = document.createElement('button');
|
||
button.type = 'button';
|
||
button.className =
|
||
'choice' +
|
||
(product.product_id === state.selectedProductID
|
||
? ' is-selected'
|
||
: '');
|
||
button.innerHTML =
|
||
'<strong>' +
|
||
escapeHTML(formatCoins(product.coin_amount)) +
|
||
' ' +
|
||
escapeHTML(t('recharge.coins', 'coins')) +
|
||
'</strong><span>' +
|
||
escapeHTML(formatUSD(product)) +
|
||
'</span>';
|
||
button.addEventListener('click', function () {
|
||
state.selectedProductID = product.product_id;
|
||
// 用户点击档位后进入支付页,后续支付方式和订单都绑定这个档位快照。
|
||
// 切换档位必须清空支付方式和旧订单,避免把前一个档位的金额带入新订单。
|
||
state.activeView = 'payment';
|
||
state.selectedMethodKey = '';
|
||
state.order = null;
|
||
state.orderModalOpen = false;
|
||
render();
|
||
scrollToTop();
|
||
});
|
||
nodes.productList.appendChild(button);
|
||
});
|
||
}
|
||
|
||
// 两个页面状态只由 activeView 和当前商品决定;商品消失时自动退回档位页,避免空支付页。
|
||
function renderViews() {
|
||
var showPayment =
|
||
state.activeView === 'payment' && !!selectedProduct();
|
||
if (!showPayment && state.activeView === 'payment') {
|
||
state.activeView = 'packages';
|
||
}
|
||
nodes.packageView.classList.toggle(
|
||
'is-active',
|
||
state.activeView !== 'payment'
|
||
);
|
||
nodes.paymentView.classList.toggle(
|
||
'is-active',
|
||
state.activeView === 'payment'
|
||
);
|
||
}
|
||
|
||
// 支付页顶部固定展示选中的档位,用户进入下一步后还能明确当前订单金额和金币数。
|
||
function renderSelectedProduct() {
|
||
var product = selectedProduct();
|
||
if (!product) {
|
||
nodes.selectedProductCard.innerHTML = '';
|
||
return;
|
||
}
|
||
nodes.selectedProductCard.innerHTML =
|
||
'<strong>' +
|
||
escapeHTML(formatCoins(product.coin_amount)) +
|
||
' ' +
|
||
escapeHTML(t('recharge.coins', 'coins')) +
|
||
'</strong><span>' +
|
||
escapeHTML(formatUSD(product)) +
|
||
'</span>';
|
||
}
|
||
|
||
// 支付方式展示由当前商品驱动:USDT 用订单美元金额,MiFaPay 用后台汇率换算本币金额。
|
||
function renderMethods() {
|
||
var methods = availableMethods();
|
||
nodes.methodList.innerHTML = '';
|
||
nodes.methodCount.textContent = methods.length
|
||
? String(methods.length)
|
||
: '';
|
||
nodes.methodEmpty.classList.toggle(
|
||
'hidden',
|
||
methods.length > 0
|
||
);
|
||
nodes.methodEmpty.textContent = state.selectedProductID
|
||
? t('recharge.noPaymentMethods', 'No payment method')
|
||
: t(
|
||
'recharge.selectProductFirst',
|
||
'Select a package first'
|
||
);
|
||
methods.forEach(function (method) {
|
||
var button = document.createElement('button');
|
||
button.type = 'button';
|
||
button.className =
|
||
'method' +
|
||
(method.key === state.selectedMethodKey
|
||
? ' is-selected'
|
||
: '');
|
||
button.innerHTML =
|
||
'<span class="method-icon">' +
|
||
escapeHTML(method.icon) +
|
||
'</span><span class="method-main"><span class="method-name">' +
|
||
escapeHTML(method.name) +
|
||
'</span><span class="method-meta">' +
|
||
escapeHTML(method.meta) +
|
||
'</span></span><span class="method-amount">' +
|
||
escapeHTML(method.amount) +
|
||
'</span>';
|
||
button.addEventListener('click', function () {
|
||
state.selectedMethodKey = method.key;
|
||
renderMethods();
|
||
});
|
||
nodes.methodList.appendChild(button);
|
||
});
|
||
nodes.createOrder.disabled =
|
||
!state.selectedProductID ||
|
||
!state.selectedMethodKey ||
|
||
state.loading;
|
||
}
|
||
|
||
// 订单弹窗是支付后的唯一操作入口;MiFaPay 展示打开支付按钮,USDT 展示地址和 txid 表单。
|
||
// 弹窗关闭只影响展示,不会取消轮询;订单终态仍由服务端查询结果驱动。
|
||
function renderOrder() {
|
||
var order = state.order;
|
||
nodes.orderModal.classList.toggle(
|
||
'hidden',
|
||
!order || !state.orderModalOpen
|
||
);
|
||
nodes.orderPanel.classList.toggle('is-visible', !!order);
|
||
if (!order) return;
|
||
nodes.orderID.textContent = order.order_id || '-';
|
||
nodes.orderStatus.textContent = statusLabel(order.status);
|
||
nodes.orderStatusText.textContent = statusLabel(
|
||
order.status
|
||
);
|
||
nodes.usdtPanel.classList.toggle(
|
||
'hidden',
|
||
order.provider_code !== 'usdt_trc20'
|
||
);
|
||
nodes.mifapayPanel.classList.toggle(
|
||
'hidden',
|
||
order.provider_code !== 'mifapay' || !order.pay_url
|
||
);
|
||
if (order.provider_code === 'usdt_trc20') {
|
||
nodes.receiveAddress.textContent =
|
||
order.receive_address ||
|
||
(state.options &&
|
||
state.options.usdt_trc20_address) ||
|
||
'-';
|
||
}
|
||
if (order.status === 'credited') {
|
||
setStatus(
|
||
nodes.orderMessage,
|
||
t('recharge.paidSuccess', 'Payment completed')
|
||
);
|
||
} else if (order.status === 'failed') {
|
||
setStatus(
|
||
nodes.orderMessage,
|
||
order.failure_reason ||
|
||
t('recharge.orderFailed', 'Order failed')
|
||
);
|
||
} else if (order.provider_code === 'mifapay') {
|
||
setStatus(
|
||
nodes.orderMessage,
|
||
t(
|
||
'recharge.payOpenHint',
|
||
'Open the payment page and finish the payment.'
|
||
)
|
||
);
|
||
} else {
|
||
setStatus(
|
||
nodes.orderMessage,
|
||
t(
|
||
'recharge.usdtHint',
|
||
'Transfer USDT and submit the TXID.'
|
||
)
|
||
);
|
||
}
|
||
}
|
||
|
||
// 可用支付方式只在选中商品后生成,避免用户先选支付方式再切商品造成金额不一致。
|
||
// MiFaPay 方式必须是 active;USDT 必须同时有开关和收款地址,缺地址时不展示。
|
||
function availableMethods() {
|
||
var product = selectedProduct();
|
||
if (!product) return [];
|
||
var methods = [];
|
||
if (
|
||
state.options &&
|
||
state.options.usdt_trc20_enabled &&
|
||
state.options.usdt_trc20_address
|
||
) {
|
||
methods.push({
|
||
key: 'usdt_trc20',
|
||
icon: 'USDT',
|
||
name: t('recharge.usdtTrc20', 'USDT-TRC20'),
|
||
meta: t('recharge.usdtNetwork', 'TRON network'),
|
||
amount: formatUSD(product),
|
||
providerCode: 'usdt_trc20',
|
||
});
|
||
}
|
||
((state.options && state.options.payment_methods) || [])
|
||
.filter(function (method) {
|
||
return method.status === 'active';
|
||
})
|
||
.forEach(function (method) {
|
||
methods.push({
|
||
key: 'mifapay:' + method.method_id,
|
||
icon: method.currency_code || 'PAY',
|
||
name:
|
||
method.method_name ||
|
||
method.pay_way ||
|
||
t(
|
||
'recharge.thirdPartyPayment',
|
||
'Local payment'
|
||
),
|
||
meta: [
|
||
method.provider_name,
|
||
method.country_name || method.country_code,
|
||
method.pay_type,
|
||
]
|
||
.filter(Boolean)
|
||
.join(' · '),
|
||
amount: convertedAmount(product, method),
|
||
providerCode: 'mifapay',
|
||
methodID: method.method_id,
|
||
});
|
||
});
|
||
return methods;
|
||
}
|
||
|
||
// 创建订单时只提交 product_id、provider 和 method_id;金额、金币、币商/普通钱包都由服务端订单快照决定。
|
||
// 下单成功后统一打开操作弹窗:MiFaPay 由用户点击 pay_url,USDT 则在弹窗里复制地址并提交 txid。
|
||
function createOrder() {
|
||
var product = selectedProduct();
|
||
var method = selectedMethod();
|
||
if (!product || !method) return;
|
||
setBusy(true);
|
||
nodes.createOrder.textContent = t(
|
||
'recharge.creating',
|
||
'Creating...'
|
||
);
|
||
window.HyAppAPI.recharge
|
||
.createOrder({
|
||
display_user_id: targetDisplayUserID(),
|
||
product_id: product.product_id,
|
||
provider_code: method.providerCode,
|
||
payment_method_id: method.methodID || 0,
|
||
command_id: commandID(product, method),
|
||
return_url: window.location.href,
|
||
})
|
||
.then(function (payload) {
|
||
state.order = payload.order || payload;
|
||
state.orderModalOpen = true;
|
||
renderOrder();
|
||
startPolling();
|
||
})
|
||
.catch(function (error) {
|
||
toast(
|
||
error.message ||
|
||
t(
|
||
'recharge.orderCreateFailed',
|
||
'Create order failed'
|
||
)
|
||
);
|
||
})
|
||
.finally(function () {
|
||
setBusy(false);
|
||
nodes.createOrder.textContent = t(
|
||
'recharge.createOrder',
|
||
'Create order'
|
||
);
|
||
renderMethods();
|
||
});
|
||
}
|
||
|
||
function closeOrderModal() {
|
||
state.orderModalOpen = false;
|
||
renderOrder();
|
||
}
|
||
|
||
// USDT 模式只收 tx_hash;链上合约、to 地址、金额、确认数和重复 txid 都由后端校验。
|
||
function submitTx(event) {
|
||
event.preventDefault();
|
||
if (!state.order || !nodes.txHash.value.trim()) return;
|
||
setBusy(true);
|
||
nodes.submitTx.textContent = t(
|
||
'recharge.submitted',
|
||
'Submitting...'
|
||
);
|
||
window.HyAppAPI.recharge
|
||
.submitTx(state.order.order_id, {
|
||
display_user_id: targetDisplayUserID(),
|
||
tx_hash: nodes.txHash.value.trim(),
|
||
})
|
||
.then(function (payload) {
|
||
state.order = payload.order || payload;
|
||
toast(t('recharge.txSubmitted', 'TXID submitted'));
|
||
renderOrder();
|
||
startPolling();
|
||
})
|
||
.catch(function (error) {
|
||
toast(
|
||
error.message ||
|
||
t('recharge.loadFailed', 'Load failed')
|
||
);
|
||
})
|
||
.finally(function () {
|
||
setBusy(false);
|
||
nodes.submitTx.textContent = t(
|
||
'recharge.submitTx',
|
||
'Submit TXID'
|
||
);
|
||
});
|
||
}
|
||
|
||
// 轮询只在订单存在时启动;credited/failed 是终态,终态后停止,避免 H5 后台持续打接口。
|
||
function startPolling() {
|
||
window.clearInterval(state.pollingTimer);
|
||
if (!state.order || !state.order.order_id) return;
|
||
state.pollingTimer = window.setInterval(function () {
|
||
if (
|
||
!state.order ||
|
||
state.order.status === 'credited' ||
|
||
state.order.status === 'failed'
|
||
) {
|
||
window.clearInterval(state.pollingTimer);
|
||
return;
|
||
}
|
||
window.HyAppAPI.recharge
|
||
.getOrder(
|
||
state.order.order_id,
|
||
targetDisplayUserID()
|
||
)
|
||
.then(function (payload) {
|
||
state.order = payload.order || payload;
|
||
renderOrder();
|
||
})
|
||
.catch(function () {});
|
||
}, 5000);
|
||
}
|
||
|
||
// 复制地址不改变订单状态,只是降低 USDT 转账出错率;浏览器不支持 clipboard 时走 textarea 兜底。
|
||
function copyAddress() {
|
||
var address = nodes.receiveAddress.textContent || '';
|
||
if (!address || address === '-') return;
|
||
var copied = window.navigator.clipboard
|
||
? window.navigator.clipboard.writeText(address)
|
||
: fallbackCopy(address);
|
||
Promise.resolve(copied)
|
||
.then(function () {
|
||
toast(t('recharge.copied', 'Copied'));
|
||
})
|
||
.catch(function () {
|
||
toast(t('recharge.copyFailed', 'Copy failed'));
|
||
});
|
||
}
|
||
|
||
function fallbackCopy(text) {
|
||
var input = document.createElement('textarea');
|
||
input.value = text;
|
||
input.style.position = 'fixed';
|
||
input.style.opacity = '0';
|
||
document.body.appendChild(input);
|
||
input.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(input);
|
||
}
|
||
|
||
// 选择对象每次从 options 当前快照查找,避免保存旧对象导致切账号后仍引用旧商品。
|
||
function selectedProduct() {
|
||
return (
|
||
((state.options && state.options.products) || []).find(
|
||
function (item) {
|
||
return (
|
||
item.product_id === state.selectedProductID
|
||
);
|
||
}
|
||
) || null
|
||
);
|
||
}
|
||
|
||
function selectedMethod() {
|
||
return (
|
||
availableMethods().find(function (item) {
|
||
return item.key === state.selectedMethodKey;
|
||
}) || null
|
||
);
|
||
}
|
||
|
||
// token 模式不回传 display_user_id,防止前端把 token 用户和输入账号混用。
|
||
function targetDisplayUserID() {
|
||
if (
|
||
state.context &&
|
||
state.context.auth_mode !== 'token' &&
|
||
state.context.account
|
||
) {
|
||
return state.context.account.display_user_id || '';
|
||
}
|
||
return '';
|
||
}
|
||
|
||
// command_id 给服务端幂等使用,包含时间、商品和方式;真实订单号仍由后端生成。
|
||
function commandID(product, method) {
|
||
return [
|
||
'h5',
|
||
Date.now(),
|
||
product.product_id,
|
||
method.key.replace(/[^a-zA-Z0-9_:-]/g, ''),
|
||
].join(':');
|
||
}
|
||
|
||
// MiFaPay 国家方式按后台 USD->本币汇率展示;无有效汇率时回退 USD 展示但后端仍会拒绝不可用配置。
|
||
function convertedAmount(product, method) {
|
||
var usd = productUSD(product);
|
||
var rate = Number(method.usd_to_currency_rate || 0);
|
||
var currency = method.currency_code || 'USD';
|
||
var amount =
|
||
Number.isFinite(rate) && rate > 0 ? usd * rate : usd;
|
||
return (
|
||
amount.toFixed(currency === 'USD' ? 2 : 2) +
|
||
' ' +
|
||
currency
|
||
);
|
||
}
|
||
|
||
function productUSD(product) {
|
||
if (product.amount_minor) return product.amount_minor / 100;
|
||
if (product.amount_micro)
|
||
return product.amount_micro / 1000000;
|
||
return 0;
|
||
}
|
||
|
||
function formatUSD(product) {
|
||
return productUSD(product).toFixed(2) + ' USD';
|
||
}
|
||
|
||
function formatCoins(value) {
|
||
var numberValue = Number(value || 0);
|
||
return Number.isFinite(numberValue)
|
||
? numberValue.toLocaleString()
|
||
: '0';
|
||
}
|
||
|
||
function statusLabel(status) {
|
||
var key = 'recharge.status.' + String(status || 'pending');
|
||
return t(key, status || 'pending');
|
||
}
|
||
|
||
// loading 同时锁住确认账号和创建订单,避免用户在请求中切账号或重复下单。
|
||
function setBusy(loading) {
|
||
state.loading = loading;
|
||
nodes.searchAccount.disabled = loading;
|
||
nodes.createOrder.disabled =
|
||
loading ||
|
||
!state.selectedProductID ||
|
||
!state.selectedMethodKey;
|
||
}
|
||
|
||
function setStatus(node, message) {
|
||
node.textContent = message || '';
|
||
}
|
||
|
||
function scrollToTop() {
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
}
|
||
|
||
function firstLetter(value) {
|
||
return (
|
||
String(value || 'H')
|
||
.trim()
|
||
.charAt(0) || 'H'
|
||
);
|
||
}
|
||
|
||
function toast(message) {
|
||
if (window.HyAppToast) window.HyAppToast.show(message);
|
||
}
|
||
|
||
function t(key, fallback) {
|
||
return window.HyAppI18n
|
||
? window.HyAppI18n.t(key, fallback)
|
||
: fallback || key;
|
||
}
|
||
|
||
function escapeHTML(value) {
|
||
return String(value || '').replace(
|
||
/[&<>"']/g,
|
||
function (char) {
|
||
return {
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": ''',
|
||
}[char];
|
||
}
|
||
);
|
||
}
|
||
|
||
function escapeAttr(value) {
|
||
return escapeHTML(value).replace(/`/g, '`');
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|