750 lines
28 KiB
HTML
750 lines
28 KiB
HTML
<!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>
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
background: var(--hy-theme-bg);
|
||
color: var(--hy-theme-text);
|
||
font-family:
|
||
-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||
}
|
||
|
||
button,
|
||
input,
|
||
select,
|
||
textarea {
|
||
font: inherit;
|
||
}
|
||
|
||
.page {
|
||
width: min(1120px, 100%);
|
||
margin: 0 auto;
|
||
padding: 28px 18px 42px;
|
||
}
|
||
|
||
.header {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
margin-bottom: 18px;
|
||
}
|
||
|
||
.title {
|
||
margin: 0;
|
||
font-size: 24px;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.meta {
|
||
margin: 6px 0 0;
|
||
color: var(--hy-theme-muted);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.grid {
|
||
display: grid;
|
||
gap: 16px;
|
||
grid-template-columns: minmax(0, 1fr) minmax(320px, 0.75fr);
|
||
}
|
||
|
||
.panel {
|
||
border: 1px solid var(--hy-theme-line);
|
||
border-radius: 14px;
|
||
background: var(--hy-theme-surface);
|
||
box-shadow: var(--hy-theme-shadow);
|
||
padding: 18px;
|
||
}
|
||
|
||
.panel h2 {
|
||
margin: 0 0 16px;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.form-grid {
|
||
display: grid;
|
||
gap: 14px;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.field {
|
||
display: flex;
|
||
min-width: 0;
|
||
flex-direction: column;
|
||
gap: 7px;
|
||
}
|
||
|
||
.field.full {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
label {
|
||
color: var(--hy-theme-muted);
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
input,
|
||
select,
|
||
textarea {
|
||
width: 100%;
|
||
min-height: 44px;
|
||
border: 1px solid var(--hy-theme-line);
|
||
border-radius: 10px;
|
||
outline: none;
|
||
background: var(--hy-theme-surface-soft);
|
||
color: var(--hy-theme-text);
|
||
padding: 10px 12px;
|
||
}
|
||
|
||
textarea {
|
||
min-height: 86px;
|
||
resize: vertical;
|
||
}
|
||
|
||
input:focus,
|
||
select:focus,
|
||
textarea:focus {
|
||
border-color: var(--hy-theme-primary-strong);
|
||
box-shadow: 0 0 0 4px var(--hy-theme-focus-ring);
|
||
}
|
||
|
||
.actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.button {
|
||
min-height: 42px;
|
||
border: 1px solid transparent;
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
padding: 0 16px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.button.primary {
|
||
background: var(--hy-theme-button);
|
||
color: #fff;
|
||
box-shadow: var(--hy-theme-button-shadow);
|
||
}
|
||
|
||
.button.secondary {
|
||
border-color: var(--hy-theme-line);
|
||
background: var(--hy-theme-surface-soft);
|
||
color: var(--hy-theme-primary-deep);
|
||
}
|
||
|
||
.button:disabled {
|
||
cursor: not-allowed;
|
||
background: var(--hy-theme-button-disabled);
|
||
color: var(--hy-theme-button-disabled-text);
|
||
box-shadow: var(--hy-theme-button-disabled-shadow);
|
||
}
|
||
|
||
.result {
|
||
display: grid;
|
||
gap: 12px;
|
||
}
|
||
|
||
.result-row {
|
||
display: grid;
|
||
min-width: 0;
|
||
gap: 4px;
|
||
}
|
||
|
||
.result-row strong {
|
||
color: var(--hy-theme-muted);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.value {
|
||
min-width: 0;
|
||
overflow-wrap: anywhere;
|
||
border-radius: 10px;
|
||
background: var(--hy-theme-primary-soft);
|
||
padding: 10px 12px;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
.status {
|
||
display: inline-flex;
|
||
width: fit-content;
|
||
align-items: center;
|
||
border-radius: 999px;
|
||
background: var(--hy-theme-primary-soft);
|
||
color: var(--hy-theme-primary-deep);
|
||
padding: 5px 10px;
|
||
font-size: 13px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.status.paid {
|
||
background: rgba(49, 198, 168, 0.14);
|
||
color: var(--hy-theme-success);
|
||
}
|
||
|
||
.status.failed {
|
||
background: rgba(217, 93, 114, 0.12);
|
||
color: var(--hy-theme-danger);
|
||
}
|
||
|
||
.message {
|
||
min-height: 22px;
|
||
margin-top: 12px;
|
||
color: var(--hy-theme-muted);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.message.error {
|
||
color: var(--hy-theme-danger);
|
||
}
|
||
|
||
.message.success {
|
||
color: var(--hy-theme-success);
|
||
}
|
||
|
||
@media (max-width: 820px) {
|
||
.header {
|
||
align-items: flex-start;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.grid,
|
||
.form-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main class="page">
|
||
<header class="header">
|
||
<div>
|
||
<h1 class="title">三方临时支付链接</h1>
|
||
<p class="meta" id="envMeta">加载环境中...</p>
|
||
</div>
|
||
<button
|
||
class="button secondary"
|
||
type="button"
|
||
id="reloadMethods"
|
||
>
|
||
刷新支付方式
|
||
</button>
|
||
</header>
|
||
|
||
<section class="grid">
|
||
<div class="panel">
|
||
<h2>创建支付链接</h2>
|
||
<form id="createForm">
|
||
<div class="form-grid">
|
||
<div class="field full">
|
||
<label for="paymentMethod">支付方式</label>
|
||
<select id="paymentMethod" required>
|
||
<option value="">
|
||
正在加载支付方式...
|
||
</option>
|
||
</select>
|
||
</div>
|
||
<div class="field">
|
||
<label for="coinAmount">金币数量</label>
|
||
<input
|
||
id="coinAmount"
|
||
inputmode="numeric"
|
||
min="1"
|
||
placeholder="例如 1000"
|
||
required
|
||
type="number"
|
||
/>
|
||
</div>
|
||
<div class="field">
|
||
<label for="usdAmount">美金金额</label>
|
||
<input
|
||
id="usdAmount"
|
||
inputmode="decimal"
|
||
min="0.01"
|
||
placeholder="例如 9.99"
|
||
required
|
||
step="0.01"
|
||
type="number"
|
||
/>
|
||
</div>
|
||
<div class="field full">
|
||
<label for="returnUrl">支付完成返回地址</label>
|
||
<input
|
||
id="returnUrl"
|
||
placeholder="不填则返回当前页面"
|
||
type="url"
|
||
/>
|
||
</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">
|
||
<h2>创建结果</h2>
|
||
<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">
|
||
<h2>验证支付链接</h2>
|
||
<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 class="panel">
|
||
<h2>验证结果</h2>
|
||
<div class="result" id="verifyResult">
|
||
<div class="value">还没有验证订单</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<script src="../common/api.js"></script>
|
||
<script>
|
||
(function () {
|
||
var methods = [];
|
||
var latestPaymentLink = '';
|
||
var latestOrderID = '';
|
||
var els = {
|
||
coinAmount: document.getElementById('coinAmount'),
|
||
copyLink: document.getElementById('copyLink'),
|
||
createButton: document.getElementById('createButton'),
|
||
createForm: document.getElementById('createForm'),
|
||
createMessage: document.getElementById('createMessage'),
|
||
createResult: document.getElementById('createResult'),
|
||
envMeta: document.getElementById('envMeta'),
|
||
openLink: document.getElementById('openLink'),
|
||
paymentMethod: document.getElementById('paymentMethod'),
|
||
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'),
|
||
};
|
||
|
||
els.envMeta.textContent =
|
||
'App:' +
|
||
window.HyAppAPI.getAppCode() +
|
||
' | API:' +
|
||
window.HyAppAPI.baseURL();
|
||
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);
|
||
loadMethods();
|
||
|
||
function loadMethods() {
|
||
setMessage(els.createMessage, '正在加载支付方式...', '');
|
||
els.paymentMethod.disabled = true;
|
||
return window.HyAppAPI.get(
|
||
'/api/v1/recharge/h5/temporary/methods'
|
||
)
|
||
.then(function (data) {
|
||
methods = Array.isArray(data.methods)
|
||
? data.methods
|
||
: [];
|
||
renderMethods();
|
||
setMessage(
|
||
els.createMessage,
|
||
methods.length
|
||
? '支付方式已加载'
|
||
: '没有可用支付方式',
|
||
methods.length ? 'success' : 'error'
|
||
);
|
||
})
|
||
.catch(function (err) {
|
||
methods = [];
|
||
renderMethods();
|
||
setMessage(
|
||
els.createMessage,
|
||
err.message || '加载支付方式失败',
|
||
'error'
|
||
);
|
||
});
|
||
}
|
||
|
||
function renderMethods() {
|
||
els.paymentMethod.innerHTML = '';
|
||
if (!methods.length) {
|
||
appendOption('', '没有可用支付方式');
|
||
els.paymentMethod.disabled = true;
|
||
return;
|
||
}
|
||
appendOption('', '请选择支付方式');
|
||
methods.forEach(function (method) {
|
||
var label = [
|
||
method.provider_name || method.provider_code,
|
||
method.method_name || method.pay_way,
|
||
method.pay_type,
|
||
method.currency_code,
|
||
method.country_code,
|
||
]
|
||
.filter(Boolean)
|
||
.join(' / ');
|
||
appendOption(String(method.method_id), label);
|
||
});
|
||
els.paymentMethod.disabled = false;
|
||
}
|
||
|
||
function appendOption(value, label) {
|
||
var option = document.createElement('option');
|
||
option.value = value;
|
||
option.textContent = label;
|
||
els.paymentMethod.appendChild(option);
|
||
}
|
||
|
||
function createPaymentLink(event) {
|
||
event.preventDefault();
|
||
var method = selectedMethod();
|
||
var coinAmount = parseInt(els.coinAmount.value, 10);
|
||
var usdMinorAmount = dollarsToMinor(els.usdAmount.value);
|
||
if (
|
||
!method ||
|
||
!coinAmount ||
|
||
coinAmount <= 0 ||
|
||
!usdMinorAmount ||
|
||
usdMinorAmount <= 0
|
||
) {
|
||
setMessage(
|
||
els.createMessage,
|
||
'请选择支付方式,并填写大于 0 的金币和美金金额',
|
||
'error'
|
||
);
|
||
return;
|
||
}
|
||
setLoading(els.createButton, true, '创建中...');
|
||
setMessage(els.createMessage, '正在创建支付链接...', '');
|
||
window.HyAppAPI.post(
|
||
'/api/v1/recharge/h5/temporary/orders',
|
||
{
|
||
coin_amount: coinAmount,
|
||
usd_minor_amount: usdMinorAmount,
|
||
provider_code: method.provider_code,
|
||
payment_method_id: method.method_id,
|
||
return_url:
|
||
els.returnUrl.value.trim() ||
|
||
window.location.href,
|
||
}
|
||
)
|
||
.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,
|
||
'正在向后端查询支付状态...',
|
||
''
|
||
);
|
||
window.HyAppAPI.get(
|
||
'/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('订单号', order.order_id || '-'),
|
||
resultRow(
|
||
'支付状态',
|
||
'<span class="status ' +
|
||
escapeHTML(order.status || '') +
|
||
'">' +
|
||
escapeHTML(statusLabel(order.status)) +
|
||
'</span>'
|
||
),
|
||
resultRow(
|
||
'金币/美金',
|
||
escapeHTML(
|
||
formatNumber(order.coin_amount) +
|
||
' 金币 / USD ' +
|
||
formatMoney(order.usd_minor_amount)
|
||
)
|
||
),
|
||
resultRow(
|
||
'支付方式',
|
||
escapeHTML(
|
||
[
|
||
order.provider_code,
|
||
order.pay_way,
|
||
order.pay_type,
|
||
]
|
||
.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 selectedMethod() {
|
||
var id = Number(els.paymentMethod.value || 0);
|
||
return methods.find(function (method) {
|
||
return Number(method.method_id) === id;
|
||
});
|
||
}
|
||
|
||
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 formatNumber(value) {
|
||
if (value === 0 || value)
|
||
return Number(value).toLocaleString('zh-CN');
|
||
return '-';
|
||
}
|
||
|
||
function formatMoney(value) {
|
||
if (!(value === 0 || value)) return '-';
|
||
return (Number(value) / 100).toLocaleString('zh-CN', {
|
||
minimumFractionDigits: 2,
|
||
maximumFractionDigits: 2,
|
||
});
|
||
}
|
||
|
||
function escapeHTML(value) {
|
||
return String(value || '')
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
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>
|