import { useState, useCallback, useEffect, useMemo, useRef } from 'react'; import { Card, Table, Button, Typography, Tag, Segmented, Result, App, theme as antTheme, } from 'antd'; import { DollarOutlined, ReloadOutlined } from '@ant-design/icons'; import { PaymentAPI, PAYMENT_STATUS, type PaymentStatus, type OrderListResponseBody, type OrderInfoResponseBody, } from '@/services/modules/payments'; import { VipAPI, VipOrderStatus, type VipOrderStatusValue, type VipOrderRead, } from '@/services/modules/vip-api'; import { centToYuan, formatDate } from '@/utils/display'; import { EnumResolver } from '@/utils/enum-resolver'; import { RequestError, RequestErrorType } from '@/services/request'; const { Text } = Typography; // ---------- 常量 ---------- /** 订单状态轮询间隔(毫秒) */ const POLL_INTERVAL_MS = 3000; /** 状态筛选项 */ const STATUS_FILTERS = [ { label: '全部', value: 'all' }, { label: '待支付', value: PAYMENT_STATUS.Pending }, { label: '已支付', value: PAYMENT_STATUS.Paid }, { label: '已过期', value: PAYMENT_STATUS.Expired }, { label: '支付失败', value: PAYMENT_STATUS.Failed }, ]; /** 订单类型 */ type OrderType = 'recharge' | 'vip'; const ORDER_TYPE_OPTIONS = [ { label: '充值订单', value: 'recharge' as OrderType }, { label: 'VIP订单', value: 'vip' as OrderType }, ]; // ---------- 组件 ---------- export function OrdersPage() { const { message } = App.useApp(); const { token } = antTheme.useToken(); // ======== 筛选 & 分页 ======== const [orderType, setOrderType] = useState('recharge'); const [statusFilter, setStatusFilter] = useState('all'); const [pagination, setPagination] = useState({ page: 1, page_size: 10 }); // ======== 数据状态 ======== const [rechargeOrders, setRechargeOrders] = useState([]); const [vipOrders, setVipOrders] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // 轮询定时器 Map(orderId → timer) const pollingRef = useRef>>(new Map()); // 组件是否已挂载 const mountedRef = useRef(true); // fetchOrders 的 AbortController(切换筛选/分页时取消旧请求) const abortRef = useRef(null); // ======== 数据获取 ======== const fetchOrders = useCallback(async () => { // 取消上一次未完成的请求(防竞态) abortRef.current?.abort(); const controller = new AbortController(); abortRef.current = controller; setLoading(true); setError(null); try { if (orderType === 'recharge') { const params: { page: number; page_size: number; status?: PaymentStatus } = { page: pagination.page, page_size: pagination.page_size, }; if (statusFilter !== 'all') { params.status = statusFilter as PaymentStatus; } const data = await PaymentAPI.getOrdersListInfo(params, controller.signal); if (mountedRef.current) { setRechargeOrders(data); } } else { const params: { page?: number; page_size?: number; status?: VipOrderStatusValue } = { page: pagination.page, page_size: pagination.page_size, }; if (statusFilter !== 'all') { params.status = statusFilter as VipOrderStatusValue; } const data = await VipAPI.getVipOrders(params); if (mountedRef.current) { setVipOrders(data); } } } catch (err) { // axios 拦截器将取消错误转为 RequestError(CANCELLED),非原始 AbortError if (err instanceof RequestError && err.type === RequestErrorType.CANCELLED) return; if (mountedRef.current) { setError((err as Error)?.message || '加载订单列表失败'); } } finally { if (mountedRef.current) { setLoading(false); } } }, [pagination, statusFilter, orderType]); // 筛选或分页变化 → 重新获取 useEffect(() => { fetchOrders(); }, [fetchOrders]); // 挂载/卸载管理 useEffect(() => { mountedRef.current = true; const polling = pollingRef.current; const abort = abortRef.current; return () => { mountedRef.current = false; // 清理所有轮询定时器 polling.forEach((timer) => clearTimeout(timer)); polling.clear(); // 取消进行中的列表请求 abort?.abort(); }; }, []); // ======== 单订单轮询 ======== /** * 单订单轮询:对指定 orderId 启动轮询,每 POLL_INTERVAL_MS 查一次 * - 幂等启动:取消同一订单的旧轮询再创建新的 * - 局部更新 orders state,终态时调 fetchOrders 保证一致性 * - 网络异常时延长间隔(2×)重试 * - 通过 mountedRef 防止卸载后 setState */ const startPolling = useCallback((orderId: string) => { // ① 取消同一订单的旧轮询(幂等启动,避免重复轮询) const existing = pollingRef.current.get(orderId); if (existing) { clearTimeout(existing); pollingRef.current.delete(orderId); } let cancelled = false; const poll = async () => { if (cancelled || !mountedRef.current) return; try { const order: OrderInfoResponseBody = await PaymentAPI.getOrderInfo({ order_id: orderId }); if (cancelled || !mountedRef.current) return; const newStatus = order.status; // 局部更新 orders state(merge 最新订单信息) setRechargeOrders((prev) => prev.map((o) => (o.id === orderId ? { ...o, ...order } : o)), ); if (newStatus === PAYMENT_STATUS.Pending) { // 仍待支付 → 继续轮询 const timer = setTimeout(poll, POLL_INTERVAL_MS); pollingRef.current.set(orderId, timer); } else { // 终态 → 停止轮询,刷新列表保证整体一致性 pollingRef.current.delete(orderId); fetchOrders(); if (newStatus === PAYMENT_STATUS.Paid) { message.success('支付成功!'); } else if (newStatus === PAYMENT_STATUS.Failed) { message.error('支付失败,请重试'); } else if (newStatus === PAYMENT_STATUS.Expired) { message.warning('订单已过期'); } } } catch { // 网络异常 → 延长间隔后重试 if (!cancelled && mountedRef.current) { const timer = setTimeout(poll, POLL_INTERVAL_MS * 2); pollingRef.current.set(orderId, timer); } } }; // 启动首次轮询 const timer = setTimeout(poll, POLL_INTERVAL_MS); pollingRef.current.set(orderId, timer); }, [message, fetchOrders]); /** VIP 订单轮询(与充值订单轮询模式一致,使用 VipAPI.getVipOrder) */ const startVipPolling = useCallback((orderId: string) => { const existing = pollingRef.current.get(orderId); if (existing) { clearTimeout(existing); pollingRef.current.delete(orderId); } let cancelled = false; const poll = async () => { if (cancelled || !mountedRef.current) return; try { const order = await VipAPI.getVipOrder(orderId); if (cancelled || !mountedRef.current) return; const newStatus = order.status; setVipOrders((prev) => prev.map((o) => (o.id === orderId ? { ...o, ...order } : o)), ); if (newStatus === VipOrderStatus.Pending) { const timer = setTimeout(poll, POLL_INTERVAL_MS); pollingRef.current.set(orderId, timer); } else { pollingRef.current.delete(orderId); fetchOrders(); if (newStatus === VipOrderStatus.Paid) { message.success('支付成功!VIP 权益已生效'); } else if (newStatus === VipOrderStatus.Expired) { message.warning('订单已过期'); } else if (newStatus === VipOrderStatus.Cancelled) { message.info('订单已取消'); } } } catch { if (!cancelled && mountedRef.current) { const timer = setTimeout(poll, POLL_INTERVAL_MS * 2); pollingRef.current.set(orderId, timer); } } }; const timer = setTimeout(poll, POLL_INTERVAL_MS); pollingRef.current.set(orderId, timer); }, [message, fetchOrders]); // ======== 操作回调 ======== /** "继续支付":打开支付页面 + 启动轮询 */ const handleContinuePay = useCallback( (orderId: string, payUrl: string) => { window.open(payUrl, '_blank'); if (orderType === 'recharge') { startPolling(orderId); } else { startVipPolling(orderId); } message.info('已打开支付页面,请完成支付'); }, [orderType, startPolling, startVipPolling, message], ); // ======== 表格列定义 ======== /** 充值订单列 */ const rechargeColumns = useMemo(() => [ { title: '订单编号', dataIndex: 'id', key: 'id', width: 200, ellipsis: true, render: (id: string) => ( {id} ), }, { title: '金额', dataIndex: 'amount_cent', key: 'amount_cent', width: 90, align: 'center' as const, render: (amount: number) => ( ¥{centToYuan(amount)} ), }, { title: '支付方式', dataIndex: 'payment_channel', key: 'payment_channel', width: 90, align: 'center' as const, render: (channel: string) => ( {EnumResolver.payments.method.label(channel)} ), }, { title: '状态', dataIndex: 'status', key: 'status', width: 90, align: 'center' as const, render: (status: string) => ( {EnumResolver.payments.status.label(status as PaymentStatus)} ), }, { title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 120, align: 'center' as const, render: (date: Date) => ( {formatDate(date)} ), }, { title: '操作', key: 'action', width: 110, align: 'center' as const, render: (_: unknown, record: OrderListResponseBody) => { if (record.status === PAYMENT_STATUS.Pending && record.pay_url) { return ( ); } return ; }, }, ], [token.colorPrimary, handleContinuePay]); /** VIP 订单列 */ const vipColumns = useMemo(() => [ { title: '订单编号', dataIndex: 'id', key: 'id', width: 220, ellipsis: true, render: (id: string) => ( {id} ), }, { title: '金额', dataIndex: 'amount_cent', key: 'amount_cent', width: 90, align: 'center' as const, render: (amount: number) => ( ¥{centToYuan(amount)} ), }, { title: '状态', dataIndex: 'status', key: 'status', width: 90, align: 'center' as const, render: (status: string) => ( {EnumResolver.vip.orderStatus.label(status as VipOrderStatusValue)} ), }, { title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 130, align: 'center' as const, render: (date: Date) => ( {formatDate(date)} ), }, { title: '操作', key: 'action', width: 110, align: 'center' as const, render: (_: unknown, record: VipOrderRead) => { if (record.status === VipOrderStatus.Pending && record.pay_url) { return ( ); } return ; }, }, ], [token.colorPrimary, handleContinuePay]); /** 当前类型对应的列和数据 */ const columns = orderType === 'recharge' ? rechargeColumns : vipColumns; const orders = orderType === 'recharge' ? rechargeOrders : vipOrders; // ============================================================ // 渲染:错误态(无数据时) // ============================================================ if (error && orders.length === 0) { return (
重试 } />
); } // ============================================================ // 渲染:正常 // ============================================================ /** 当前订单类型对应的状态筛选项 */ const currentStatusFilters = orderType === 'recharge' ? STATUS_FILTERS : [ { label: '全部', value: 'all' }, { label: '待支付', value: VipOrderStatus.Pending }, { label: '已支付', value: VipOrderStatus.Paid }, { label: '已过期', value: VipOrderStatus.Expired }, { label: '已取消', value: VipOrderStatus.Cancelled }, ]; return (
{/* ---- 订单类型 + 状态筛选 ---- */} { setOrderType(val as OrderType); setStatusFilter('all'); setPagination((prev) => ({ ...prev, page: 1 })); }} block style={{ backgroundColor: token.colorFillSecondary }} /> { setStatusFilter(val as string); setPagination((prev) => ({ ...prev, page: 1 })); }} block style={{ backgroundColor: token.colorFillSecondary }} /> {/* ---- 手动刷新 ---- */}
{/* ---- 订单表格 ---- */} {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} columns={columns} dataSource={orders} rowKey="id" loading={loading} size="small" pagination={{ current: pagination.page, pageSize: pagination.page_size, total: orders.length >= pagination.page_size ? pagination.page * pagination.page_size + 1 : pagination.page * pagination.page_size, showSizeChanger: true, pageSizeOptions: ['10', '20', '50'], showTotal: (total: number, range: [number, number]) => `${range[0]}-${range[1]},共 ${total}+ 条`, onChange: (page: number, pageSize: number) => { setPagination({ page, page_size: pageSize }); }, placement: ['bottomCenter'], }} locale={{ emptyText: '暂无订单记录' }} />
); }