hyapp-h5/recharge/create.html
2026-06-27 18:45:00 +08:00

1947 lines
74 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>三方临时支付链接</title>
<link rel="stylesheet" href="../common/theme.css" />
<style>
:root {
--pay-page-bg: #f5f7fb;
--pay-ink: #111827;
--pay-ink-strong: #07111f;
--pay-muted: #697386;
--pay-muted-soft: #8b94a7;
--pay-line: #dde4f0;
--pay-line-strong: #c6cfde;
--pay-surface: #ffffff;
--pay-surface-soft: #f8fafd;
--pay-surface-hover: #f2f5fa;
--pay-shadow: 0 18px 48px rgba(15, 23, 42, 0.08);
--pay-shadow-soft: 0 10px 26px rgba(15, 23, 42, 0.06);
--pay-accent: var(--hy-theme-primary-strong);
--pay-accent-deep: var(--hy-theme-primary-deep);
--pay-accent-soft: var(--hy-theme-primary-soft);
--pay-focus: var(--hy-theme-focus-ring);
--pay-success: var(--hy-theme-success);
--pay-danger: var(--hy-theme-danger);
}
* {
box-sizing: border-box;
}
[hidden] {
display: none !important;
}
body {
min-height: 100vh;
margin: 0;
background: var(--pay-page-bg);
color: var(--pay-ink);
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
letter-spacing: 0;
}
button,
input,
textarea {
font: inherit;
letter-spacing: 0;
}
button {
appearance: none;
-webkit-appearance: none;
}
input,
textarea {
appearance: none;
-webkit-appearance: none;
width: 100%;
border: 1px solid var(--pay-line);
border-radius: 12px;
outline: none;
background: var(--pay-surface);
color: var(--pay-ink);
padding: 12px 14px;
transition:
border-color 0.16s ease,
box-shadow 0.16s ease,
background 0.16s ease;
}
input {
min-height: 48px;
}
textarea {
min-height: 96px;
resize: vertical;
}
input::placeholder,
textarea::placeholder {
color: var(--pay-muted-soft);
}
input:focus,
textarea:focus {
border-color: var(--pay-accent);
box-shadow: 0 0 0 4px var(--pay-focus);
}
.page {
width: min(1180px, 100%);
margin: 0 auto;
padding: 26px 18px 44px;
}
.topbar {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
margin-bottom: 18px;
}
.title-block {
min-width: 0;
}
.kicker {
display: inline-flex;
align-items: center;
gap: 8px;
margin: 0 0 8px;
color: var(--pay-muted);
font-size: 12px;
font-weight: 800;
text-transform: uppercase;
}
.kicker::before {
width: 8px;
height: 8px;
border-radius: 999px;
background: var(--pay-accent);
content: '';
}
.title {
margin: 0;
color: var(--pay-ink-strong);
font-size: 28px;
font-weight: 850;
line-height: 1.2;
}
.meta {
margin: 8px 0 0;
color: var(--pay-muted);
font-size: 13px;
line-height: 1.5;
overflow-wrap: anywhere;
}
.toolbar {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 10px;
}
.button {
display: inline-flex;
min-height: 44px;
align-items: center;
justify-content: center;
border: 1px solid transparent;
border-radius: 12px;
cursor: pointer;
padding: 0 16px;
font-size: 14px;
font-weight: 850;
transition:
transform 0.16s ease,
box-shadow 0.16s ease,
border-color 0.16s ease,
background 0.16s ease,
color 0.16s ease;
}
.button:hover:not(:disabled) {
transform: translateY(-1px);
}
.button.primary {
background: var(--hy-theme-button);
color: #fff;
box-shadow: var(--hy-theme-button-shadow);
}
.button.secondary {
border-color: var(--pay-line);
background: var(--pay-surface);
color: var(--pay-accent-deep);
box-shadow: var(--pay-shadow-soft);
}
.button:disabled {
cursor: not-allowed;
transform: none;
border-color: var(--pay-line);
background: var(--hy-theme-button-disabled);
color: var(--hy-theme-button-disabled-text);
box-shadow: none;
}
.workspace {
display: grid;
gap: 18px;
grid-template-columns: minmax(0, 1.08fr) minmax(360px, 0.78fr);
align-items: start;
}
.column {
display: grid;
gap: 18px;
min-width: 0;
}
.panel {
min-width: 0;
border: 1px solid var(--pay-line);
border-radius: 18px;
background: var(--pay-surface);
box-shadow: var(--pay-shadow);
padding: 20px;
}
.panel-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
margin-bottom: 18px;
}
.panel-title {
min-width: 0;
}
.eyebrow {
margin: 0 0 5px;
color: var(--pay-muted);
font-size: 12px;
font-weight: 850;
text-transform: uppercase;
}
.panel h2 {
margin: 0;
color: var(--pay-ink-strong);
font-size: 18px;
font-weight: 850;
line-height: 1.3;
}
.panel-badge {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
border: 1px solid var(--pay-line);
border-radius: 999px;
background: var(--pay-surface-soft);
color: var(--pay-muted);
padding: 7px 10px;
font-size: 12px;
font-weight: 800;
}
.form-grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.field {
display: grid;
min-width: 0;
gap: 8px;
}
.field.full,
.method-section {
grid-column: 1 / -1;
}
.field-label,
label {
color: var(--pay-muted);
font-size: 13px;
font-weight: 850;
}
.segmented {
display: grid;
gap: 8px;
grid-template-columns: repeat(3, minmax(0, 1fr));
border: 1px solid var(--pay-line);
border-radius: 14px;
background: var(--pay-surface-soft);
padding: 6px;
}
.segment {
min-height: 42px;
border: 1px solid transparent;
border-radius: 10px;
background: transparent;
color: var(--pay-muted);
cursor: pointer;
font-weight: 850;
}
.segment.is-active {
border-color: var(--pay-line-strong);
background: var(--pay-surface);
color: var(--pay-ink-strong);
box-shadow: var(--pay-shadow-soft);
}
.combo {
position: relative;
min-width: 0;
}
.combo-trigger {
display: flex;
width: 100%;
min-height: 52px;
align-items: center;
justify-content: space-between;
gap: 12px;
border: 1px solid var(--pay-line);
border-radius: 14px;
background: var(--pay-surface);
color: var(--pay-ink);
cursor: pointer;
padding: 11px 14px;
text-align: left;
transition:
border-color 0.16s ease,
box-shadow 0.16s ease;
}
.combo-trigger:focus,
.combo.is-open .combo-trigger {
border-color: var(--pay-accent);
box-shadow: 0 0 0 4px var(--pay-focus);
}
.combo-copy {
display: grid;
min-width: 0;
gap: 3px;
}
.combo-title {
overflow: hidden;
color: var(--pay-ink-strong);
font-weight: 850;
text-overflow: ellipsis;
white-space: nowrap;
}
.combo-subtitle {
overflow: hidden;
color: var(--pay-muted);
font-size: 12px;
font-weight: 750;
text-overflow: ellipsis;
white-space: nowrap;
}
.chevron {
flex: 0 0 auto;
width: 10px;
height: 10px;
border-right: 2px solid var(--pay-muted);
border-bottom: 2px solid var(--pay-muted);
transform: rotate(45deg);
transition: transform 0.16s ease;
}
.combo.is-open .chevron {
transform: rotate(225deg);
}
.combo-menu {
position: absolute;
z-index: 20;
top: calc(100% + 8px);
right: 0;
left: 0;
border: 1px solid var(--pay-line);
border-radius: 16px;
background: var(--pay-surface);
box-shadow: 0 20px 48px rgba(15, 23, 42, 0.16);
padding: 10px;
}
.combo-search {
margin-bottom: 8px;
}
.option-list {
display: grid;
max-height: 280px;
gap: 4px;
overflow-y: auto;
padding-right: 2px;
}
.option {
display: grid;
width: 100%;
gap: 3px;
border: 1px solid transparent;
border-radius: 12px;
background: transparent;
color: var(--pay-ink);
cursor: pointer;
padding: 10px;
text-align: left;
}
.option:hover,
.option.is-active {
border-color: var(--pay-line);
background: var(--pay-surface-hover);
}
.option.is-active {
box-shadow: inset 3px 0 0 var(--pay-accent);
}
.option-title {
color: var(--pay-ink-strong);
font-size: 14px;
font-weight: 850;
}
.option-meta {
color: var(--pay-muted);
font-size: 12px;
font-weight: 750;
}
.empty-state {
border: 1px dashed var(--pay-line-strong);
border-radius: 14px;
background: var(--pay-surface-soft);
color: var(--pay-muted);
padding: 18px;
text-align: center;
font-size: 13px;
font-weight: 750;
}
.method-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.method-count {
color: var(--pay-muted);
font-size: 12px;
font-weight: 850;
}
.method-grid {
display: grid;
max-height: 422px;
gap: 10px;
overflow-y: auto;
padding-right: 3px;
}
.method-card {
display: grid;
grid-template-columns: 42px minmax(0, 1fr) auto;
gap: 12px;
align-items: center;
width: 100%;
border: 1px solid var(--pay-line);
border-radius: 14px;
background: var(--pay-surface);
color: var(--pay-ink);
cursor: pointer;
padding: 12px;
text-align: left;
transition:
border-color 0.16s ease,
box-shadow 0.16s ease,
transform 0.16s ease,
background 0.16s ease;
}
.method-card:hover {
transform: translateY(-1px);
border-color: var(--pay-line-strong);
background: var(--pay-surface-hover);
}
.method-card.is-active {
border-color: var(--pay-accent);
background: #fbfaff;
box-shadow: 0 0 0 4px var(--pay-focus);
}
.method-logo {
display: grid;
width: 42px;
height: 42px;
place-items: center;
overflow: hidden;
border: 1px solid var(--pay-line);
border-radius: 12px;
background: var(--pay-surface-soft);
color: var(--pay-accent-deep);
font-size: 12px;
font-weight: 900;
}
.method-logo img {
width: 100%;
height: 100%;
object-fit: contain;
padding: 6px;
}
.method-copy {
display: grid;
min-width: 0;
gap: 4px;
}
.method-title {
overflow: hidden;
color: var(--pay-ink-strong);
font-weight: 850;
text-overflow: ellipsis;
white-space: nowrap;
}
.method-meta {
overflow: hidden;
color: var(--pay-muted);
font-size: 12px;
font-weight: 750;
text-overflow: ellipsis;
white-space: nowrap;
}
.method-tags {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 6px;
}
.pill {
display: inline-flex;
align-items: center;
border: 1px solid var(--pay-line);
border-radius: 999px;
background: var(--pay-surface-soft);
color: var(--pay-muted);
padding: 5px 8px;
font-size: 11px;
font-weight: 850;
line-height: 1;
white-space: nowrap;
}
.pill.accent {
border-color: transparent;
background: var(--pay-accent-soft);
color: var(--pay-accent-deep);
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 16px;
}
.message {
min-height: 22px;
margin-top: 12px;
color: var(--pay-muted);
font-size: 13px;
font-weight: 750;
}
.message.error {
color: var(--pay-danger);
}
.message.success {
color: var(--pay-success);
}
.result {
display: grid;
gap: 12px;
}
.result-row {
display: grid;
min-width: 0;
gap: 5px;
}
.result-row strong {
color: var(--pay-muted);
font-size: 12px;
font-weight: 850;
}
.value {
min-width: 0;
overflow-wrap: anywhere;
border: 1px solid var(--pay-line);
border-radius: 12px;
background: var(--pay-surface-soft);
padding: 11px 12px;
line-height: 1.5;
}
.status {
display: inline-flex;
width: fit-content;
align-items: center;
border-radius: 999px;
background: var(--pay-accent-soft);
color: var(--pay-accent-deep);
padding: 6px 10px;
font-size: 12px;
font-weight: 850;
}
.status.paid {
background: rgba(49, 198, 168, 0.14);
color: var(--pay-success);
}
.status.failed {
background: rgba(217, 93, 114, 0.12);
color: var(--pay-danger);
}
@media (max-width: 920px) {
.topbar {
align-items: stretch;
flex-direction: column;
}
.toolbar {
justify-content: flex-start;
}
.workspace,
.form-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 560px) {
.page {
padding: 18px 12px 32px;
}
.title {
font-size: 24px;
}
.panel {
border-radius: 14px;
padding: 16px;
}
.segmented {
grid-template-columns: 1fr;
}
.method-card {
grid-template-columns: 38px minmax(0, 1fr);
}
.method-tags {
grid-column: 1 / -1;
justify-content: flex-start;
}
}
</style>
</head>
<body>
<main class="page">
<header class="topbar">
<div class="title-block">
<p class="kicker">Payment Operations</p>
<h1 class="title">三方临时支付链接</h1>
<p class="meta" id="envMeta">加载环境中...</p>
</div>
<div class="toolbar">
<button
class="button secondary"
type="button"
id="reloadMethods"
>
刷新支付方式
</button>
</div>
</header>
<section class="workspace">
<div class="column">
<div class="panel">
<div class="panel-header">
<div class="panel-title">
<p class="eyebrow">Create</p>
<h2>创建支付链接</h2>
</div>
<span class="panel-badge" id="loadedBadge">
等待支付方式
</span>
</div>
<form id="createForm" novalidate>
<div class="form-grid">
<div class="field full">
<span class="field-label">Return App</span>
<div
class="segmented"
id="appSegments"
role="tablist"
aria-label="Return App"
>
<button
class="segment is-active"
type="button"
data-app-code="lalu"
aria-selected="true"
>
lalu
</button>
<button
class="segment"
type="button"
data-app-code="yumi"
aria-selected="false"
>
yumi
</button>
<button
class="segment"
type="button"
data-app-code="aslan"
aria-selected="false"
>
aslan
</button>
</div>
</div>
<div class="field full">
<label for="countrySearch">国家</label>
<div class="combo" id="countryCombo">
<button
class="combo-trigger"
type="button"
id="countryToggle"
aria-expanded="false"
aria-haspopup="listbox"
>
<span class="combo-copy">
<span
class="combo-title"
id="countrySelected"
>
正在加载国家...
</span>
<span
class="combo-subtitle"
id="countryMeta"
>
根据国家筛选可用支付方式
</span>
</span>
<span
class="chevron"
aria-hidden="true"
></span>
</button>
<div
class="combo-menu"
id="countryMenu"
hidden
>
<input
class="combo-search"
id="countrySearch"
placeholder="搜索国家名称或代码"
type="text"
autocomplete="off"
/>
<div
class="option-list"
id="countryOptions"
role="listbox"
aria-label="国家列表"
></div>
</div>
</div>
</div>
<div class="field">
<label for="usdAmount">美金金额</label>
<input
id="usdAmount"
inputmode="decimal"
placeholder="例如 300 或 9.99"
type="text"
/>
</div>
<div class="field full">
<label for="returnUrl">
支付完成返回地址
</label>
<input
id="returnUrl"
placeholder="默认按选择的 App 返回充值页"
type="url"
/>
</div>
<div class="method-section">
<div class="method-head">
<span class="field-label">
支付方式
</span>
<span
class="method-count"
id="methodCount"
>
-
</span>
</div>
<div
class="method-grid"
id="methodGrid"
role="radiogroup"
aria-label="支付方式"
>
<div class="empty-state">
正在加载支付方式...
</div>
</div>
</div>
</div>
<div class="actions">
<button
class="button primary"
type="submit"
id="createButton"
>
创建链接
</button>
</div>
<div class="message" id="createMessage"></div>
</form>
</div>
<div class="panel">
<div class="panel-header">
<div class="panel-title">
<p class="eyebrow">Verify</p>
<h2>验证支付链接</h2>
</div>
</div>
<div class="field">
<label for="verifyInput">订单号或支付链接</label>
<input
id="verifyInput"
placeholder="输入 order_id或粘贴创建出来的支付链接"
/>
</div>
<div class="actions">
<button
class="button primary"
type="button"
id="verifyButton"
>
验证支付状态
</button>
</div>
<div class="message" id="verifyMessage"></div>
</div>
</div>
<div class="column">
<div class="panel">
<div class="panel-header">
<div class="panel-title">
<p class="eyebrow">Create Result</p>
<h2>创建结果</h2>
</div>
</div>
<div class="result" id="createResult">
<div class="value">还没有创建支付链接</div>
</div>
<div class="actions">
<button
class="button secondary"
disabled
type="button"
id="copyLink"
>
复制链接
</button>
<button
class="button secondary"
disabled
type="button"
id="openLink"
>
打开支付
</button>
</div>
</div>
<div class="panel">
<div class="panel-header">
<div class="panel-title">
<p class="eyebrow">Verify Result</p>
<h2>验证结果</h2>
</div>
</div>
<div class="result" id="verifyResult">
<div class="value">还没有验证订单</div>
</div>
</div>
</div>
</section>
</main>
<script>
(function () {
var TEST_API_BASE_URL =
'https://api-test.global-interaction.com/';
var LOCAL_API_BASE_URL = 'http://127.0.0.1:13000/';
var DEFAULT_API_BASE_URL =
'https://api.global-interaction.com/';
var API_BASE_URL = resolveAPIBaseURL();
var RETURN_URL_BASE =
'https://h5.global-interaction.com/recharge/index.html';
var APP_CODES = ['lalu', 'yumi', 'aslan'];
var methods = [];
var countries = [];
var selectedAppCode = 'lalu';
var selectedCountryCode = '';
var selectedMethodID = 0;
var latestPaymentLink = '';
var latestOrderID = '';
var returnUrlTouched = false;
var els = {
appButtons: [].slice.call(
document.querySelectorAll('[data-app-code]')
),
copyLink: document.getElementById('copyLink'),
countryCombo: document.getElementById('countryCombo'),
countryMenu: document.getElementById('countryMenu'),
countryMeta: document.getElementById('countryMeta'),
countryOptions: document.getElementById('countryOptions'),
countrySearch: document.getElementById('countrySearch'),
countrySelected: document.getElementById('countrySelected'),
countryToggle: document.getElementById('countryToggle'),
createButton: document.getElementById('createButton'),
createForm: document.getElementById('createForm'),
createMessage: document.getElementById('createMessage'),
createResult: document.getElementById('createResult'),
envMeta: document.getElementById('envMeta'),
loadedBadge: document.getElementById('loadedBadge'),
methodCount: document.getElementById('methodCount'),
methodGrid: document.getElementById('methodGrid'),
openLink: document.getElementById('openLink'),
reloadMethods: document.getElementById('reloadMethods'),
returnUrl: document.getElementById('returnUrl'),
usdAmount: document.getElementById('usdAmount'),
verifyButton: document.getElementById('verifyButton'),
verifyInput: document.getElementById('verifyInput'),
verifyMessage: document.getElementById('verifyMessage'),
verifyResult: document.getElementById('verifyResult'),
};
setDefaultReturnURL(true);
renderEnvMeta();
bindEvents();
loadMethods();
function bindEvents() {
els.appButtons.forEach(function (button) {
button.addEventListener('click', function () {
selectApp(button.getAttribute('data-app-code'));
});
});
els.countryToggle.addEventListener('click', function () {
setCountryMenuOpen(
els.countryMenu.hasAttribute('hidden')
);
});
els.countrySearch.addEventListener(
'input',
renderCountries
);
els.countrySearch.addEventListener(
'keydown',
function (event) {
if (event.key === 'Escape')
setCountryMenuOpen(false);
}
);
document.addEventListener('click', function (event) {
if (!els.countryCombo.contains(event.target)) {
setCountryMenuOpen(false);
}
});
els.returnUrl.addEventListener('input', function () {
returnUrlTouched = true;
});
els.createForm.addEventListener(
'submit',
createPaymentLink
);
els.reloadMethods.addEventListener('click', loadMethods);
els.copyLink.addEventListener('click', function () {
copyText(latestPaymentLink, els.createMessage);
});
els.openLink.addEventListener('click', function () {
if (latestPaymentLink) {
window.open(
latestPaymentLink,
'_blank',
'noopener,noreferrer'
);
}
});
els.verifyButton.addEventListener(
'click',
verifyPaymentLink
);
}
function loadMethods() {
setMessage(els.createMessage, '正在加载支付方式...', '');
setMethodsLoading(true);
return apiGet('/api/v1/recharge/h5/temporary/methods')
.then(function (data) {
methods = Array.isArray(data.methods)
? data.methods
: [];
countries = buildCountries(methods);
reconcileSelection();
renderAllSelectionViews();
setMessage(
els.createMessage,
methods.length
? '支付方式已加载'
: '没有可用支付方式',
methods.length ? 'success' : 'error'
);
})
.catch(function (err) {
methods = [];
countries = [];
selectedCountryCode = '';
selectedMethodID = 0;
renderAllSelectionViews();
setMessage(
els.createMessage,
err.message || '加载支付方式失败',
'error'
);
})
.finally(function () {
setMethodsLoading(false);
});
}
function setMethodsLoading(loading) {
els.reloadMethods.disabled = loading;
els.createButton.disabled = loading;
if (loading) {
els.loadedBadge.textContent = '加载中';
els.methodGrid.innerHTML =
'<div class="empty-state">正在加载支付方式...</div>';
}
}
function buildCountries(sourceMethods) {
var byCode = {};
sourceMethods.forEach(function (method, index) {
var code = countryCode(method);
if (!code) return;
if (!byCode[code]) {
byCode[code] = {
code: code,
name: method.country_name || code,
currency: method.currency_code || '',
count: 0,
firstSort:
Number(method.sort_order || 0) || index,
};
}
byCode[code].count += 1;
});
return Object.keys(byCode)
.map(function (code) {
return byCode[code];
})
.sort(function (a, b) {
return a.firstSort - b.firstSort;
});
}
function reconcileSelection() {
if (
selectedCountryCode &&
!countries.some(function (country) {
return country.code === selectedCountryCode;
})
) {
selectedCountryCode = '';
}
if (!selectedCountryCode && countries.length) {
selectedCountryCode = countries[0].code;
}
if (
selectedMethodID &&
!filteredMethods().some(function (method) {
return (
Number(method.method_id) === selectedMethodID
);
})
) {
selectedMethodID = 0;
}
if (!selectedMethodID && filteredMethods().length) {
selectedMethodID = Number(
filteredMethods()[0].method_id
);
}
}
function renderAllSelectionViews() {
renderAppSegments();
renderCountries();
renderCountrySummary();
renderMethods();
renderEnvMeta();
}
function selectApp(appCode) {
selectedAppCode =
APP_CODES.indexOf(appCode) >= 0 ? appCode : 'lalu';
setDefaultReturnURL(false);
renderAppSegments();
renderEnvMeta();
}
function renderAppSegments() {
els.appButtons.forEach(function (button) {
var active =
button.getAttribute('data-app-code') ===
selectedAppCode;
button.classList.toggle('is-active', active);
button.setAttribute(
'aria-selected',
active ? 'true' : 'false'
);
});
}
function renderCountries() {
var query = normalizeSearch(els.countrySearch.value);
var rows = countries.filter(function (country) {
var searchable = normalizeSearch(
[country.name, country.code, country.currency].join(
' '
)
);
return !query || searchable.indexOf(query) >= 0;
});
clearNode(els.countryOptions);
if (!rows.length) {
els.countryOptions.appendChild(
createNode('div', 'empty-state', '没有匹配的国家')
);
return;
}
rows.forEach(function (country) {
var option = createNode('button', 'option');
option.type = 'button';
option.setAttribute('role', 'option');
option.setAttribute(
'aria-selected',
country.code === selectedCountryCode
? 'true'
: 'false'
);
option.classList.toggle(
'is-active',
country.code === selectedCountryCode
);
option.appendChild(
createNode(
'span',
'option-title',
countryLabel(country)
)
);
option.appendChild(
createNode(
'span',
'option-meta',
country.currency +
' · ' +
country.count +
' 个支付方式'
)
);
option.addEventListener('click', function () {
selectCountry(country.code);
});
els.countryOptions.appendChild(option);
});
}
function selectCountry(countryCodeValue) {
selectedCountryCode = countryCodeValue || '';
selectedMethodID = 0;
reconcileSelection();
renderCountrySummary();
renderCountries();
renderMethods();
setCountryMenuOpen(false);
}
function renderCountrySummary() {
var country = selectedCountry();
if (!country) {
els.countrySelected.textContent = countries.length
? '请选择国家'
: '没有可用国家';
els.countryMeta.textContent =
'根据国家筛选可用支付方式';
return;
}
els.countrySelected.textContent = countryLabel(country);
els.countryMeta.textContent =
country.currency +
' · ' +
country.count +
' 个支付方式';
}
function renderMethods() {
var rows = filteredMethods();
clearNode(els.methodGrid);
els.methodCount.textContent = selectedCountryCode
? rows.length + ' 个可用'
: '-';
els.loadedBadge.textContent = methods.length
? countries.length +
' 个国家 / ' +
methods.length +
' 个方式'
: '没有支付方式';
if (!selectedCountryCode) {
els.methodGrid.appendChild(
createNode('div', 'empty-state', '请先选择国家')
);
return;
}
if (!rows.length) {
els.methodGrid.appendChild(
createNode(
'div',
'empty-state',
'当前国家没有可用支付方式'
)
);
return;
}
rows.forEach(function (method) {
els.methodGrid.appendChild(createMethodCard(method));
});
}
function createMethodCard(method) {
var selected =
Number(method.method_id) === Number(selectedMethodID);
var card = createNode('button', 'method-card');
card.type = 'button';
card.setAttribute('role', 'radio');
card.setAttribute(
'aria-checked',
selected ? 'true' : 'false'
);
card.classList.toggle('is-active', selected);
card.addEventListener('click', function () {
selectedMethodID = Number(method.method_id);
renderMethods();
});
card.appendChild(methodLogo(method));
var copy = createNode('span', 'method-copy');
copy.appendChild(
createNode(
'span',
'method-title',
method.method_name ||
method.pay_type ||
method.pay_way ||
'-'
)
);
copy.appendChild(
createNode(
'span',
'method-meta',
[
method.provider_name || method.provider_code,
method.pay_way,
method.pay_type,
]
.filter(Boolean)
.join(' / ')
)
);
card.appendChild(copy);
var tags = createNode('span', 'method-tags');
tags.appendChild(
createNode(
'span',
'pill accent',
method.currency_code || '-'
)
);
tags.appendChild(
createNode('span', 'pill', 'ID ' + method.method_id)
);
card.appendChild(tags);
return card;
}
function methodLogo(method) {
var logo = createNode('span', 'method-logo');
if (method.logo_url) {
var image = document.createElement('img');
image.alt =
method.method_name || method.pay_type || 'pay';
image.src = method.logo_url;
image.addEventListener('error', function () {
image.remove();
logo.textContent = logoText(method);
});
logo.appendChild(image);
return logo;
}
logo.textContent = logoText(method);
return logo;
}
function setCountryMenuOpen(open) {
els.countryCombo.classList.toggle('is-open', open);
els.countryToggle.setAttribute(
'aria-expanded',
open ? 'true' : 'false'
);
if (open) {
els.countryMenu.removeAttribute('hidden');
renderCountries();
window.requestAnimationFrame(function () {
els.countrySearch.focus();
els.countrySearch.select();
});
} else {
els.countryMenu.setAttribute('hidden', '');
els.countrySearch.value = '';
}
}
function filteredMethods() {
if (!selectedCountryCode) return [];
return methods
.filter(function (method) {
return countryCode(method) === selectedCountryCode;
})
.sort(function (a, b) {
return (
(Number(a.sort_order || 0) || 0) -
(Number(b.sort_order || 0) || 0) ||
methodLabel(a).localeCompare(methodLabel(b))
);
});
}
function selectedCountry() {
return countries.find(function (country) {
return country.code === selectedCountryCode;
});
}
function selectedMethod() {
var id = Number(selectedMethodID || 0);
return methods.find(function (method) {
return Number(method.method_id) === id;
});
}
function createPaymentLink(event) {
event.preventDefault();
var method = selectedMethod();
var usdMinorAmount = dollarsToMinor(els.usdAmount.value);
if (!selectedCountryCode) {
setMessage(
els.createMessage,
'请选择国家后再选择支付方式',
'error'
);
return;
}
if (!method || !usdMinorAmount || usdMinorAmount <= 0) {
setMessage(
els.createMessage,
'请选择支付方式,并填写大于 0 的美金金额',
'error'
);
return;
}
setLoading(els.createButton, true, '创建中...');
setMessage(els.createMessage, '正在创建支付链接...', '');
apiPost('/api/v1/recharge/h5/temporary/orders', {
coin_amount: 0,
usd_minor_amount: usdMinorAmount,
provider_code: method.provider_code,
payment_method_id: method.method_id,
return_url: currentReturnURL(),
})
.then(function (data) {
latestPaymentLink = data.payment_link || '';
latestOrderID =
data.order && data.order.order_id
? data.order.order_id
: '';
renderOrder(els.createResult, data);
els.copyLink.disabled = !latestPaymentLink;
els.openLink.disabled = !latestPaymentLink;
if (latestOrderID) {
els.verifyInput.value = latestOrderID;
}
setMessage(
els.createMessage,
'支付链接已创建',
'success'
);
})
.catch(function (err) {
setMessage(
els.createMessage,
err.message || '创建支付链接失败',
'error'
);
})
.finally(function () {
setLoading(els.createButton, false, '创建链接');
});
}
function verifyPaymentLink() {
var orderID = extractOrderID(els.verifyInput.value);
if (!orderID) {
setMessage(
els.verifyMessage,
'请输入订单号,或粘贴包含订单号的支付链接',
'error'
);
return;
}
setLoading(els.verifyButton, true, '验证中...');
setMessage(
els.verifyMessage,
'正在向后端查询支付状态...',
''
);
apiGet(
'/api/v1/recharge/h5/temporary/orders/' +
encodeURIComponent(orderID)
)
.then(function (data) {
renderOrder(els.verifyResult, data);
setMessage(
els.verifyMessage,
statusText(data.order && data.order.status),
data.order && data.order.status === 'paid'
? 'success'
: ''
);
})
.catch(function (err) {
setMessage(
els.verifyMessage,
err.message || '验证支付状态失败',
'error'
);
})
.finally(function () {
setLoading(els.verifyButton, false, '验证支付状态');
});
}
function renderOrder(target, data) {
var order = data.order || {};
var paymentLink = data.payment_link || order.pay_url || '';
target.innerHTML = [
resultRow('订单ID', order.order_id || '-'),
resultRow(
'支付状态',
'<span class="status ' +
escapeHTML(order.status || '') +
'">' +
escapeHTML(statusLabel(order.status)) +
'</span>'
),
resultRow(
'美金金额',
escapeHTML(
'USD ' + formatMoney(order.usd_minor_amount)
)
),
resultRow(
'支付方式',
escapeHTML(
[
order.provider_code,
order.pay_way,
order.pay_type,
]
.filter(Boolean)
.join(' / ') || '-'
)
),
resultRow(
'国家',
escapeHTML(
[
order.country_name,
order.country_code,
order.currency_code,
]
.filter(Boolean)
.join(' / ') || '-'
)
),
resultRow('支付链接', escapeHTML(paymentLink || '-')),
data.wrapped
? resultRow(
'中转说明',
escapeHTML(
data.wrapped_note ||
'已生成 COS 中转 HTML'
)
)
: '',
data.raw_pay_url
? resultRow(
'原始链接',
escapeHTML(data.raw_pay_url)
)
: '',
]
.filter(Boolean)
.join('');
}
function resultRow(label, valueHTML) {
return (
'<div class="result-row"><strong>' +
escapeHTML(label) +
'</strong><div class="value">' +
valueHTML +
'</div></div>'
);
}
function apiGet(path) {
return apiRequest(path, { method: 'GET' });
}
function apiPost(path, body) {
return apiRequest(path, {
method: 'POST',
body: body || {},
});
}
function apiRequest(path, options) {
var opts = options || {};
var headers = {
// 临时支付链接只做收款和人工核销,不绑定具体 App 发币;统一用 lalu 的支付方式和汇率建单。
// Return App 只控制付款后回到哪个充值页,不改变收款口径,避免回调查单和汇率口径被前端选择打散。
'X-App-Code': paymentAppCode(),
};
var body;
if (opts.body) {
headers['Content-Type'] = 'application/json';
body = JSON.stringify(opts.body);
}
return fetch(apiURL(path), {
method: opts.method || 'GET',
headers: headers,
body: body,
credentials: 'omit',
}).then(function (response) {
return response.text().then(function (text) {
var payload = parsePayload(text);
if (!response.ok) {
var error = new Error(
errorMessage(response, payload, text)
);
error.status = response.status;
error.payload = payload;
throw error;
}
if (isAPIFailure(payload)) {
throw new Error(
payload.message ||
payload.errorMsg ||
payload.error ||
'接口请求失败'
);
}
return Object.prototype.hasOwnProperty.call(
payload,
'data'
)
? payload.data
: payload;
});
});
}
function apiURL(path) {
return new URL(
path.replace(/^\/+/, ''),
API_BASE_URL
).toString();
}
function resolveAPIBaseURL() {
var env = currentEnv();
if (env === 'test') {
return normalizeBaseURL(
isLocalDevOrigin()
? window.location.origin + '/__api_test__/'
: TEST_API_BASE_URL
);
}
if (env === 'local')
return normalizeBaseURL(LOCAL_API_BASE_URL);
return normalizeBaseURL(DEFAULT_API_BASE_URL);
}
function currentEnv() {
var params = new URLSearchParams(window.location.search);
var env = String(params.get('env') || '').trim();
return env === 'test' || env === 'local' ? env : '';
}
function isLocalDevOrigin() {
return (
window.location.hostname === '127.0.0.1' ||
window.location.hostname === 'localhost' ||
window.location.hostname === '::1'
);
}
function normalizeBaseURL(value) {
return String(value || '').replace(/\/+$/, '') + '/';
}
function parsePayload(text) {
if (!text) return {};
try {
return JSON.parse(text);
} catch (_) {
return { message: text };
}
}
function errorMessage(response, payload, text) {
if (response.status === 404) {
return '接口不存在或后端还没有部署到线上';
}
return (
payload.message ||
payload.errorMsg ||
payload.error ||
text ||
response.statusText ||
'接口请求失败'
);
}
function isAPIFailure(payload) {
var code = payload && payload.code;
var errorCode = payload && payload.errorCode;
return (
(typeof code === 'number' && code !== 0) ||
(typeof code === 'string' &&
code !== '' &&
code !== 'OK') ||
(typeof errorCode === 'number' && errorCode !== 0) ||
(typeof errorCode === 'string' &&
errorCode !== '' &&
errorCode !== '0' &&
errorCode !== 'OK')
);
}
function currentAppCode() {
return selectedAppCode || 'lalu';
}
function paymentAppCode() {
return 'lalu';
}
function currentReturnURL() {
return (
els.returnUrl.value.trim() ||
defaultReturnURL(currentAppCode())
);
}
function defaultReturnURL(appCode) {
var url = new URL(RETURN_URL_BASE);
url.searchParams.set('app_code', appCode || 'lalu');
return url.toString();
}
function setDefaultReturnURL(force) {
var nextURL = defaultReturnURL(currentAppCode());
if (
force ||
!returnUrlTouched ||
isKnownDefaultReturnURL(els.returnUrl.value)
) {
els.returnUrl.value = nextURL;
returnUrlTouched = false;
}
}
function isKnownDefaultReturnURL(value) {
var text = String(value || '').trim();
return APP_CODES.some(function (appCode) {
return text === defaultReturnURL(appCode);
});
}
function renderEnvMeta() {
els.envMeta.textContent =
'Return App' +
currentAppCode() +
' 支付汇率:' +
paymentAppCode() +
' API' +
API_BASE_URL;
}
function dollarsToMinor(value) {
var text = String(value || '').trim();
if (!/^\d+(\.\d{1,2})?$/.test(text)) return 0;
return Math.round(Number(text) * 100);
}
function extractOrderID(value) {
var text = String(value || '').trim();
if (!text) return '';
try {
var url = new URL(text, window.location.href);
var queryID =
url.searchParams.get('order_id') ||
url.searchParams.get('orderId');
if (queryID) return queryID.trim();
var match = url.pathname.match(
/temporary-pay\/([^/]+)\.html$/
);
if (match) return decodeURIComponent(match[1]);
} catch (_) {
// 输入可能就是订单号,不是 URL。
}
return text.replace(/^order_id=/, '').trim();
}
function setLoading(button, loading, text) {
button.disabled = loading;
button.textContent = text;
}
function setMessage(node, text, tone) {
node.textContent = text || '';
node.className = ['message', tone || '']
.filter(Boolean)
.join(' ');
}
function statusText(status) {
return status === 'paid'
? '支付已成功'
: '当前状态:' + statusLabel(status);
}
function statusLabel(status) {
if (status === 'paid') return '已支付';
if (status === 'redirected') return '已生成链接';
if (status === 'pending') return '待支付';
if (status === 'failed') return '失败';
if (status === 'credited') return '已入账';
return status || '-';
}
function formatMoney(value) {
if (!(value === 0 || value)) return '-';
return (Number(value) / 100).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
}
function countryCode(method) {
return String(method.country_code || '')
.trim()
.toUpperCase();
}
function countryLabel(country) {
if (!country) return '';
return country.name && country.name !== country.code
? country.name + ' · ' + country.code
: country.code;
}
function methodLabel(method) {
return [
method.method_name,
method.pay_way,
method.pay_type,
method.provider_name,
]
.filter(Boolean)
.join(' ');
}
function logoText(method) {
var value =
method.method_name ||
method.pay_type ||
method.provider_name ||
method.provider_code ||
'Pay';
return String(value)
.replace(/[^a-z0-9]/gi, '')
.slice(0, 2)
.toUpperCase();
}
function normalizeSearch(value) {
return String(value || '')
.trim()
.toLowerCase();
}
function createNode(tagName, className, text) {
var node = document.createElement(tagName);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
}
function clearNode(node) {
while (node.firstChild) node.removeChild(node.firstChild);
}
function escapeHTML(value) {
return String(value || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function copyText(value, messageNode) {
if (!value) return;
var done = function () {
setMessage(messageNode, '已复制支付链接', 'success');
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard
.writeText(value)
.then(done)
.catch(function () {
fallbackCopy(value);
done();
});
return;
}
fallbackCopy(value);
done();
}
function fallbackCopy(value) {
var input = document.createElement('textarea');
input.value = value;
input.setAttribute('readonly', '');
input.style.position = 'fixed';
input.style.opacity = '0';
document.body.appendChild(input);
input.select();
document.execCommand('copy');
input.remove();
}
})();
</script>
</body>
</html>