// ============================================================ // VipPage — VIP 会员开通页面 // // 通过 PageDispatcher 调度(Modal 容器),导航栏"开会员/激活"按钮触发。 // 四部分布局: // 激活码 — Space.Compact 输入框 + 激活按钮 // 套餐选择 — Radio.Group 卡片式单选,API 获取套餐列表 // 支付协议 — Checkbox + LegalTextModal(用户服务协议) + 确认开通 // 权益说明 — 选中套餐的大图展示 // ============================================================ import {useState, useCallback, useEffect, useRef} from 'react'; import { logger } from '@/shared/utils/bus'; import { Radio, Button, Checkbox, Input, Image, Typography, App, Space, Divider, theme as antTheme, Spin, Result, } from 'antd'; import {CrownOutlined, GiftOutlined, DollarOutlined} from '@ant-design/icons'; import {useTheme} from '@/app/providers/ThemeProvider'; import {LegalTextModal} from '@/shared/components/LegalTextModal'; import { VipAPI, VipOrderStatus, type VipLevelsItem, type VipOrderStatusValue, } from '@/modules/account/controllers/vip-api'; import {centToYuan} from '@/shared/utils/display'; import {EnumResolver} from '@/shared/utils/enum-resolver'; import {emit, EVENTS} from '@/shared/utils/bus'; import {RequestError, RequestErrorType} from '@/shared/services/http'; const {Text} = Typography; // ---------- 常量 ---------- /** 订单状态轮询间隔(毫秒) */ const POLL_INTERVAL_MS = 3000; /** 页面阶段 */ type VipPhase = 'form' | 'activate' | VipOrderStatusValue; /** 状态 → Result status 映射 */ function getResultStatus(status: VipOrderStatusValue): 'success' | 'info' | 'error' { if (status === VipOrderStatus.Paid) return 'success'; if (status === VipOrderStatus.Pending) return 'info'; return 'error'; } // ---------- 组件 ---------- export function VipPage() { const {isDark} = useTheme(); const {message} = App.useApp(); const {token} = antTheme.useToken(); // ======== 套餐列表 ======== const [levels, setLevels] = useState([]); const [loading, setLoading] = useState(true); const [fetchError, setFetchError] = useState(null); const abortRef = useRef(null); const mountedRef = useRef(true); // ======== 表单状态 ======== const [selectedLevelId, setSelectedLevelId] = useState(null); const [agreedToTerms, setAgreedToTerms] = useState(false); const [submitting, setSubmitting] = useState(false); const [errors, setErrors] = useState>({}); const [legalOpen, setLegalOpen] = useState(false); // ======== 激活码 ======== const [activateCode, setActivateCode] = useState(''); const [activating, setActivating] = useState(false); // ======== 支付订单状态 ======== const [phase, setPhase] = useState('form'); const [vipOrderId, setVipOrderId] = useState(null); const [vipOrderStatus, setVipOrderStatus] = useState(null); const [vipPayUrl, setVipPayUrl] = useState(null); // ======== 工具函数 ======== const clearError = useCallback((name: string) => { setErrors((prev) => { if (!(name in prev)) return prev; const next = {...prev}; delete next[name]; return next; }); }, []); // ======== 获取套餐列表 ======== const fetchLevels = useCallback(async () => { abortRef.current?.abort(); const controller = new AbortController(); abortRef.current = controller; setLoading(true); setFetchError(null); try { const data = await VipAPI.getVipLevels(controller.signal); if (mountedRef.current) { setLevels(data.filter((l) => l.is_active && l.image)); } } catch (err) { // axios 拦截器将取消错误转为 RequestError(CANCELLED),非原始 AbortError if (err instanceof RequestError && err.type === RequestErrorType.CANCELLED) return; logger.warn('ui', '加载VIP套餐列表失败', err instanceof Error ? err : new Error(String(err))); if (mountedRef.current) { setFetchError((err as Error)?.message || '加载套餐列表失败'); } } finally { if (mountedRef.current) { setLoading(false); } } }, []); useEffect(() => { mountedRef.current = true; void fetchLevels(); return () => { mountedRef.current = false; abortRef.current?.abort(); }; }, [fetchLevels]); // ======== 校验 ======== const validate = useCallback((): Record => { const errs: Record = {}; if (selectedLevelId === null) { errs.selectedLevel = '请选择会员套餐'; } if (!agreedToTerms) { errs.agreedToTerms = '请阅读并同意用户服务协议'; } return errs; }, [selectedLevelId, agreedToTerms]); // ======== 提交 ======== const handleSubmit = useCallback(async () => { const validationErrors = validate(); if (Object.keys(validationErrors).length > 0) { setErrors(validationErrors); return; } setSubmitting(true); try { const res = await VipAPI.createVipOrder({ vip_level_id: selectedLevelId!, payment_channel: 'alipay', }); // 保存订单信息,进入待支付阶段 setVipOrderId(res.id); setVipOrderStatus(VipOrderStatus.Pending); setVipPayUrl(res.pay_url); setPhase(VipOrderStatus.Pending); window.open(res.pay_url ?? '', '_blank'); void message.success('订单已创建,请在新页面中完成支付'); } catch (err: unknown) { logger.warn('ui', 'VIP创建订单失败', err instanceof Error ? err : new Error(String(err))); message.error((err as Error)?.message || '创建订单失败,请重试'); } finally { setSubmitting(false); } }, [selectedLevelId, validate, message]); // ======== 激活码提交 ======== const handleActivate = useCallback(async () => { const code = activateCode.trim(); if (!code) { message.warning('请输入激活码'); return; } setActivating(true); try { await VipAPI.activateVipCode({code}); void message.success('激活成功!VIP 权益已生效'); emit(EVENTS.CLOSE_PAGE); } catch (err: unknown) { logger.warn('ui', 'VIP激活失败', err instanceof Error ? err : new Error(String(err))); message.error((err as Error)?.message || '激活失败,请检查激活码是否正确'); } finally { setActivating(false); } }, [activateCode, message]); // ======== 操作回调 ======== // ======== 订单状态轮询 ======== useEffect(() => { if (phase !== VipOrderStatus.Pending || !vipOrderId) return; let timer: ReturnType; let cancelled = false; const poll = async () => { try { const order = await VipAPI.getVipOrder(vipOrderId); if (cancelled) return; const newStatus = order.status; setVipOrderStatus(newStatus); if (newStatus === VipOrderStatus.Pending) { timer = setTimeout(poll, POLL_INTERVAL_MS); } else { setPhase(newStatus); if (newStatus === VipOrderStatus.Paid) { void message.success('支付成功!VIP 权益已生效'); } } } catch { if (!cancelled) timer = setTimeout(poll, POLL_INTERVAL_MS * 2); } }; timer = setTimeout(poll, POLL_INTERVAL_MS); return () => { cancelled = true; clearTimeout(timer); }; }, [phase, vipOrderId, message]); // ======== 页面锁定(待支付时禁止关闭容器) ======== useEffect(() => { const locked = phase === VipOrderStatus.Pending; emit(EVENTS.PAGE_LOCK, { locked }); return () => { if (locked) emit(EVENTS.PAGE_LOCK, { locked: false }); }; }, [phase]); // ======== 操作回调 ======== /** 重新下单:回到表单,保留套餐选择 */ const handleRetry = useCallback(() => { setPhase('form'); setVipOrderId(null); setVipOrderStatus(null); setVipPayUrl(null); }, []); const handleClose = useCallback(() => { emit(EVENTS.CLOSE_PAGE); }, []); // ================================================================ // 渲染:加载态 // ================================================================ if (loading) { return (
加载套餐列表…
); } // ================================================================ // 渲染:错误态 // ================================================================ if (fetchError) { return (
重试 } />
); } // ================================================================ // 渲染:支付状态视图(非 form / activate 阶段) // ================================================================ if (phase !== 'form' && phase !== 'activate') { const statusText = vipOrderStatus ? EnumResolver.vip.orderStatus.label(vipOrderStatus) : ''; const isPending = phase === VipOrderStatus.Pending; const isPaid = phase === VipOrderStatus.Paid; return ( <>
{/* ---- 订单信息卡片 ---- */} {isPending && (
订单编号:{vipOrderId} 支付金额: ¥{(() => { const lvl = levels.find((l) => l.id === selectedLevelId); return lvl ? centToYuan(lvl.price_cent) : '—'; })()} 套餐: {levels.find((l) => l.id === selectedLevelId)?.name ?? '—'}
)} {/* ---- 操作按钮 ---- */} {isPending && vipPayUrl && ( )} {!isPending && !isPaid && ( )}
{/* ---- 协议弹窗(待支付阶段仍可查看) ---- */} setLegalOpen(false)} /> ); } // ================================================================ // 渲染:正常 // ================================================================ return ( <>
使用激活码 setActivateCode(e.target.value)} placeholder="输入 VIP 激活码" allowClear onPressEnter={() => void handleActivate()} style={{flex: 1}} />
选择会员套餐 {/* 双列布局:左列套餐选择 | 右列权益图库 */}
{/* ---- 左列:套餐选择 ---- */}
{ setSelectedLevelId(e.target.value); clearError('selectedLevel'); }} style={{width: '100%'}} > {levels.map((level) => (
{level.name} ¥{centToYuan(level.price_cent)}
{level.duration_days}天 {level.gift_balance_cent > 0 && ( 赠{centToYuan(level.gift_balance_cent)}积分 )} {EnumResolver.vip.targetAccountType.label(level.target_account_type)}
{level.description && ( {level.description} )}
))}
{errors.selectedLevel && ( {errors.selectedLevel} )}
{/* ---- 右列:权益图库 ---- */}
权益说明(点击放大) {levels.map((level) => (
{`${level.name}
))}
{/* ---- Part 2:支付协议 + 确认开通 ---- */}
{ setAgreedToTerms(e.target.checked); clearError('agreedToTerms'); }} > 我已阅读并同意 {errors.agreedToTerms && ( {errors.agreedToTerms} )}
{/* 操作按钮 */}
{/* ---- 协议弹窗 ---- */} setLegalOpen(false)} /> ); }