import { useState, useCallback, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useSpaceData } from './hooks/useSpaceData'; import { useHistoricalData } from './hooks/useHistoricalData'; import { useTrajectory } from './hooks/useTrajectory'; import { useScreenshot } from './hooks/useScreenshot'; import { Header } from './components/Header'; import { Scene } from './components/Scene'; import { ProbeList } from './components/ProbeList'; import { TimelineController } from './components/TimelineController'; import { Loading } from './components/Loading'; import { InterstellarTicker } from './components/InterstellarTicker'; import { ControlPanel } from './components/ControlPanel'; import { AuthModal } from './components/AuthModal'; import { MessageBoard } from './components/MessageBoard'; import { auth } from './utils/auth'; import type { CelestialBody } from './types'; import { useToast } from './contexts/ToastContext'; // Timeline configuration - will be fetched from backend later const TIMELINE_DAYS = 30; // Total days in timeline range const PREFS_KEY = 'cosmo_preferences'; function App() { const navigate = useNavigate(); const toast = useToast(); // Load preferences const [isTimelineMode, setIsTimelineMode] = useState(false); // Usually not persisted const [showOrbits, setShowOrbits] = useState(true); const [isSoundOn, setIsSoundOn] = useState(false); const [showMessageBoard, setShowMessageBoard] = useState(false); // Initialize state from localStorage useEffect(() => { const savedPrefs = localStorage.getItem(PREFS_KEY); if (savedPrefs) { try { const prefs = JSON.parse(savedPrefs); if (prefs.showOrbits !== undefined) setShowOrbits(prefs.showOrbits); if (prefs.isSoundOn !== undefined) setIsSoundOn(prefs.isSoundOn); } catch (e) { console.error('Failed to parse preferences:', e); } } }, []); // Persist preferences useEffect(() => { const prefs = { showOrbits, isSoundOn, }; localStorage.setItem(PREFS_KEY, JSON.stringify(prefs)); }, [showOrbits, isSoundOn]); const [selectedDate, setSelectedDate] = useState(null); const { takeScreenshot } = useScreenshot(); const [resetTrigger, setResetTrigger] = useState(0); // Auth state const [user, setUser] = useState(auth.getUser()); const [showAuthModal, setShowAuthModal] = 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; const [selectedBody, setSelectedBody] = useState(null); const { trajectoryPositions } = useTrajectory(selectedBody); // 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 now (will play backward) setSelectedDate(new Date()); } else { setSelectedDate(null); } }, [isTimelineMode]); // Filter probes and planets from all bodies const probes = bodies.filter((b) => b.type === 'probe'); const planets = bodies.filter((b) => b.type === 'planet' || b.type === 'dwarf_planet' || b.type === 'satellite' || b.type === 'comet' ); const handleBodySelect = (body: CelestialBody | null) => { setSelectedBody(body); }; // Screenshot handler with auth check const handleScreenshot = useCallback(() => { if (!user) { toast.warning('请先登录以拍摄宇宙快照'); setShowAuthModal(true); return; } // Use username or full_name or fallback const nickname = user.full_name || user.username || 'Explorer'; takeScreenshot(nickname); }, [user, takeScreenshot]); // Auth handlers const handleLoginSuccess = (userData: any) => { setUser(userData); setShowAuthModal(false); }; const handleLogout = () => { auth.logout(); setUser(null); }; // Only show full screen loading when we have no data // This prevents flashing when timeline is playing and fetching new data if (loading && bodies.length === 0) { return ; } if (error) { return (

数据加载失败

{error}

请确保后端 API 运行在 http://localhost:8000

); } return (
{/* Header with simplified branding and User Auth */}
b.is_active !== false).length} selectedBodyName={selectedBody?.name} user={user} onOpenAuth={() => setShowAuthModal(true)} onLogout={handleLogout} onNavigateToAdmin={() => navigate('/admin')} /> {/* Right Control Panel */} setShowOrbits(!showOrbits)} isSoundOn={isSoundOn} onToggleSound={() => setIsSoundOn(!isSoundOn)} showMessageBoard={showMessageBoard} onToggleMessageBoard={() => setShowMessageBoard(!showMessageBoard)} onScreenshot={handleScreenshot} /> {/* Auth Modal */} setShowAuthModal(false)} onLoginSuccess={handleLoginSuccess} /> {/* Message Board */} setShowMessageBoard(false)} /> {/* Probe List Sidebar */} setResetTrigger(prev => prev + 1)} /> {/* 3D Scene */} {/* Timeline Controller */} {isTimelineMode && ( )} {/* Interstellar Ticker Sound (Controlled) */} {/* Instructions overlay (Only show when exploring freely) */} {!selectedBody && (
左键 旋转
右键 平移
滚轮 缩放
)}
); } export default App;