4b529a8952
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m59s
Project CI / Backend tests (pull_request) Failing after 4m8s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m9s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m20s
Project CI / Frontend tests (pull_request) Successful in 3m17s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m48s
Project CI / Repository checks (pull_request) Failing after 4m15s
Project CI / Native shell tests (pull_request) Successful in 6m52s
新增支付应用、API Key 与外部产品订单接口 接入微信 Native 下单、二维码收银台与支付结果回调 新增后台支付订单与回调投递页面 补齐 SpacetimeDB 支付表、生成绑定、OpenAPI 与使用说明
200 lines
5.9 KiB
TypeScript
200 lines
5.9 KiB
TypeScript
import {
|
|
CheckCircle2,
|
|
Clock3,
|
|
Copy,
|
|
LoaderCircle,
|
|
ShieldCheck,
|
|
} from 'lucide-react';
|
|
import QRCode from 'qrcode';
|
|
import { useEffect, useState } from 'react';
|
|
|
|
import type { PaymentOrder } from '../../../packages/shared/src/contracts/payment';
|
|
import { getPublicPaymentCheckout } from '../../services/payment/paymentCheckoutClient';
|
|
|
|
type PaymentCheckoutViewProps = {
|
|
checkoutToken: string;
|
|
};
|
|
|
|
export function PaymentCheckoutView({
|
|
checkoutToken,
|
|
}: PaymentCheckoutViewProps) {
|
|
const [order, setOrder] = useState<PaymentOrder | null>(null);
|
|
const [qrDataUrl, setQrDataUrl] = useState('');
|
|
const [error, setError] = useState('');
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
const load = async () => {
|
|
try {
|
|
const response = await getPublicPaymentCheckout(checkoutToken);
|
|
if (!cancelled) {
|
|
setOrder(response.order);
|
|
setError('');
|
|
}
|
|
} catch (nextError: unknown) {
|
|
if (!cancelled) {
|
|
setError(
|
|
nextError instanceof Error ? nextError.message : '读取支付订单失败',
|
|
);
|
|
}
|
|
}
|
|
};
|
|
void load();
|
|
const timer = window.setInterval(() => {
|
|
if (
|
|
order?.status === 'paid' ||
|
|
order?.status === 'closed' ||
|
|
order?.status === 'expired'
|
|
) {
|
|
return;
|
|
}
|
|
void load();
|
|
}, 5000);
|
|
return () => {
|
|
cancelled = true;
|
|
window.clearInterval(timer);
|
|
};
|
|
}, [checkoutToken, order?.status]);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
if (!order?.providerQrCode) {
|
|
setQrDataUrl('');
|
|
return;
|
|
}
|
|
QRCode.toDataURL(order.providerQrCode, {
|
|
width: 320,
|
|
margin: 2,
|
|
color: { dark: '#2c2723', light: '#ffffff' },
|
|
}).then((dataUrl) => {
|
|
if (!cancelled) {
|
|
setQrDataUrl(dataUrl);
|
|
}
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [order?.providerQrCode]);
|
|
|
|
const copyAmount = () => {
|
|
if (order) {
|
|
void navigator.clipboard?.writeText((order.amountCents / 100).toFixed(2));
|
|
}
|
|
};
|
|
|
|
if (error) {
|
|
return (
|
|
<main className="payment-checkout-page">
|
|
<div className="payment-checkout-error">{error}</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
if (!order) {
|
|
return (
|
|
<main className="payment-checkout-page">
|
|
<div className="payment-checkout-loading">
|
|
<LoaderCircle className="h-5 w-5 animate-spin" />
|
|
正在读取支付订单...
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
const amount = (order.amountCents / 100).toFixed(2);
|
|
const isPaid = order.status === 'paid';
|
|
const isClosed = order.status === 'closed' || order.status === 'expired';
|
|
|
|
return (
|
|
<main className="payment-checkout-page">
|
|
<div className="payment-checkout-shell">
|
|
<section className="payment-checkout-summary">
|
|
<div className="payment-checkout-brand">
|
|
<span>¥</span>
|
|
<div>
|
|
<strong>扫码付款</strong>
|
|
<small>支付成功后自动更新订单状态</small>
|
|
</div>
|
|
</div>
|
|
<div className="payment-checkout-code">
|
|
订单号 <strong>{order.merchantOrderId}</strong>
|
|
</div>
|
|
<h1>{order.title}</h1>
|
|
<p className="payment-checkout-description">
|
|
请核对商品与订单信息,使用右侧二维码完成支付。
|
|
</p>
|
|
<div className="payment-checkout-alert">
|
|
<strong>付款金额说明</strong>
|
|
<span>
|
|
请完全按照页面显示的金额付款,少付或多付都可能导致支付无法自动确认。
|
|
</span>
|
|
</div>
|
|
<div className="payment-checkout-state">
|
|
{isPaid ? (
|
|
<CheckCircle2 className="text-emerald-600" />
|
|
) : (
|
|
<Clock3 className="text-amber-500" />
|
|
)}
|
|
<div>
|
|
<strong>
|
|
{isPaid ? '支付成功' : isClosed ? '订单已关闭' : '等待支付'}
|
|
</strong>
|
|
<span>
|
|
{isPaid
|
|
? '平台已确认到账'
|
|
: isClosed
|
|
? '请重新创建订单'
|
|
: '完成付款后页面会自动更新状态'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className="payment-checkout-copy"
|
|
onClick={copyAmount}
|
|
>
|
|
<Copy className="h-4 w-4" />
|
|
复制金额
|
|
</button>
|
|
</section>
|
|
<section className="payment-checkout-qr-panel">
|
|
<div className="payment-checkout-qr-title">
|
|
<strong>扫码付款</strong>
|
|
<span>{isPaid ? '已完成' : isClosed ? '已关闭' : '支付中'}</span>
|
|
</div>
|
|
<div className="payment-checkout-amount">
|
|
<small>应付金额</small>
|
|
<strong>¥{amount}</strong>
|
|
</div>
|
|
{isPaid ? (
|
|
<div className="payment-checkout-success">
|
|
<CheckCircle2 className="h-12 w-12" />
|
|
<strong>支付成功</strong>
|
|
</div>
|
|
) : isClosed ? (
|
|
<div className="payment-checkout-success payment-checkout-success--muted">
|
|
<Clock3 className="h-12 w-12" />
|
|
<strong>订单已关闭</strong>
|
|
</div>
|
|
) : qrDataUrl ? (
|
|
<img
|
|
className="payment-checkout-qr"
|
|
src={qrDataUrl}
|
|
alt="支付二维码"
|
|
/>
|
|
) : (
|
|
<div className="payment-checkout-qr-placeholder">
|
|
<LoaderCircle className="h-6 w-6 animate-spin" />
|
|
</div>
|
|
)}
|
|
<div className="payment-checkout-provider">
|
|
<ShieldCheck className="h-4 w-4" />
|
|
微信支付
|
|
</div>
|
|
<p className="payment-checkout-hint">请使用微信扫一扫完成支付</p>
|
|
</section>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|