/** * NASA Data Download Page * Downloads position data for celestial bodies from NASA Horizons API */ import { useState, useEffect } from 'react'; import { Row, Col, Card, Checkbox, DatePicker, Button, Badge, Spin, Typography, Collapse, Space, Progress, Calendar, Alert } from 'antd'; import { DownloadOutlined, CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons'; import type { CheckboxChangeEvent } from 'antd/es/checkbox'; import type { Dayjs } from 'dayjs'; import dayjs from 'dayjs'; import isBetween from 'dayjs/plugin/isBetween'; import { request } from '../../utils/request'; import { useToast } from '../../contexts/ToastContext'; // Extend dayjs with isBetween plugin dayjs.extend(isBetween); const { Title, Text } = Typography; const { RangePicker } = DatePicker; interface CelestialBody { id: string; name: string; name_zh: string; type: string; is_active: boolean; description: string; } interface GroupedBodies { [type: string]: CelestialBody[]; } export function NASADownload() { const [loading, setLoading] = useState(false); const [bodies, setBodies] = useState({}); const [selectedBodies, setSelectedBodies] = useState([]); const [dateRange, setDateRange] = useState<[Dayjs, Dayjs]>([ dayjs().startOf('month'), dayjs().endOf('month') ]); const [availableDates, setAvailableDates] = useState>(new Set()); const [loadingDates, setLoadingDates] = useState(false); const [downloading, setDownloading] = useState(false); const [downloadProgress, setDownloadProgress] = useState({ current: 0, total: 0 }); const toast = useToast(); // Type name mapping const typeNames: Record = { star: '恒星', planet: '行星', dwarf_planet: '矮行星', satellite: '卫星', probe: '探测器', }; useEffect(() => { loadBodies(); }, []); useEffect(() => { if (selectedBodies.length > 0) { loadAvailableDates(); } else { setAvailableDates(new Set()); } }, [selectedBodies, dateRange]); const loadBodies = async () => { setLoading(true); try { const { data } = await request.get('/celestial/positions/download/bodies'); setBodies(data.bodies || {}); } catch (error) { toast.error('加载天体列表失败'); } finally { setLoading(false); } }; const loadAvailableDates = async () => { if (selectedBodies.length === 0) return; setLoadingDates(true); try { const allDates = new Set(); // Load available dates for the first selected body const bodyId = selectedBodies[0]; const startDate = dateRange[0].format('YYYY-MM-DD'); const endDate = dateRange[1].format('YYYY-MM-DD'); const { data } = await request.get('/celestial/positions/download/status', { params: { body_id: bodyId, start_date: startDate, end_date: endDate } }); data.available_dates.forEach((date: string) => { allDates.add(date); }); setAvailableDates(allDates); } catch (error) { toast.error('加载数据状态失败'); } finally { setLoadingDates(false); } }; const handleBodySelect = (bodyId: string, checked: boolean) => { setSelectedBodies(prev => checked ? [...prev, bodyId] : prev.filter(id => id !== bodyId) ); }; const handleTypeSelectAll = (type: string, checked: boolean) => { const typeBodyIds = bodies[type].map(b => b.id); setSelectedBodies(prev => { if (checked) { return [...new Set([...prev, ...typeBodyIds])]; } else { return prev.filter(id => !typeBodyIds.includes(id)); } }); }; const handleDateRangeChange = (dates: any) => { if (dates && dates[0] && dates[1]) { setDateRange([dates[0], dates[1]]); } }; const handleDownload = async (selectedDate?: Dayjs) => { if (selectedBodies.length === 0) { toast.warning('请先选择至少一个天体'); return; } let datesToDownload: string[] = []; if (selectedDate) { // Download single date datesToDownload = [selectedDate.format('YYYY-MM-DD')]; } else { // Download all dates in range const start = dateRange[0]; const end = dateRange[1]; let current = start; while (current.isBefore(end) || current.isSame(end, 'day')) { datesToDownload.push(current.format('YYYY-MM-DD')); current = current.add(1, 'day'); } } setDownloading(true); setDownloadProgress({ current: 0, total: datesToDownload.length }); try { 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 }); if (data.total_success > 0) { toast.success(`成功下载 ${data.total_success} 条数据${data.total_failed > 0 ? `,${data.total_failed} 条失败` : ''}`); loadAvailableDates(); } else { toast.error('下载失败'); } } else { // Async download for range await request.post('/celestial/positions/download-async', { body_ids: selectedBodies, dates: datesToDownload }); toast.success('后台下载任务已启动,请前往“系统任务”查看进度'); } } catch (error) { toast.error('请求失败'); } finally { setDownloading(false); setDownloadProgress({ current: 0, total: 0 }); } }; // Custom calendar cell renderer const dateCellRender = (value: Dayjs) => { const dateStr = value.format('YYYY-MM-DD'); const hasData = availableDates.has(dateStr); const inRange = value.isBetween(dateRange[0], dateRange[1], 'day', '[]'); if (!inRange) return null; return (
{hasData ? ( ) : ( )}
); }; const disabledDate = (current: Dayjs) => { // Cannot select dates in the future return current && current.isAfter(dayjs(), 'day'); }; const handleCalendarDateClick = (date: Dayjs) => { const dateStr = date.format('YYYY-MM-DD'); const hasData = availableDates.has(dateStr); const inRange = date.isBetween(dateRange[0], dateRange[1], 'day', '[]'); if (!inRange) { toast.warning('请选择在日期范围内的日期'); return; } if (hasData) { toast.info('该日期已有数据'); return; } if (selectedBodies.length === 0) { toast.warning('请先选择天体'); return; } handleDownload(date); }; return (
{/* Left: Body Selection */} 已选择: {selectedBodies.length} } > ({ key: type, label: (
{typeNames[type] || type} selectedBodies.includes(b.id))} indeterminate={ typeBodies.some(b => selectedBodies.includes(b.id)) && !typeBodies.every(b => selectedBodies.includes(b.id)) } onChange={(e) => { e.stopPropagation(); handleTypeSelectAll(type, e.target.checked); }} > 全选
), children: ( {typeBodies.map((body) => ( handleBodySelect(body.id, e.target.checked)} > {body.name_zh || body.name} {!body.is_active && } ))} ), }))} />
{/* Right: Date Selection and Calendar */} } > }>
{downloading && (
正在下载: {downloadProgress.current} / {downloadProgress.total}
)}
); }