2025-11-27 05:16:19 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Cosmo - Deep Space Explorer
|
|
|
|
|
|
* Main application component
|
|
|
|
|
|
*/
|
2025-11-27 10:14:25 +00:00
|
|
|
|
import { useState, useCallback } from 'react';
|
2025-11-27 05:16:19 +00:00
|
|
|
|
import { useSpaceData } from './hooks/useSpaceData';
|
2025-11-27 10:14:25 +00:00
|
|
|
|
import { useHistoricalData } from './hooks/useHistoricalData';
|
2025-11-27 05:16:19 +00:00
|
|
|
|
import { useTrajectory } from './hooks/useTrajectory';
|
|
|
|
|
|
import { Scene } from './components/Scene';
|
|
|
|
|
|
import { ProbeList } from './components/ProbeList';
|
2025-11-27 10:14:25 +00:00
|
|
|
|
import { TimelineController } from './components/TimelineController';
|
2025-11-27 05:16:19 +00:00
|
|
|
|
import { Loading } from './components/Loading';
|
|
|
|
|
|
import type { CelestialBody } from './types';
|
|
|
|
|
|
|
|
|
|
|
|
function App() {
|
2025-11-27 10:14:25 +00:00
|
|
|
|
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
|
|
|
|
|
|
const [isTimelineMode, setIsTimelineMode] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
// Use real-time data or historical data based on mode
|
|
|
|
|
|
const { bodies: realTimeBodies, loading: realTimeLoading, error: realTimeError } = useSpaceData();
|
|
|
|
|
|
const { bodies: historicalBodies, loading: historicalLoading, error: historicalError } = useHistoricalData(selectedDate);
|
|
|
|
|
|
|
|
|
|
|
|
const bodies = isTimelineMode ? historicalBodies : realTimeBodies;
|
|
|
|
|
|
const loading = isTimelineMode ? historicalLoading : realTimeLoading;
|
|
|
|
|
|
const error = isTimelineMode ? historicalError : realTimeError;
|
|
|
|
|
|
|
2025-11-27 05:16:19 +00:00
|
|
|
|
const [selectedBody, setSelectedBody] = useState<CelestialBody | null>(null);
|
|
|
|
|
|
const { trajectoryPositions } = useTrajectory(selectedBody);
|
|
|
|
|
|
|
2025-11-27 10:14:25 +00:00
|
|
|
|
// Handle time change from timeline controller
|
|
|
|
|
|
const handleTimeChange = useCallback((date: Date) => {
|
|
|
|
|
|
setSelectedDate(date);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// Toggle timeline mode
|
|
|
|
|
|
const toggleTimelineMode = useCallback(() => {
|
|
|
|
|
|
setIsTimelineMode((prev) => !prev);
|
|
|
|
|
|
if (!isTimelineMode) {
|
|
|
|
|
|
// Entering timeline mode, set initial date to Cassini launch (1997)
|
|
|
|
|
|
setSelectedDate(new Date(1997, 0, 1));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setSelectedDate(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [isTimelineMode]);
|
|
|
|
|
|
|
2025-11-27 05:16:19 +00:00
|
|
|
|
// Filter probes and planets from all bodies
|
|
|
|
|
|
const probes = bodies.filter((b) => b.type === 'probe');
|
|
|
|
|
|
const planets = bodies.filter((b) => b.type === 'planet');
|
|
|
|
|
|
|
|
|
|
|
|
const handleBodySelect = (body: CelestialBody | null) => {
|
|
|
|
|
|
setSelectedBody(body);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
|
return <Loading />;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="w-full h-full flex items-center justify-center bg-black text-white">
|
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
|
<h1 className="text-2xl font-bold mb-4">数据加载失败</h1>
|
|
|
|
|
|
<p className="text-red-400">{error}</p>
|
|
|
|
|
|
<p className="mt-4 text-sm text-gray-400">
|
|
|
|
|
|
请确保后端 API 运行在 http://localhost:8000
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="w-full h-full relative">
|
|
|
|
|
|
{/* Title overlay */}
|
|
|
|
|
|
<div className="absolute top-4 left-4 z-50 text-white">
|
|
|
|
|
|
<h1 className="text-3xl font-bold mb-1">Cosmo</h1>
|
|
|
|
|
|
<p className="text-sm text-gray-300">深空探测器可视化</p>
|
|
|
|
|
|
<p className="text-xs text-gray-400 mt-1">
|
|
|
|
|
|
{selectedBody ? `聚焦: ${selectedBody.name}` : `${bodies.length} 个天体`}
|
|
|
|
|
|
</p>
|
2025-11-27 10:14:25 +00:00
|
|
|
|
<button
|
|
|
|
|
|
onClick={toggleTimelineMode}
|
|
|
|
|
|
className={`mt-2 px-4 py-2 rounded text-sm font-medium transition-colors ${
|
|
|
|
|
|
isTimelineMode
|
|
|
|
|
|
? 'bg-blue-600 hover:bg-blue-700 text-white'
|
|
|
|
|
|
: 'bg-gray-700 hover:bg-gray-600 text-gray-300'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{isTimelineMode ? '🕐 时间轴模式 (点击退出)' : '📅 切换到时间轴模式'}
|
|
|
|
|
|
</button>
|
2025-11-27 05:16:19 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Probe List Sidebar */}
|
|
|
|
|
|
<ProbeList
|
|
|
|
|
|
probes={probes}
|
|
|
|
|
|
planets={planets}
|
|
|
|
|
|
onBodySelect={handleBodySelect}
|
|
|
|
|
|
selectedBody={selectedBody}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 3D Scene */}
|
|
|
|
|
|
<Scene
|
|
|
|
|
|
bodies={bodies}
|
|
|
|
|
|
selectedBody={selectedBody}
|
|
|
|
|
|
trajectoryPositions={trajectoryPositions}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2025-11-27 10:14:25 +00:00
|
|
|
|
{/* Timeline Controller */}
|
|
|
|
|
|
{isTimelineMode && (
|
|
|
|
|
|
<TimelineController
|
|
|
|
|
|
onTimeChange={handleTimeChange}
|
|
|
|
|
|
minDate={new Date(1997, 0, 1)} // Cassini launch date
|
|
|
|
|
|
maxDate={new Date()}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-11-27 05:16:19 +00:00
|
|
|
|
{/* Instructions overlay */}
|
|
|
|
|
|
<div className="absolute bottom-4 right-4 z-50 text-white text-xs bg-black bg-opacity-70 p-3 rounded">
|
|
|
|
|
|
{selectedBody ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<p className="text-cyan-400 font-bold mb-2">聚焦模式</p>
|
|
|
|
|
|
<p>点击侧边栏的"返回太阳系视图"按钮</p>
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<p className="font-bold mb-2">太阳系俯视图</p>
|
|
|
|
|
|
<p>🖱️ 左键拖动: 旋转</p>
|
|
|
|
|
|
<p>🖱️ 右键拖动: 平移</p>
|
|
|
|
|
|
<p>🖱️ 滚轮: 缩放</p>
|
|
|
|
|
|
<p className="mt-2 text-gray-400">点击左侧天体列表查看详情</p>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default App;
|