优化了部分支付宝界面

This commit is contained in:
yml2213
2026-08-12 19:12:22 +08:00
parent 1a20c367db
commit 5074faf9f6
13 changed files with 980 additions and 117 deletions
+428 -40
View File
@@ -1,6 +1,12 @@
import { useEffect, useMemo, useState } from 'react';
import { Alert, Button, Card, Col, Descriptions, Image, Radio, Row, Select, Space, Steps, Tag, Typography, message } from 'antd';
import { CheckCircleOutlined, QrcodeOutlined, ReloadOutlined, ShoppingCartOutlined, WechatOutlined } from '@ant-design/icons';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
Alert, Button, Card, Col, Collapse, Descriptions, Empty, Image, List, 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';
@@ -8,89 +14,471 @@ import { usePermissions } from '../hooks/usePermissions';
const { Title, Text } = Typography;
const phaseIndex: Record<string, number> = { login: 0, selection: 1, payment: 2, completed: 3 };
const STATUS_META: Record<string, { label: string; color: string }> = {
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 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 (
<Descriptions column={1} size="small" bordered>
<Descriptions.Item label="平台">{task.platform === 'ios' ? 'iOS 区' : 'Android 区'}</Descriptions.Item>
<Descriptions.Item label="商品">
{task.points ? `${task.points} 点券` : '-'}
{task.price_fen ? <Text type="danger">{yuan(task.price_fen)}</Text> : null}
</Descriptions.Item>
<Descriptions.Item label="区服">{task.zone_name || '-'}</Descriptions.Item>
<Descriptions.Item label="角色">{task.role_name ? `${task.role_name}${task.role_id}` : '-'}</Descriptions.Item>
<Descriptions.Item label="金额">{task.price_fen ? yuan(task.price_fen) : '-'}</Descriptions.Item>
</Descriptions>
);
}
export default function YybRechargePage() {
const [task, setTask] = useState<YybTask | null>(null);
const [recentTasks, setRecentTasks] = useState<YybTask[]>([]);
const [options, setOptions] = useState<YybSelectionOptions | null>(null);
const [platform, setPlatform] = useState<'android' | 'ios'>('android');
const [points, setPoints] = useState<number>();
const [zoneId, setZoneId] = useState<string>();
const [roleId, setRoleId] = useState<string>();
const [loading, setLoading] = useState(false);
const [optionsLoading, setOptionsLoading] = useState(false);
const [logsOpen, setLogsOpen] = useState(false);
const { can } = usePermissions(getUser());
const refresh = async () => {
if (!task) return;
try { setTask(await yybApi.getTask(task.id)); } catch { /* 保留当前状态 */ }
};
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 })); }
catch (error) { message.error(error instanceof Error ? error.message : '加载任务列表失败'); }
}, []);
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 (!task || ['success', 'failed'].includes(task.status)) return;
const timer = window.setInterval(refresh, 2500);
if (currentStatus === undefined || TERMINAL_STATUSES.includes(currentStatus)) return;
const timer = window.setInterval(() => void refresh(), 2500);
return () => window.clearInterval(timer);
}, [task?.id, task?.status]);
}, [task?.id, currentStatus, refresh]);
useEffect(() => {
if (task?.status === 'ready' && task.phase === 'selection' && !options) {
void loadOptions(platform);
}
}, [task?.status, task?.phase]);
let cancelled = false;
(async () => {
try {
const tasks = await yybApi.listTasks({ scope: 'mine' });
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 = async (nextPlatform: 'android' | 'ios', nextPoints?: number, nextZone?: string) => {
if (!task) return;
setLoading(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(task.id, nextPlatform, nextPoints, nextZone);
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);
setRoleId(undefined);
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 { setLoading(false); }
};
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.find(item => item.role_id === roleId), [options, roleId]);
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); } catch (error) { message.error(error instanceof Error ? error.message : '创建任务失败'); }
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') => {
if (!task) return;
setLoading(true);
try { setTask(await yybApi.login(task.id, provider)); } catch (error) { message.error(error instanceof Error ? error.message : '启动登录失败'); }
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 submitSelection = async () => {
if (!task || !selectedProduct || !selectedZone || !selectedRole) return;
setLoading(true);
try {
setTask(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(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 : '保存选择失败'); }
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 : '创建付款码失败'); }
try { setTask(await yybApi.payment(task.id)); }
catch (error) { message.error(error instanceof Error ? error.message : '创建付款码失败'); }
finally { setLoading(false); }
};
return <Space direction="vertical" size={16} style={{ width: '100%' }}>
<Row justify="space-between" align="middle"><Title level={3} style={{ margin: 0 }}></Title><Button icon={<ReloadOutlined />} onClick={task ? refresh : createTask}>{task ? '刷新任务' : '新建充值任务'}</Button></Row>
<Steps current={phaseIndex[task?.phase || 'login']} items={[{ title: '扫码登录' }, { title: '选择充值信息' }, { title: '微信付款' }, { title: '完成' }]} />
{!task && <Card><Space direction="vertical"><Text>使 YYB </Text><Button type="primary" icon={<ShoppingCartOutlined />} onClick={createTask} loading={loading}></Button></Space></Card>}
{task && <>
{task.status === 'failed' && <Alert type="error" showIcon message={task.message || '任务失败'} />}
{task.phase === 'login' && <Card title="扫码登录"><Space direction="vertical" size={12}><Radio.Group value={task.provider || undefined} onChange={event => void startLogin(event.target.value)} disabled={loading}><Radio.Button value="qq">QQ </Radio.Button><Radio.Button value="wechat"><WechatOutlined /> </Radio.Button></Radio.Group>{task.login_qr_data && <Image width={240} preview src={`data:${task.login_qr_mime_type || 'image/jpeg'};base64,${task.login_qr_data}`} />}<Text type="secondary">{task.message}</Text></Space></Card>}
{task.phase === 'selection' && <Card title="选择充值信息" loading={loading}><Space direction="vertical" style={{ width: '100%' }}><Radio.Group value={platform} onChange={event => { const value = event.target.value; setPlatform(value); void loadOptions(value); }}><Radio.Button value="android">Android </Radio.Button><Radio.Button value="ios">iOS </Radio.Button></Radio.Group>{options && <Row gutter={[12, 12]}><Col xs={24} md={8}><Select style={{ width: '100%' }} placeholder="点券档位" value={points} options={options.products.map(item => ({ value: item.points, label: `${item.points} 点券(${(item.price_fen / 100).toFixed(2)} 元)` }))} onChange={value => { setPoints(value); void loadOptions(platform, value, zoneId); }} /></Col><Col xs={24} md={8}><Select style={{ width: '100%' }} placeholder="区服" value={zoneId} options={options.zones.map(item => ({ value: item.zone_id, label: `${item.name}${item.zone_id}` }))} onChange={value => { setZoneId(value); void loadOptions(platform, points, value); }} /></Col><Col xs={24} md={8}><Select style={{ width: '100%' }} placeholder="角色" value={roleId} options={options.roles.filter(item => item.ban_status !== '1').map(item => ({ value: item.role_id, label: `${item.name}${item.role_id}` }))} onChange={setRoleId} /></Col></Row>}<Button type="primary" disabled={!selectedProduct || !selectedZone || !selectedRole} onClick={submitSelection}></Button></Space></Card>}
{task.phase === 'payment' && <Card title="微信付款"><Descriptions column={1} size="small"><Descriptions.Item label="平台">{task.platform === 'ios' ? 'iOS' : 'Android'}</Descriptions.Item><Descriptions.Item label="商品">{task.points} </Descriptions.Item><Descriptions.Item label="区服">{task.zone_name}</Descriptions.Item><Descriptions.Item label="角色">{task.role_name}{task.role_id}</Descriptions.Item></Descriptions>{task.payment_qr_data ? <Space direction="vertical"><Image width={280} preview src={`data:${task.payment_qr_mime_type || 'image/png'};base64,${task.payment_qr_data}`} /><Tag icon={<QrcodeOutlined />} color="blue">使</Tag></Space> : can('yyb:recharge') ? <Button type="primary" icon={<WechatOutlined />} onClick={createPayment} loading={loading || task.status === 'running'} disabled={task.status === 'running'}>{task.status === 'running' ? '正在生成付款码' : '生成微信付款码'}</Button> : <Text type="secondary"></Text>}<div><Text type="secondary">{task.message}</Text></div></Card>}
{task.phase === 'completed' && <Alert type="success" showIcon icon={<CheckCircleOutlined />} message="充值订单已确认完成" description={task.message} />}
</>}
</Space>;
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 (
<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>
{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}`} />
<Text type="secondary">使{task.provider === 'qq' ? '手机 QQ' : '微信'}</Text>
</Space>
)}
<Text type="secondary">{task.message}</Text>
{started && task.status !== 'failed' && (
<Space>
<Button icon={<ReloadOutlined />} onClick={() => void startLogin(task.provider as 'qq' | 'wechat')} loading={loading}></Button>
{canStop && <Popconfirm title="确定停止该任务?" onConfirm={stopTask}><Button danger icon={<StopOutlined />} loading={loading}></Button></Popconfirm>}
</Space>
)}
</Space>
</Card>
);
};
const renderSelectionCard = () => {
if (!task) return null;
return (
<Card title="选择充值信息" loading={optionsLoading}>
<Space direction="vertical" size={14} style={{ width: '100%' }}>
<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>
</Radio.Group>
{options ? (
<>
<div>
<Text strong></Text>
<div style={{ marginTop: 8 }}>
<Radio.Group
value={points}
onChange={event => { const value = event.target.value as number; setPoints(value); void loadOptions(platform, value, zoneId); }}
>
<Space direction="vertical" size={6}>
{options.products.map(item => (
<Radio key={item.product_id} value={item.points}>
<span style={{ display: 'inline-block', minWidth: 110 }}>{item.points} </span>
<Text type="danger">{yuan(item.price_fen)}</Text>
</Radio>
))}
</Space>
</Radio.Group>
</div>
</div>
<div style={{ maxWidth: 360 }}>
<Text strong></Text>
<Select
style={{ width: '100%', marginTop: 8 }}
placeholder="选择区服"
value={zoneId}
options={options.zones.map(item => ({ value: item.zone_id, label: `${item.name}${item.zone_id}` }))}
onChange={value => { setZoneId(value); void loadOptions(platform, points, value); }}
/>
</div>
<div>
<Text strong></Text>
<div style={{ marginTop: 8 }}>
{selectedRole ? (
<Text>{selectedRole.name}{selectedRole.role_id}</Text>
) : <Text type="secondary"></Text>}
</div>
</div>
<Button type="primary" disabled={!selectedProduct || !selectedZone || !selectedRole} onClick={() => void submitSelection()} loading={loading}>
</Button>
</>
) : <Spin tip="正在查询商品信息..." />}
</Space>
</Card>
);
};
const renderPaymentCard = () => {
if (!task) return null;
const terminal = task.status === 'failed' || task.status === 'stopped';
return (
<Card title="微信付款">
<Space direction="vertical" size={14} style={{ width: '100%' }}>
<OrderSummary task={task} />
{status === 'ready' && (
<>
<Alert type="info" showIcon message={`确认本次充值 ${task.points} 点券(${yuan(task.price_fen)}),角色与区服见上方摘要`} />
<Button type="primary" icon={<WechatOutlined />} onClick={() => void createPayment()} disabled={!task.price_fen} loading={loading}>
</Button>
</>
)}
{status === 'ordering' && (
<Space direction="vertical" size={10} style={{ width: '100%' }}>
<Spin tip={task.message || '正在生成付款码'}><div style={{ height: 40 }} /></Spin>
{showLogs && (
<Collapse
activeKey={logsOpen ? ['logs'] : undefined}
onChange={keys => setLogsOpen(Array.isArray(keys) && keys.includes('logs'))}
items={[{
key: 'logs',
label: <Text strong></Text>,
children: (
<div style={{ maxHeight: 200, overflow: 'auto', fontSize: 12, fontFamily: 'monospace' }}>
{logs.map((line, index) => <div key={index} style={{ whiteSpace: 'pre-wrap' }}>{line}</div>)}
</div>
),
}]}
/>
)}
</Space>
)}
{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="正在生成付款码..." />}
<Text type="secondary">{task.message}</Text>
<Popconfirm title="确定放弃到账追踪?商城订单仍可能完成,请自行核对。" onConfirm={stopTask}>
<Button danger icon={<StopOutlined />} loading={loading}></Button>
</Popconfirm>
</Space>
)}
{status === 'payment_timeout' && (
<Space direction="vertical" size={10}>
<Alert type="warning" showIcon
message="付款码已生成,但未在时限内确认到账"
description="订单可能已支付或尚未支付。可重新检测到账(不会重复下单),或在执行记录中核对订单状态。" />
{task.payment_qr_data && (
<Image width={280} preview src={`data:${task.payment_qr_mime_type || 'image/png'};base64,${task.payment_qr_data}`} />
)}
<Space>
<Button type="primary" onClick={() => void reCheck()} loading={loading}></Button>
<Button onClick={() => setLogsOpen(true)}></Button>
<Popconfirm title="确定放弃到账追踪?商城订单仍可能完成,请自行核对。" onConfirm={stopTask}>
<Button danger icon={<StopOutlined />} loading={loading}></Button>
</Popconfirm>
</Space>
</Space>
)}
{terminal && <Alert type={status === 'failed' ? 'error' : 'info'} showIcon message={task.message || '任务已结束'} />}
</Space>
</Card>
);
};
const renderCompletedCard = () => (
<Alert type="success" showIcon icon={<CheckCircleOutlined />}
message="充值订单已确认到账"
description={task?.message} />
);
const renderMainCard = () => {
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 === 'completed') return renderCompletedCard();
return <Card><Text type="secondary">{task.message}</Text></Card>;
};
return (
<Row gutter={[16, 16]}>
<Col xs={24} lg={16}>
<Space direction="vertical" size={16} style={{ width: '100%' }}>
<Card size="small">
<Row justify="space-between" align="middle" gutter={[12, 8]}>
<Col>
<Space wrap>
<Title level={4} style={{ margin: 0 }}></Title>
{task && <Tag color={statusMeta.color}>{statusMeta.label}</Tag>}
</Space>
</Col>
<Col>
{task && (
<Button icon={<ShoppingCartOutlined />} onClick={() => void createTask()} loading={loading}>
</Button>
)}
</Col>
</Row>
{task && (
<div style={{ marginTop: 8 }}>
<Text type="secondary">
{task.created_by_username || `#${task.created_by}`}
{task.created_at ? ` · 创建于 ${fmtTime(task.created_at)}` : ''}
{task.finished_at ? ` · 结束于 ${fmtTime(task.finished_at)}` : ''}
</Text>
</div>
)}
</Card>
{!task && (
<Card title="扫码登录">
<Space direction="vertical" size={12}>
<Text></Text>
<Space size={12}>
<Button size="large" icon={<QqOutlined />} onClick={() => void startLogin('qq')} loading={loading}>QQ </Button>
<Button size="large" icon={<WechatOutlined />} onClick={() => void startLogin('wechat')} loading={loading}></Button>
</Space>
</Space>
</Card>
)}
{task && renderMainCard()}
</Space>
</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
}>
{recentTasks.length === 0 ? <Empty description="暂无任务" image={Empty.PRESENTED_IMAGE_SIMPLE} /> : (
<List
size="small"
dataSource={recentTasks.slice(0, 12)}
renderItem={item => (
<List.Item
style={{ cursor: 'pointer', paddingLeft: 4 }}
onClick={() => 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>
)}
/>
)}
</Card>
{task && (
<Card title="订单摘要" size="small">
<OrderSummary task={task} />
</Card>
)}
{showLogs && (
<Collapse
ghost
activeKey={logsOpen ? ['logs'] : undefined}
onChange={keys => setLogsOpen(Array.isArray(keys) && keys.includes('logs'))}
items={[{
key: 'logs',
label: <Text strong>{task?.status === 'failed' ? '(失败原因)' : ''}</Text>,
children: (
<div style={{ maxHeight: 280, overflow: 'auto', fontSize: 12, fontFamily: 'monospace' }}>
{logs.map((line, index) => <div key={index} style={{ whiteSpace: 'pre-wrap' }}>{line}</div>)}
</div>
),
}]}
/>
)}
</Space>
</Col>
</Row>
);
}