feat: Add admin tasks page and integrate async download

main
mula.liu 2025-12-01 00:06:28 +08:00
parent e0cf0ce598
commit 8585f89c53
3 changed files with 199 additions and 12 deletions

View File

@ -10,6 +10,7 @@ import { StaticData } from './pages/admin/StaticData';
import { Users } from './pages/admin/Users'; import { Users } from './pages/admin/Users';
import { NASADownload } from './pages/admin/NASADownload'; import { NASADownload } from './pages/admin/NASADownload';
import { SystemSettings } from './pages/admin/SystemSettings'; import { SystemSettings } from './pages/admin/SystemSettings';
import { Tasks } from './pages/admin/Tasks';
import { auth } from './utils/auth'; import { auth } from './utils/auth';
import { ToastProvider } from './contexts/ToastContext'; import { ToastProvider } from './contexts/ToastContext';
import App from './App'; import App from './App';
@ -48,6 +49,7 @@ export function Router() {
<Route path="static-data" element={<StaticData />} /> <Route path="static-data" element={<StaticData />} />
<Route path="users" element={<Users />} /> <Route path="users" element={<Users />} />
<Route path="nasa-data" element={<NASADownload />} /> <Route path="nasa-data" element={<NASADownload />} />
<Route path="tasks" element={<Tasks />} />
<Route path="settings" element={<SystemSettings />} /> <Route path="settings" element={<SystemSettings />} />
</Route> </Route>

View File

@ -179,22 +179,31 @@ export function NASADownload() {
setDownloadProgress({ current: 0, total: datesToDownload.length }); setDownloadProgress({ current: 0, total: datesToDownload.length });
try { try {
const { data } = await request.post('/celestial/positions/download', { if (selectedDate) {
body_ids: selectedBodies, // Sync download for single date
dates: datesToDownload const { data } = await request.post('/celestial/positions/download', {
}); body_ids: selectedBodies,
dates: datesToDownload
});
setDownloadProgress({ current: datesToDownload.length, total: datesToDownload.length }); setDownloadProgress({ current: datesToDownload.length, total: datesToDownload.length });
if (data.total_success > 0) { if (data.total_success > 0) {
message.success(`成功下载 ${data.total_success} 条数据${data.total_failed > 0 ? `${data.total_failed} 条失败` : ''}`); message.success(`成功下载 ${data.total_success} 条数据${data.total_failed > 0 ? `${data.total_failed} 条失败` : ''}`);
// Reload available dates to show newly downloaded data loadAvailableDates();
loadAvailableDates(); } else {
message.error('下载失败');
}
} else { } else {
message.error('下载失败'); // Async download for range
await request.post('/celestial/positions/download-async', {
body_ids: selectedBodies,
dates: datesToDownload
});
message.success('后台下载任务已启动,请前往“系统任务”查看进度');
} }
} catch (error) { } catch (error) {
message.error('下载请求失败'); message.error('请求失败');
} finally { } finally {
setDownloading(false); setDownloading(false);
setDownloadProgress({ current: 0, total: 0 }); setDownloadProgress({ current: 0, total: 0 });
@ -331,7 +340,7 @@ export function NASADownload() {
disabled={selectedBodies.length === 0} disabled={selectedBodies.length === 0}
loading={downloading} loading={downloading}
> >
()
</Button> </Button>
</Space> </Space>
} }

View File

@ -0,0 +1,176 @@
import { useState, useEffect, useRef } from 'react';
import { Tag, Progress, Button, Modal, Descriptions, Badge, Typography } from 'antd';
import { ReloadOutlined, EyeOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { DataTable } from '../../components/admin/DataTable';
import { request } from '../../utils/request';
const { Text } = Typography;
interface Task {
id: number;
task_type: string;
status: string;
progress: number;
description: string;
created_at: string;
started_at?: string;
completed_at?: string;
error_message?: string;
result?: any;
}
export function Tasks() {
const [data, setData] = useState<Task[]>([]);
const [loading, setLoading] = useState(false);
const [currentTask, setCurrentTask] = useState<Task | null>(null);
const [detailsVisible, setDetailsVisible] = useState(false);
// Auto-refresh logic
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const loadData = async () => {
setLoading(true);
try {
const res = await request.get('/celestial/tasks?limit=50');
setData(res.data);
} finally {
setLoading(false);
}
};
// Polling for active tasks
useEffect(() => {
loadData();
timerRef.current = setInterval(() => {
// Silent refresh to avoid table flickering
request.get('/celestial/tasks?limit=50').then(res => {
setData(res.data);
});
}, 3000);
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, []);
const columns: ColumnsType<Task> = [
{
title: 'ID',
dataIndex: 'id',
width: 80,
},
{
title: '任务类型',
dataIndex: 'task_type',
width: 150,
render: (type: string) => <Tag color="blue">{type}</Tag>
},
{
title: '描述',
dataIndex: 'description',
ellipsis: true,
},
{
title: '状态',
dataIndex: 'status',
width: 120,
render: (status: string) => {
const colors: Record<string, string> = {
pending: 'default',
running: 'processing',
completed: 'success',
failed: 'error',
cancelled: 'warning'
};
return <Badge status={colors[status] as any} text={status.toUpperCase()} />;
}
},
{
title: '进度',
dataIndex: 'progress',
width: 200,
render: (progress: number, record: Task) => (
<Progress
percent={progress}
size="small"
status={record.status === 'failed' ? 'exception' : record.status === 'completed' ? 'success' : 'active'}
/>
)
},
{
title: '创建时间',
dataIndex: 'created_at',
width: 180,
render: (time: string) => new Date(time).toLocaleString()
},
{
title: '操作',
key: 'action',
width: 100,
fixed: 'right',
render: (_, record) => (
<Button
icon={<EyeOutlined />}
size="small"
onClick={() => {
setCurrentTask(record);
setDetailsVisible(true);
}}
>
</Button>
)
}
];
return (
<div className="p-0">
<div className="mb-4 flex justify-end">
<Button icon={<ReloadOutlined />} onClick={loadData}></Button>
</div>
<DataTable
title="系统任务列表"
columns={columns}
dataSource={data}
loading={loading}
rowKey="id"
/>
<Modal
title="任务详情"
open={detailsVisible}
onCancel={() => setDetailsVisible(false)}
footer={null}
width={800}
>
{currentTask && (
<Descriptions bordered column={1}>
<Descriptions.Item label="任务ID">{currentTask.id}</Descriptions.Item>
<Descriptions.Item label="类型">{currentTask.task_type}</Descriptions.Item>
<Descriptions.Item label="状态">
<Badge status={currentTask.status === 'completed' ? 'success' : currentTask.status === 'running' ? 'processing' : 'default'} text={currentTask.status} />
</Descriptions.Item>
<Descriptions.Item label="描述">{currentTask.description}</Descriptions.Item>
{currentTask.error_message && (
<Descriptions.Item label="错误信息">
<Text type="danger">{currentTask.error_message}</Text>
</Descriptions.Item>
)}
<Descriptions.Item label="结果">
<div className="bg-gray-100 p-2 rounded max-h-60 overflow-auto text-xs font-mono">
{currentTask.result ? (
<pre>{JSON.stringify(currentTask.result, null, 2)}</pre>
) : (
<span className="text-gray-400"></span>
)}
</div>
</Descriptions.Item>
</Descriptions>
)}
</Modal>
</div>
);
}