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 { 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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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