import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Alert, Button, Card, Col, Collapse, Descriptions, Empty, Image, List, Modal, Popconfirm, Radio, Row, Select, Space, Spin, Tag, Typography, message, } from 'antd'; import { CheckCircleOutlined, QqOutlined, ReloadOutlined, ShoppingCartOutlined, WechatOutlined, StopOutlined, } from '@ant-design/icons'; import dayjs from 'dayjs'; import { yybApi } from '../api/modules'; import type { YybSelectionOptions, YybTask } from '../api/types'; import { getUser } from '../store/auth'; import { usePermissions } from '../hooks/usePermissions'; const { Title, Text } = Typography; const STATUS_META: Record = { created: { label: '待登录', color: 'default' }, waiting_login: { label: '等待扫码', color: 'processing' }, ready: { label: '待操作', color: 'gold' }, running: { label: '处理中', color: 'processing' }, ordering: { label: '下单中', color: 'processing' }, waiting_payment: { label: '待微信付款', color: 'blue' }, payment_timeout: { label: '确认超时', color: 'warning' }, success: { label: '已确认到账', color: 'success' }, failed: { label: '失败', color: 'error' }, stopped: { label: '已停止', color: 'default' }, }; const TERMINAL_STATUSES = ['success', 'failed', 'stopped']; const RECENT_TASK_LIMIT = 6; const fmtTime = (value?: string | null) => (value ? dayjs(value).format('MM-DD HH:mm') : '-'); const yuan = (fen?: number | null) => `¥${((fen ?? 0) / 100).toFixed(2)}`; function OrderSummary({ task }: { task: YybTask }) { return ( {task.platform === 'ios' ? 'iOS 区' : 'Android 区'} {task.points ? `${task.points} 点券` : '-'} {task.price_fen ? ({yuan(task.price_fen)}) : null} {task.zone_name || '-'} {task.role_name ? `${task.role_name}(${task.role_id})` : '-'} {task.price_fen ? yuan(task.price_fen) : '-'} ); } function PaymentQrBox({ qrData, mimeType, loading }: { qrData?: string | null; mimeType?: string | null; loading: boolean; }) { return (
{qrData ? ( ) : loading ? ( 付款码生成中... ) : ( 微信付款码 )}
); } export default function YybRechargePage() { const [task, setTask] = useState(null); const [recentTasks, setRecentTasks] = useState([]); const [historyTasks, setHistoryTasks] = useState([]); const [options, setOptions] = useState(null); const [platform, setPlatform] = useState<'android' | 'ios'>('android'); const [points, setPoints] = useState(); const [zoneId, setZoneId] = useState(); const [roleId, setRoleId] = useState(); const [loading, setLoading] = useState(false); const [optionsLoading, setOptionsLoading] = useState(false); const [logsOpen, setLogsOpen] = useState(false); const [historyOpen, setHistoryOpen] = useState(false); const [historyLoading, setHistoryLoading] = useState(false); const { can } = usePermissions(getUser()); const optionsSeq = useRef(0); const taskIdRef = useRef(null); useEffect(() => { taskIdRef.current = task?.id ?? null; }, [task?.id]); const loadRecent = useCallback(async () => { try { setRecentTasks(await yybApi.listTasks({ scope: 'mine', limit: RECENT_TASK_LIMIT })); } catch (error) { message.error(error instanceof Error ? error.message : '加载任务列表失败'); } }, []); const openHistory = async () => { setHistoryOpen(true); setHistoryLoading(true); try { setHistoryTasks(await yybApi.listTasks({ scope: can('yyb:history') ? 'all' : 'mine', limit: 200, })); } catch (error) { message.error(error instanceof Error ? error.message : '加载任务列表失败'); } finally { setHistoryLoading(false); } }; const refresh = useCallback(async () => { const id = taskIdRef.current; if (id == null) return; try { setTask(await yybApi.getTask(id)); } catch { /* 保留当前状态 */ } }, []); const currentStatus = task?.status; const currentPhase = task?.phase; useEffect(() => { if (currentStatus === undefined || TERMINAL_STATUSES.includes(currentStatus)) return; const timer = window.setInterval(() => void refresh(), 2500); return () => window.clearInterval(timer); }, [task?.id, currentStatus, refresh]); useEffect(() => { let cancelled = false; (async () => { try { const tasks = await yybApi.listTasks({ scope: 'mine', limit: RECENT_TASK_LIMIT }); if (cancelled) return; setRecentTasks(tasks); const active = tasks.find(item => !TERMINAL_STATUSES.includes(item.status)); if (active) { const synced = await yybApi.getTask(active.id); if (!cancelled) setTask(synced); } } catch { /* 忽略列表加载失败 */ } })(); return () => { cancelled = true; }; }, []); const loadOptions = useCallback(async (nextPlatform: 'android' | 'ios', nextPoints?: number, nextZone?: string) => { const id = taskIdRef.current; if (id == null) return; const seq = ++optionsSeq.current; setOptionsLoading(true); try { const data = await yybApi.options(id, nextPlatform, nextPoints, nextZone); if (seq !== optionsSeq.current) return; setOptions(data); if (nextPoints === undefined && data.default_product) setPoints(data.default_product.points); if (nextZone === undefined && data.default_zone) setZoneId(data.default_zone.zone_id); const firstRole = data.roles.filter(item => item.ban_status !== '1')[0]; setRoleId(firstRole?.role_id); } catch (error) { message.error(error instanceof Error ? error.message : '查询商品失败'); } finally { if (seq === optionsSeq.current) setOptionsLoading(false); } }, []); useEffect(() => { if (currentStatus === 'ready' && currentPhase === 'selection' && !options) { void loadOptions(platform); } }, [currentStatus, currentPhase, options, platform, loadOptions]); const selectedProduct = useMemo(() => options?.products.find(item => item.points === points), [options, points]); const selectedZone = useMemo(() => options?.zones.find(item => item.zone_id === zoneId), [options, zoneId]); const selectedRole = useMemo(() => options?.roles.filter(item => item.ban_status !== '1').find(item => item.role_id === roleId), [options, roleId]); const openTask = async (id: number) => { setLoading(true); try { const synced = await yybApi.getTask(id); setTask(synced); setOptions(null); setRoleId(undefined); setLogsOpen(false); if (synced.phase === 'selection') setPlatform(synced.platform || 'android'); } catch (error) { message.error(error instanceof Error ? error.message : '加载任务失败'); } finally { setLoading(false); } }; const createTask = async () => { setLoading(true); try { setTask(await yybApi.createTask()); setOptions(null); setRoleId(undefined); } catch (error) { message.error(error instanceof Error ? error.message : '创建任务失败'); } finally { setLoading(false); } }; const startLogin = async (provider: 'qq' | 'wechat') => { setLoading(true); try { let current = task; if (!current) { current = await yybApi.createTask(); setTask(current); setOptions(null); setRoleId(undefined); } setTask(await yybApi.login(current.id, provider)); } catch (error) { message.error(error instanceof Error ? error.message : '启动登录失败'); } finally { setLoading(false); } }; const confirmSelectionAndCreatePayment = async () => { if (!task || !selectedProduct || !selectedZone || !selectedRole) return; const originalTask = task; setLoading(true); setTask({ ...task, platform, points: selectedProduct.points, price_fen: selectedProduct.price_fen, zone_id: selectedZone.zone_id, zone_name: selectedZone.name, role_id: selectedRole.role_id, role_name: selectedRole.name, phase: 'payment', status: 'ordering', message: '正在获取微信支付二维码', }); try { const selectedTask = await yybApi.select(task.id, { platform, points: selectedProduct.points, product_id: selectedProduct.product_id, zone_id: selectedZone.zone_id, zone_name: selectedZone.name, role_id: selectedRole.role_id, role_name: selectedRole.name, }); setTask({ ...selectedTask, phase: 'payment', status: 'ordering', message: '正在获取微信支付二维码' }); const paymentTask = await yybApi.payment(selectedTask.id); setTask(paymentTask); void loadRecent(); } catch (error) { try { setTask(await yybApi.getTask(task.id)); } catch { setTask(originalTask); } message.error(error instanceof Error ? error.message : '保存选择或生成付款码失败'); } finally { setLoading(false); } }; const createPayment = async () => { if (!task) return; setLoading(true); try { setTask(await yybApi.payment(task.id)); } catch (error) { message.error(error instanceof Error ? error.message : '创建付款码失败'); } finally { setLoading(false); } }; const reCheck = async () => { if (!task) return; setLoading(true); try { setTask(await yybApi.paymentCheck(task.id)); message.success('已重新检测到账'); } catch (error) { message.error(error instanceof Error ? error.message : '检测到账失败'); } finally { setLoading(false); } }; const stopTask = async () => { if (!task) return; setLoading(true); try { setTask(await yybApi.stop(task.id)); } catch (error) { message.error(error instanceof Error ? error.message : '停止任务失败'); } finally { setLoading(false); } }; const status = task?.status ?? ''; const statusMeta = STATUS_META[status] ?? { label: status, color: 'default' }; const logs = task?.result?.logs ?? []; const showLogs = logs.length > 0; const canStop = task && !['waiting_payment', 'payment_timeout', 'success'].includes(status); const renderLoginCard = () => { if (!task) return null; const started = status !== 'created'; return ( {!started ? ( 选择登录方式 登录后可选择区服、角色和点券档位,再获取微信支付二维码。 ) : ( void startLogin(event.target.value)}> QQ 登录 微信登录 {task.login_qr_data && ( 请使用{task.provider === 'qq' ? '手机 QQ' : '微信'}扫码并确认登录 )} {task.message} {started && task.status !== 'failed' && ( {canStop && } )} )} ); }; const renderSelectionCard = () => { if (!task) return null; return ( {optionsLoading && options && ( 正在更新可用角色... )} { const value = event.target.value as 'android' | 'ios'; setPlatform(value); void loadOptions(value); }}> Android 区 iOS 区 {options ? ( <>
点券档位
{ const value = event.target.value as number; setPoints(value); void loadOptions(platform, value, zoneId); }} > {options.products.map(item => ( {item.points} 点券 {yuan(item.price_fen)} ))}
区服