// ============================================================ // RechargePage — 积分充值表单 + 支付状态跟踪 // // 通过 PageDispatcher 调度(Modal 容器),导航栏"充值"按钮触发。 // 两阶段视图: // form — 金额选择 → 自定义输入 → 支付方式 → 协议 → 提交 // pending — 订单已创建,轮询支付状态,可重新打开支付页面 // paid — 支付成功 // expired — 订单过期,可重新下单 // failed — 支付失败,可重新下单 // ============================================================ import { useState, useMemo, useCallback, useEffect } from 'react'; import { Radio, InputNumber, Button, Checkbox, Typography, App, Space, Divider, theme as antTheme, Result, } from 'antd'; import { AlipayOutlined, WechatOutlined, WalletOutlined, DollarOutlined, } from '@ant-design/icons'; import { useTheme } from '@/components/providers/ThemeProvider'; import { LegalTextModal } from '@/components/ui/LegalTextModal'; import { PaymentAPI, PAYMENT_METHODS, PAYMENT_STATUS, getPaymentStatusText, type PaymentStatus, } from '@/services/modules/payments'; import { emit, EVENTS } from '@/utils/bus'; const { Text } = Typography; // ---------- 常量 ---------- const PRESET_AMOUNTS = [10, 100, 500, 1000, 5000, 10000] as const; /** 单次充值上限(元) */ const MAX_AMOUNT_YUAN = 100000; /** 订单状态轮询间隔(毫秒) */ const POLL_INTERVAL_MS = 3000; /** 页面阶段 */ type RechargePhase = 'form' | PaymentStatus; // ---------- 组件 ---------- export function RechargePage() { const { isDark } = useTheme(); const { message } = App.useApp(); const { token } = antTheme.useToken(); // ======== 表单状态 ======== const [selectedAmount, setSelectedAmount] = useState(null); const [customAmount, setCustomAmount] = useState(null); const [paymentMethod, setPaymentMethod] = useState(PAYMENT_METHODS.Alipay); const [agreedToTerms, setAgreedToTerms] = useState(false); const [submitting, setSubmitting] = useState(false); const [errors, setErrors] = useState>({}); const [legalOpen, setLegalOpen] = useState(false); // ======== 订单状态 ======== const [phase, setPhase] = useState('form'); const [orderId, setOrderId] = useState(null); const [orderStatus, setOrderStatus] = useState(null); const [payUrl, setPayUrl] = useState(null); // ======== 派生值 ======== /** 最终充值金额(元):自定义金额优先,否则预设金额,否则 0 */ const finalAmountYuan = useMemo(() => { if (customAmount !== null && customAmount !== undefined) { return customAmount; } return selectedAmount ?? 0; }, [customAmount, selectedAmount]); /** 最终充值金额(分):元 × 100,用于 API 调用 */ const finalAmountCent = finalAmountYuan * 100; // ======== 工具函数 ======== const clearError = useCallback((name: string) => { setErrors((prev) => { if (!(name in prev)) return prev; const next = { ...prev }; delete next[name]; return next; }); }, []); // ======== 校验 ======== const validate = useCallback((): Record => { const errs: Record = {}; if (!finalAmountYuan || finalAmountYuan <= 0) { errs.amount = '请选择或输入充值金额'; } else if (!Number.isInteger(finalAmountYuan) || finalAmountYuan % 10 !== 0) { errs.customAmount = '金额必须为 10 的倍数'; } else if (finalAmountYuan > MAX_AMOUNT_YUAN) { errs.customAmount = `单次充值上限为 ${MAX_AMOUNT_YUAN.toLocaleString()} 元`; } if (!agreedToTerms) { errs.agreedToTerms = '请阅读并同意充值协议'; } return errs; }, [finalAmountYuan, agreedToTerms]); // ======== 支付方式切换 ======== const handlePaymentChange = useCallback( (value: string) => { if (value === PAYMENT_METHODS.WeChat) { void message.info('微信支付正在接入中,敬请期待'); return; // 不更新 state,保持支付宝选中 } setPaymentMethod(value); }, [message], ); // ======== 提交流程 ======== const handleSubmit = useCallback(async () => { const validationErrors = validate(); if (Object.keys(validationErrors).length > 0) { setErrors(validationErrors); return; } setSubmitting(true); try { const res = await PaymentAPI.createOrder({ amount_cent: finalAmountCent, payment_channel: PAYMENT_METHODS.Alipay, }); // 保存订单信息,进入等待支付阶段 setOrderId(res.id); setOrderStatus(PAYMENT_STATUS.Pending); setPayUrl(res.pay_url); setPhase(PAYMENT_STATUS.Pending); window.open(res.pay_url, '_blank'); void message.success('订单已创建,请在新页面中完成支付'); } catch (err: unknown) { message.error((err as Error)?.message || '创建订单失败,请重试'); } finally { setSubmitting(false); } }, [finalAmountCent, validate, message]); // ======== 订单状态轮询 ======== useEffect(() => { if (phase !== PAYMENT_STATUS.Pending || !orderId) return; let timer: ReturnType; let cancelled = false; const poll = async () => { try { const order = await PaymentAPI.getOrderInfo({ order_id: orderId }); if (cancelled) return; const newStatus = order.status; setOrderStatus(newStatus); if (newStatus === PAYMENT_STATUS.Pending) { // 仍待支付 → 继续轮询 timer = setTimeout(poll, POLL_INTERVAL_MS); } else { // 终态 → 停止轮询,切换阶段 setPhase(newStatus); if (newStatus === PAYMENT_STATUS.Paid) { void message.success('支付成功,积分已到账'); } } } catch { // 网络异常 → 延长间隔后继续轮询 if (!cancelled) timer = setTimeout(poll, POLL_INTERVAL_MS * 2); } }; timer = setTimeout(poll, POLL_INTERVAL_MS); return () => { cancelled = true; clearTimeout(timer); }; }, [phase, orderId, message]); // ======== 页面锁定(待支付时禁止关闭容器) ======== useEffect(() => { const locked = phase === PAYMENT_STATUS.Pending; emit(EVENTS.PAGE_LOCK, { locked }); // 组件卸载时确保解锁(防止锁定残留) return () => { if (locked) emit(EVENTS.PAGE_LOCK, { locked: false }); }; }, [phase]); // ======== 操作回调 ======== /** 重新下单:回到表单,保留之前填写的金额 */ const handleRetry = useCallback(() => { setPhase('form'); setOrderId(null); setOrderStatus(null); setPayUrl(null); }, []); /** 关闭充值页面 */ const handleClose = useCallback(() => { emit(EVENTS.CLOSE_PAGE); }, []); // ================================================================ // 渲染:支付状态视图(非 form 阶段) // ================================================================ if (phase !== 'form') { const statusText = orderStatus ? getPaymentStatusText(orderStatus) : ''; const isPending = phase === PAYMENT_STATUS.Pending; const isPaid = phase === PAYMENT_STATUS.Paid; const isTerminal = !isPending; return ( <>
{/* ---- 订单信息卡片 ---- */} {isPending && (
订单编号:{orderId} 充值金额: ¥{finalAmountYuan.toLocaleString()} 元 到账积分: {finalAmountYuan.toLocaleString()} 积分
)} {/* ---- 操作按钮 ---- */} {isPending && payUrl && ( )} {isTerminal && !isPaid && ( )}
{/* ---- 协议弹窗(待支付阶段仍可查看) ---- */} setLegalOpen(false)} /> ); } // ================================================================ // 渲染:表单视图(form 阶段) // ================================================================ return ( <>
{/* ---- 页面副标题 ---- */} 选择充值金额或输入自定义金额(元) {/* ---- Part 1:预设金额单选组 ---- */}
{ setSelectedAmount(e.target.value); setCustomAmount(null); clearError('amount'); }} optionType="button" buttonStyle="solid" style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }} > {PRESET_AMOUNTS.map((amt) => ( {amt} 元 ))} {errors.amount && ( {errors.amount} )}
{/* ---- Part 2:自定义金额输入 ---- */}
{ setCustomAmount(val); setSelectedAmount(null); clearError('customAmount'); }} min={10} step={10} max={MAX_AMOUNT_YUAN} placeholder="输入自定义金额(10的倍数)" style={{ flex: 1 }} size="large" precision={0} /> {errors.customAmount && ( {errors.customAmount} )}
{/* ---- Part 3:到账信息 + 支付金额 ---- */} {finalAmountYuan > 0 && (
到账积分: {finalAmountYuan.toLocaleString()} 积分 支付金额: ¥{finalAmountYuan.toLocaleString()} {' '}元
)} {/* ---- Part 4:支付方式 ---- */}
选择支付方式 handlePaymentChange(e.target.value)} optionType="button" buttonStyle="solid" > 支付宝 微信支付
{/* ---- Part 5:充值协议 ---- */}
{ setAgreedToTerms(e.target.checked); clearError('agreedToTerms'); }} > 我已阅读并同意 {errors.agreedToTerms && ( {errors.agreedToTerms} )}
{/* ---- Part 6:操作按钮 ---- */}
{/* ---- 协议弹窗 ---- */} setLegalOpen(false)} /> ); }