From f1a22e87e9799ea007c8a1666b03d7369ad06a7b Mon Sep 17 00:00:00 2001 From: yml2213 Date: Wed, 12 Aug 2026 22:23:59 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=BA=94=E7=94=A8=E5=AE=9D?= =?UTF-8?q?=E5=85=85=E5=80=BC=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/frontend/src/api/yyb.ts | 2 +- web/frontend/src/pages/YybRechargePage.tsx | 210 ++++++++++++++++----- 2 files changed, 165 insertions(+), 47 deletions(-) diff --git a/web/frontend/src/api/yyb.ts b/web/frontend/src/api/yyb.ts index 62f8e65..b0e9442 100644 --- a/web/frontend/src/api/yyb.ts +++ b/web/frontend/src/api/yyb.ts @@ -6,7 +6,7 @@ export const yybApi = { login: (id: number, provider: 'qq' | 'wechat') => api.post(`/yyb/tasks/${id}/login`, { provider, timeout: 600 }), getTask: (id: number) => api.get(`/yyb/tasks/${id}`), - listTasks: (params?: { scope?: 'mine' | 'all'; status?: string }) => + listTasks: (params?: { scope?: 'mine' | 'all'; status?: string; limit?: number }) => api.get('/yyb/tasks', { params }), options: (id: number, platform: 'android' | 'ios', points?: number, zone_id?: string) => api.get(`/yyb/tasks/${id}/selection-options`, { diff --git a/web/frontend/src/pages/YybRechargePage.tsx b/web/frontend/src/pages/YybRechargePage.tsx index 6afeea9..8120dea 100644 --- a/web/frontend/src/pages/YybRechargePage.tsx +++ b/web/frontend/src/pages/YybRechargePage.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { - Alert, Button, Card, Col, Collapse, Descriptions, Empty, Image, List, Popconfirm, + Alert, Button, Card, Col, Collapse, Descriptions, Empty, Image, List, Modal, Popconfirm, Radio, Row, Select, Space, Spin, Tag, Typography, message, } from 'antd'; import { @@ -28,6 +28,7 @@ const STATUS_META: Record = { }; 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)}`; @@ -47,9 +48,39 @@ function OrderSummary({ task }: { task: YybTask }) { ); } +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(); @@ -58,17 +89,34 @@ export default function YybRechargePage() { 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 (scope: 'mine' | 'all') => { - try { setRecentTasks(await yybApi.listTasks({ scope })); } + 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; @@ -88,7 +136,7 @@ export default function YybRechargePage() { let cancelled = false; (async () => { try { - const tasks = await yybApi.listTasks({ scope: 'mine' }); + 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)); @@ -163,17 +211,38 @@ export default function YybRechargePage() { finally { setLoading(false); } }; - const submitSelection = async () => { + 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 { - setTask(await yybApi.select(task.id, { + 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, - })); - message.success('选择已保存'); - } catch (error) { message.error(error instanceof Error ? error.message : '保存选择失败'); } + }); + 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); } }; @@ -211,12 +280,32 @@ export default function YybRechargePage() { if (!task) return null; const started = status !== 'created'; return ( - - - void startLogin(event.target.value)}> - QQ 登录 - 微信登录 - + + {!started ? ( + + + 选择登录方式 + 登录后可选择区服、角色和点券档位,再获取微信支付二维码。 + + + + + + + + + + + ) : ( + + void startLogin(event.target.value)}> + QQ 登录 + 微信登录 + {task.login_qr_data && ( @@ -230,7 +319,8 @@ export default function YybRechargePage() { {canStop && } )} - + + )} ); }; @@ -238,8 +328,14 @@ export default function YybRechargePage() { const renderSelectionCard = () => { if (!task) return null; return ( - + + {optionsLoading && options && ( + + + 正在更新可用角色... + + )} { const value = event.target.value as 'android' | 'ios'; setPlatform(value); void loadOptions(value); }}> Android 区 iOS 区 @@ -282,8 +378,8 @@ export default function YybRechargePage() { ) : 当前区服暂无可用角色} - ) : } @@ -307,19 +403,12 @@ export default function YybRechargePage() { )} - {status === 'ordering' && ( + {['running', 'ordering'].includes(status) && ( + -
- - 付款码生成中... -
+ message="正在获取微信支付二维码,请耐心等待" + description="订单创建和二维码生成通常需要几十秒,请勿关闭或刷新页面,生成后会自动显示。" /> {showLogs && ( - {task.payment_qr_data ? ( - - - 请使用微信扫码付款 - 生成于 {fmtTime(task.payment_qr_created_at)} · 最近检测 {fmtTime(task.payment_last_checked_at)} - - ) : } + + {task.payment_qr_data && 请使用微信扫码付款} + {task.payment_qr_data && 生成于 {fmtTime(task.payment_qr_created_at)} · 最近检测 {fmtTime(task.payment_last_checked_at)}} {task.message} @@ -358,7 +443,7 @@ export default function YybRechargePage() { message="付款码已生成,但未在时限内确认到账" description="订单可能已支付或尚未支付。可重新检测到账(不会重复下单),或在执行记录中核对订单状态。" /> {task.payment_qr_data && ( - + )} @@ -385,7 +470,7 @@ export default function YybRechargePage() { if (!task) return null; if (task.phase === 'login') return renderLoginCard(); if (task.phase === 'selection') return renderSelectionCard(); - if (task.phase === 'payment') return renderPaymentCard(); + if (task.phase === 'payment' || ['running', 'ordering', 'waiting_payment', 'payment_timeout'].includes(task.status)) return renderPaymentCard(); if (task.phase === 'completed') return renderCompletedCard(); return {task.message}; }; @@ -436,15 +521,11 @@ export default function YybRechargePage() { - void loadRecent('all')}>全部任务 - ) : undefined - }> + void openHistory()}>全部任务}> {recentTasks.length === 0 ? : ( ( )} - {task && ( + {task && (task.points || task.price_fen || task.zone_name || task.role_name) && ( @@ -489,6 +570,43 @@ export default function YybRechargePage() { )} + setHistoryOpen(false)} + width={680} + > + `共 ${total} 条`, + }} + renderItem={item => ( + { setHistoryOpen(false); void openTask(item.id); }} + > + + + {STATUS_META[item.status]?.label ?? item.status} + {item.points ? {item.points} 点券 : null} + {item.price_fen ? {yuan(item.price_fen)} : null} + {item.role_name ? {item.role_name} : null} + + + {item.created_by_username || `#${item.created_by}`} · {fmtTime(item.created_at)} + + + + )} + /> + ); }