优化应用宝充值界面
This commit is contained in:
@@ -6,7 +6,7 @@ export const yybApi = {
|
||||
login: (id: number, provider: 'qq' | 'wechat') =>
|
||||
api.post<YybTask, YybTask>(`/yyb/tasks/${id}/login`, { provider, timeout: 600 }),
|
||||
getTask: (id: number) => api.get<YybTask, YybTask>(`/yyb/tasks/${id}`),
|
||||
listTasks: (params?: { scope?: 'mine' | 'all'; status?: string }) =>
|
||||
listTasks: (params?: { scope?: 'mine' | 'all'; status?: string; limit?: number }) =>
|
||||
api.get<YybTask[], YybTask[]>('/yyb/tasks', { params }),
|
||||
options: (id: number, platform: 'android' | 'ios', points?: number, zone_id?: string) =>
|
||||
api.get<YybSelectionOptions, YybSelectionOptions>(`/yyb/tasks/${id}/selection-options`, {
|
||||
|
||||
@@ -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<string, { label: string; color: string }> = {
|
||||
};
|
||||
|
||||
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 (
|
||||
<div style={{
|
||||
width: 280, height: 280, border: '2px dashed #d9d9d9', borderRadius: 8,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: '#fafafa', overflow: 'hidden',
|
||||
}}>
|
||||
{qrData ? (
|
||||
<Image
|
||||
width={260}
|
||||
preview
|
||||
src={`data:${mimeType || 'image/png'};base64,${qrData}`}
|
||||
/>
|
||||
) : loading ? (
|
||||
<Space direction="vertical" size={12} align="center">
|
||||
<Spin size="large" />
|
||||
<Text type="secondary">付款码生成中...</Text>
|
||||
</Space>
|
||||
) : (
|
||||
<Text type="secondary">微信付款码</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function YybRechargePage() {
|
||||
const [task, setTask] = useState<YybTask | null>(null);
|
||||
const [recentTasks, setRecentTasks] = useState<YybTask[]>([]);
|
||||
const [historyTasks, setHistoryTasks] = useState<YybTask[]>([]);
|
||||
const [options, setOptions] = useState<YybSelectionOptions | null>(null);
|
||||
const [platform, setPlatform] = useState<'android' | 'ios'>('android');
|
||||
const [points, setPoints] = useState<number>();
|
||||
@@ -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<number | null>(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 (
|
||||
<Card title="扫码登录">
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
<Radio.Group value={task.provider || undefined} disabled={started} onChange={event => void startLogin(event.target.value)}>
|
||||
<Radio.Button value="qq">QQ 登录</Radio.Button>
|
||||
<Radio.Button value="wechat"><WechatOutlined /> 微信登录</Radio.Button>
|
||||
</Radio.Group>
|
||||
<Card title="扫码登录" style={!started ? { minHeight: 330 } : undefined}>
|
||||
{!started ? (
|
||||
<Space direction="vertical" size={24} style={{ width: '100%', maxWidth: 600, margin: '36px auto 0', display: 'flex', alignItems: 'center' }}>
|
||||
<Space direction="vertical" size={6} align="center">
|
||||
<Title level={4} style={{ margin: 0 }}>选择登录方式</Title>
|
||||
<Text type="secondary">登录后可选择区服、角色和点券档位,再获取微信支付二维码。</Text>
|
||||
</Space>
|
||||
<Row gutter={[16, 16]} style={{ width: '100%' }}>
|
||||
<Col xs={24} sm={12}>
|
||||
<Button block size="large" icon={<QqOutlined />} onClick={() => void startLogin('qq')} loading={loading} style={{ height: 72, fontSize: 17 }}>
|
||||
QQ 登录
|
||||
</Button>
|
||||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<Button block size="large" icon={<WechatOutlined />} onClick={() => void startLogin('wechat')} loading={loading} style={{ height: 72, fontSize: 17 }}>
|
||||
微信登录
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Space>
|
||||
) : (
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
<Radio.Group value={task.provider || undefined} disabled onChange={event => void startLogin(event.target.value)}>
|
||||
<Radio.Button value="qq">QQ 登录</Radio.Button>
|
||||
<Radio.Button value="wechat"><WechatOutlined /> 微信登录</Radio.Button>
|
||||
</Radio.Group>
|
||||
{task.login_qr_data && (
|
||||
<Space direction="vertical" size={4}>
|
||||
<Image width={240} preview src={`data:${task.login_qr_mime_type || 'image/jpeg'};base64,${task.login_qr_data}`} />
|
||||
@@ -230,7 +319,8 @@ export default function YybRechargePage() {
|
||||
{canStop && <Popconfirm title="确定停止该任务?" onConfirm={stopTask}><Button danger icon={<StopOutlined />} loading={loading}>停止任务</Button></Popconfirm>}
|
||||
</Space>
|
||||
)}
|
||||
</Space>
|
||||
</Space>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -238,8 +328,14 @@ export default function YybRechargePage() {
|
||||
const renderSelectionCard = () => {
|
||||
if (!task) return null;
|
||||
return (
|
||||
<Card title="选择充值信息" loading={optionsLoading}>
|
||||
<Card title="选择充值信息">
|
||||
<Space direction="vertical" size={14} style={{ width: '100%' }}>
|
||||
{optionsLoading && options && (
|
||||
<Space size={8}>
|
||||
<Spin size="small" />
|
||||
<Text type="secondary">正在更新可用角色...</Text>
|
||||
</Space>
|
||||
)}
|
||||
<Radio.Group value={platform} onChange={event => { const value = event.target.value as 'android' | 'ios'; setPlatform(value); void loadOptions(value); }}>
|
||||
<Radio.Button value="android">Android 区</Radio.Button>
|
||||
<Radio.Button value="ios">iOS 区</Radio.Button>
|
||||
@@ -282,8 +378,8 @@ export default function YybRechargePage() {
|
||||
) : <Text type="secondary">当前区服暂无可用角色</Text>}
|
||||
</div>
|
||||
</div>
|
||||
<Button type="primary" disabled={!selectedProduct || !selectedZone || !selectedRole} onClick={() => void submitSelection()} loading={loading}>
|
||||
确认充值信息
|
||||
<Button type="primary" icon={<WechatOutlined />} disabled={optionsLoading || !selectedProduct || !selectedZone || !selectedRole} onClick={() => void confirmSelectionAndCreatePayment()} loading={loading}>
|
||||
确认充值信息并获取微信支付二维码
|
||||
</Button>
|
||||
</>
|
||||
) : <Spin tip="正在查询商品信息..." />}
|
||||
@@ -307,19 +403,12 @@ export default function YybRechargePage() {
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{status === 'ordering' && (
|
||||
{['running', 'ordering'].includes(status) && (
|
||||
<Space direction="vertical" size={10} style={{ width: '100%' }}>
|
||||
<PaymentQrBox loading />
|
||||
<Alert type="info" showIcon
|
||||
message="正在生成微信付款码"
|
||||
description="下单与付款码生成约需 30~60 秒,请勿关闭页面;生成后二维码会自动显示。" />
|
||||
<div style={{
|
||||
width: 280, height: 280, border: '2px dashed #d9d9d9', borderRadius: 12,
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
|
||||
gap: 12, background: '#fafafa',
|
||||
}}>
|
||||
<Spin size="large" />
|
||||
<Text type="secondary">付款码生成中...</Text>
|
||||
</div>
|
||||
message="正在获取微信支付二维码,请耐心等待"
|
||||
description="订单创建和二维码生成通常需要几十秒,请勿关闭或刷新页面,生成后会自动显示。" />
|
||||
{showLogs && (
|
||||
<Collapse
|
||||
activeKey={logsOpen ? ['logs'] : undefined}
|
||||
@@ -339,13 +428,9 @@ export default function YybRechargePage() {
|
||||
)}
|
||||
{status === 'waiting_payment' && (
|
||||
<Space direction="vertical" size={10}>
|
||||
{task.payment_qr_data ? (
|
||||
<Space direction="vertical" size={4}>
|
||||
<Image width={280} preview src={`data:${task.payment_qr_mime_type || 'image/png'};base64,${task.payment_qr_data}`} />
|
||||
<Tag color="blue">请使用微信扫码付款</Tag>
|
||||
<Text type="secondary">生成于 {fmtTime(task.payment_qr_created_at)} · 最近检测 {fmtTime(task.payment_last_checked_at)}</Text>
|
||||
</Space>
|
||||
) : <Spin tip="正在生成付款码..." />}
|
||||
<PaymentQrBox qrData={task.payment_qr_data} mimeType={task.payment_qr_mime_type} loading={!task.payment_qr_data} />
|
||||
{task.payment_qr_data && <Tag color="blue">请使用微信扫码付款</Tag>}
|
||||
{task.payment_qr_data && <Text type="secondary">生成于 {fmtTime(task.payment_qr_created_at)} · 最近检测 {fmtTime(task.payment_last_checked_at)}</Text>}
|
||||
<Text type="secondary">{task.message}</Text>
|
||||
<Popconfirm title="确定放弃到账追踪?商城订单仍可能完成,请自行核对。" onConfirm={stopTask}>
|
||||
<Button danger icon={<StopOutlined />} loading={loading}>放弃追踪</Button>
|
||||
@@ -358,7 +443,7 @@ export default function YybRechargePage() {
|
||||
message="付款码已生成,但未在时限内确认到账"
|
||||
description="订单可能已支付或尚未支付。可重新检测到账(不会重复下单),或在执行记录中核对订单状态。" />
|
||||
{task.payment_qr_data && (
|
||||
<Image width={280} preview src={`data:${task.payment_qr_mime_type || 'image/png'};base64,${task.payment_qr_data}`} />
|
||||
<PaymentQrBox qrData={task.payment_qr_data} mimeType={task.payment_qr_mime_type} loading={false} />
|
||||
)}
|
||||
<Space>
|
||||
<Button type="primary" onClick={() => void reCheck()} loading={loading}>重新检测到账</Button>
|
||||
@@ -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 <Card><Text type="secondary">{task.message}</Text></Card>;
|
||||
};
|
||||
@@ -436,15 +521,11 @@ export default function YybRechargePage() {
|
||||
</Col>
|
||||
<Col xs={24} lg={8}>
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
<Card title="最近任务" size="small" extra={
|
||||
can('yyb:history') ? (
|
||||
<Button type="link" size="small" onClick={() => void loadRecent('all')}>全部任务</Button>
|
||||
) : undefined
|
||||
}>
|
||||
<Card title="最近任务" size="small" extra={<Button type="link" size="small" onClick={() => void openHistory()}>全部任务</Button>}>
|
||||
{recentTasks.length === 0 ? <Empty description="暂无任务" image={Empty.PRESENTED_IMAGE_SIMPLE} /> : (
|
||||
<List
|
||||
size="small"
|
||||
dataSource={recentTasks.slice(0, 12)}
|
||||
dataSource={recentTasks}
|
||||
renderItem={item => (
|
||||
<List.Item
|
||||
style={{ cursor: 'pointer', paddingLeft: 4 }}
|
||||
@@ -466,7 +547,7 @@ export default function YybRechargePage() {
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
{task && (
|
||||
{task && (task.points || task.price_fen || task.zone_name || task.role_name) && (
|
||||
<Card title="订单摘要" size="small">
|
||||
<OrderSummary task={task} />
|
||||
</Card>
|
||||
@@ -489,6 +570,43 @@ export default function YybRechargePage() {
|
||||
)}
|
||||
</Space>
|
||||
</Col>
|
||||
<Modal
|
||||
title={can('yyb:history') ? '全部充值任务' : '我的全部充值任务'}
|
||||
open={historyOpen}
|
||||
footer={null}
|
||||
onCancel={() => setHistoryOpen(false)}
|
||||
width={680}
|
||||
>
|
||||
<List
|
||||
loading={historyLoading}
|
||||
locale={{ emptyText: '暂无任务' }}
|
||||
dataSource={historyTasks}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
size: 'small',
|
||||
showSizeChanger: false,
|
||||
showTotal: total => `共 ${total} 条`,
|
||||
}}
|
||||
renderItem={item => (
|
||||
<List.Item
|
||||
style={{ cursor: 'pointer', paddingInline: 4 }}
|
||||
onClick={() => { setHistoryOpen(false); void openTask(item.id); }}
|
||||
>
|
||||
<Space direction="vertical" size={2} style={{ width: '100%' }}>
|
||||
<Space wrap size={6}>
|
||||
<Tag color={STATUS_META[item.status]?.color ?? 'default'} style={{ marginInlineEnd: 0 }}>{STATUS_META[item.status]?.label ?? item.status}</Tag>
|
||||
{item.points ? <Text>{item.points} 点券</Text> : null}
|
||||
{item.price_fen ? <Text type="danger">{yuan(item.price_fen)}</Text> : null}
|
||||
{item.role_name ? <Text type="secondary">{item.role_name}</Text> : null}
|
||||
</Space>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{item.created_by_username || `#${item.created_by}`} · {fmtTime(item.created_at)}
|
||||
</Text>
|
||||
</Space>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Modal>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user