Files
Genarrative/src/components/platform-entry/PaymentCheckoutView.tsx
T
kdletters 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 与使用说明
2026-10-03 17:18:13 +08:00

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>
);
}