feat: Add admin tasks page and integrate async download
parent
e0cf0ce598
commit
8585f89c53
|
|
@ -10,6 +10,7 @@ import { StaticData } from './pages/admin/StaticData';
|
|||
import { Users } from './pages/admin/Users';
|
||||
import { NASADownload } from './pages/admin/NASADownload';
|
||||
import { SystemSettings } from './pages/admin/SystemSettings';
|
||||
import { Tasks } from './pages/admin/Tasks';
|
||||
import { auth } from './utils/auth';
|
||||
import { ToastProvider } from './contexts/ToastContext';
|
||||
import App from './App';
|
||||
|
|
@ -48,6 +49,7 @@ export function Router() {
|
|||
<Route path="static-data" element={<StaticData />} />
|
||||
<Route path="users" element={<Users />} />
|
||||
<Route path="nasa-data" element={<NASADownload />} />
|
||||
<Route path="tasks" element={<Tasks />} />
|
||||
<Route path="settings" element={<SystemSettings />} />
|
||||
</Route>
|
||||
|
||||
|
|
|
|||
|
|
@ -179,22 +179,31 @@ export function NASADownload() {
|
|||
setDownloadProgress({ current: 0, total: datesToDownload.length });
|
||||
|
||||
try {
|
||||
const { data } = await request.post('/celestial/positions/download', {
|
||||
body_ids: selectedBodies,
|
||||
dates: datesToDownload
|
||||
});
|
||||
if (selectedDate) {
|
||||
// Sync download for single date
|
||||
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) {
|
||||
message.success(`成功下载 ${data.total_success} 条数据${data.total_failed > 0 ? `,${data.total_failed} 条失败` : ''}`);
|
||||
// Reload available dates to show newly downloaded data
|
||||
loadAvailableDates();
|
||||
if (data.total_success > 0) {
|
||||
message.success(`成功下载 ${data.total_success} 条数据${data.total_failed > 0 ? `,${data.total_failed} 条失败` : ''}`);
|
||||
loadAvailableDates();
|
||||
} else {
|
||||
message.error('下载失败');
|
||||
}
|
||||
} else {
|
||||
message.error('下载失败');
|
||||
// Async download for range
|
||||
await request.post('/celestial/positions/download-async', {
|
||||
body_ids: selectedBodies,
|
||||
dates: datesToDownload
|
||||
});
|
||||
message.success('后台下载任务已启动,请前往“系统任务”查看进度');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('下载请求失败');
|
||||
message.error('请求失败');
|
||||
} finally {
|
||||
setDownloading(false);
|
||||
setDownloadProgress({ current: 0, total: 0 });
|
||||
|
|
@ -331,7 +340,7 @@ export function NASADownload() {
|
|||
disabled={selectedBodies.length === 0}
|
||||
loading={downloading}
|
||||
>
|
||||
下载范围内所有日期
|
||||
下载范围内数据 (后台任务)
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue