diff --git a/frontend/index.html b/frontend/index.html index 072a57e..d3cdd1c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,10 +1,10 @@ - + - frontend + Cosmo - 深空探测器可视化
diff --git a/frontend/package.json b/frontend/package.json index c8b6788..6e0a5e7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,11 +10,14 @@ "preview": "vite preview" }, "dependencies": { + "@ant-design/icons": "^6.1.0", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.4.0", + "antd": "^6.0.0", "axios": "^1.13.2", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-router-dom": "^7.9.6", "three": "^0.181.2" }, "devDependencies": { diff --git a/frontend/public/data/constellations.json b/frontend/public/data/constellations.json deleted file mode 100644 index da7421f..0000000 --- a/frontend/public/data/constellations.json +++ /dev/null @@ -1,96 +0,0 @@ -[ - { - "name": "Orion", - "name_zh": "猎户座", - "stars": [ - {"name": "Betelgeuse", "ra": 88.79, "dec": 7.41}, - {"name": "Bellatrix", "ra": 81.28, "dec": 6.35}, - {"name": "Alnitak", "ra": 85.19, "dec": -1.94}, - {"name": "Alnilam", "ra": 84.05, "dec": -1.20}, - {"name": "Mintaka", "ra": 83.00, "dec": -0.30}, - {"name": "Saiph", "ra": 86.94, "dec": -9.67}, - {"name": "Rigel", "ra": 78.63, "dec": -8.20} - ], - "lines": [ - [0, 1], - [1, 2], - [2, 3], - [3, 4], - [2, 5], - [5, 6] - ] - }, - { - "name": "Ursa Major", - "name_zh": "大熊座", - "stars": [ - {"name": "Dubhe", "ra": 165.93, "dec": 61.75}, - {"name": "Merak", "ra": 165.46, "dec": 56.38}, - {"name": "Phecda", "ra": 178.46, "dec": 53.69}, - {"name": "Megrez", "ra": 183.86, "dec": 57.03}, - {"name": "Alioth", "ra": 193.51, "dec": 55.96}, - {"name": "Mizar", "ra": 200.98, "dec": 54.93}, - {"name": "Alkaid", "ra": 206.89, "dec": 49.31} - ], - "lines": [ - [0, 1], - [1, 2], - [2, 3], - [3, 4], - [4, 5], - [5, 6] - ] - }, - { - "name": "Cassiopeia", - "name_zh": "仙后座", - "stars": [ - {"name": "Caph", "ra": 2.29, "dec": 59.15}, - {"name": "Schedar", "ra": 10.13, "dec": 56.54}, - {"name": "Navi", "ra": 14.18, "dec": 60.72}, - {"name": "Ruchbah", "ra": 21.45, "dec": 60.24}, - {"name": "Segin", "ra": 25.65, "dec": 63.67} - ], - "lines": [ - [0, 1], - [1, 2], - [2, 3], - [3, 4] - ] - }, - { - "name": "Leo", - "name_zh": "狮子座", - "stars": [ - {"name": "Regulus", "ra": 152.09, "dec": 11.97}, - {"name": "Denebola", "ra": 177.26, "dec": 14.57}, - {"name": "Algieba", "ra": 154.99, "dec": 19.84}, - {"name": "Zosma", "ra": 168.53, "dec": 20.52}, - {"name": "Chertan", "ra": 173.95, "dec": 15.43} - ], - "lines": [ - [0, 2], - [2, 3], - [3, 4], - [4, 1], - [1, 0] - ] - }, - { - "name": "Scorpius", - "name_zh": "天蝎座", - "stars": [ - {"name": "Antares", "ra": 247.35, "dec": -26.43}, - {"name": "Shaula", "ra": 263.40, "dec": -37.10}, - {"name": "Sargas", "ra": 264.33, "dec": -43.00}, - {"name": "Dschubba", "ra": 240.08, "dec": -22.62}, - {"name": "Lesath", "ra": 262.69, "dec": -37.29} - ], - "lines": [ - [3, 0], - [0, 1], - [1, 4], - [1, 2] - ] - } -] diff --git a/frontend/public/data/galaxies.json b/frontend/public/data/galaxies.json deleted file mode 100644 index c6ede3e..0000000 --- a/frontend/public/data/galaxies.json +++ /dev/null @@ -1,57 +0,0 @@ -[ - { - "name": "Andromeda Galaxy", - "name_zh": "仙女座星系", - "type": "spiral", - "distance_mly": 2.537, - "ra": 10.68, - "dec": 41.27, - "magnitude": 3.44, - "diameter_kly": 220, - "color": "#CCDDFF" - }, - { - "name": "Triangulum Galaxy", - "name_zh": "三角座星系", - "type": "spiral", - "distance_mly": 2.73, - "ra": 23.46, - "dec": 30.66, - "magnitude": 5.72, - "diameter_kly": 60, - "color": "#AACCEE" - }, - { - "name": "Large Magellanic Cloud", - "name_zh": "大麦哲伦云", - "type": "irregular", - "distance_mly": 0.163, - "ra": 80.89, - "dec": -69.76, - "magnitude": 0.9, - "diameter_kly": 14, - "color": "#DDCCFF" - }, - { - "name": "Small Magellanic Cloud", - "name_zh": "小麦哲伦云", - "type": "irregular", - "distance_mly": 0.197, - "ra": 12.80, - "dec": -73.15, - "magnitude": 2.7, - "diameter_kly": 7, - "color": "#CCBBEE" - }, - { - "name": "Milky Way Center", - "name_zh": "银河系中心", - "type": "galactic_center", - "distance_mly": 0.026, - "ra": 266.42, - "dec": -29.01, - "magnitude": -1, - "diameter_kly": 100, - "color": "#FFFFAA" - } -] diff --git a/frontend/public/data/nearby-stars.json b/frontend/public/data/nearby-stars.json deleted file mode 100644 index 5f2764b..0000000 --- a/frontend/public/data/nearby-stars.json +++ /dev/null @@ -1,110 +0,0 @@ -[ - { - "name": "Proxima Centauri", - "name_zh": "比邻星", - "distance_ly": 4.24, - "ra": 217.43, - "dec": -62.68, - "magnitude": 11.05, - "color": "#FF9966" - }, - { - "name": "Alpha Centauri A", - "name_zh": "南门二A", - "distance_ly": 4.37, - "ra": 219.90, - "dec": -60.83, - "magnitude": -0.01, - "color": "#FFFFAA" - }, - { - "name": "Barnard's Star", - "name_zh": "巴纳德星", - "distance_ly": 5.96, - "ra": 269.45, - "dec": 4.69, - "magnitude": 9.54, - "color": "#FF6666" - }, - { - "name": "Sirius", - "name_zh": "天狼星", - "distance_ly": 8.6, - "ra": 101.29, - "dec": -16.72, - "magnitude": -1.46, - "color": "#FFFFFF" - }, - { - "name": "Epsilon Eridani", - "name_zh": "天苑四", - "distance_ly": 10.5, - "ra": 53.23, - "dec": -9.46, - "magnitude": 3.73, - "color": "#FFDDAA" - }, - { - "name": "Procyon", - "name_zh": "南河三", - "distance_ly": 11.4, - "ra": 114.83, - "dec": 5.22, - "magnitude": 0.34, - "color": "#FFFFDD" - }, - { - "name": "Tau Ceti", - "name_zh": "天仓五", - "distance_ly": 11.9, - "ra": 26.02, - "dec": -15.94, - "magnitude": 3.50, - "color": "#FFFFCC" - }, - { - "name": "Vega", - "name_zh": "织女星", - "distance_ly": 25, - "ra": 279.23, - "dec": 38.78, - "magnitude": 0.03, - "color": "#AACCFF" - }, - { - "name": "Arcturus", - "name_zh": "大角星", - "distance_ly": 37, - "ra": 213.92, - "dec": 19.18, - "magnitude": -0.05, - "color": "#FFCC99" - }, - { - "name": "Altair", - "name_zh": "牛郎星", - "distance_ly": 17, - "ra": 297.70, - "dec": 8.87, - "magnitude": 0.77, - "color": "#FFFFFF" - }, - { - "name": "Betelgeuse", - "name_zh": "参宿四", - "distance_ly": 548, - "ra": 88.79, - "dec": 7.41, - "magnitude": 0.42, - "color": "#FF4444" - }, - { - "name": "Rigel", - "name_zh": "参宿七", - "distance_ly": 860, - "ra": 78.63, - "dec": -8.20, - "magnitude": 0.13, - "color": "#AADDFF" - } -] diff --git a/frontend/public/data/probe-models.json b/frontend/public/data/probe-models.json deleted file mode 100644 index 391c8e9..0000000 --- a/frontend/public/data/probe-models.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Voyager 1": "/models/voyager_1.glb", - "Voyager 2": "/models/voyager_2.glb", - "Juno": "/models/juno.glb", - "Cassini": "/models/cassini.glb", - "New Horizons": null, - "Parker Solar Probe": "/models/parker_solar_probe.glb", - "Perseverance": null -} diff --git a/frontend/public/data/solar-system.json b/frontend/public/data/solar-system.json deleted file mode 100644 index 25b286b..0000000 --- a/frontend/public/data/solar-system.json +++ /dev/null @@ -1,142 +0,0 @@ -{ - "bodies": [ - { - "id": "10", - "name": "Sun", - "name_zh": "太阳", - "type": "star", - "description": "太阳,太阳系的中心" - }, - { - "id": "199", - "name": "Mercury", - "name_zh": "水星", - "type": "planet", - "description": "水星,距离太阳最近的行星" - }, - { - "id": "299", - "name": "Venus", - "name_zh": "金星", - "type": "planet", - "description": "金星,太阳系中最热的行星" - }, - { - "id": "399", - "name": "Earth", - "name_zh": "地球", - "type": "planet", - "description": "地球,我们的家园" - }, - { - "id": "301", - "name": "Moon", - "name_zh": "月球", - "type": "planet", - "description": "月球,地球的天然卫星" - }, - { - "id": "499", - "name": "Mars", - "name_zh": "火星", - "type": "planet", - "description": "火星,红色星球" - }, - { - "id": "599", - "name": "Jupiter", - "name_zh": "木星", - "type": "planet", - "description": "木星,太阳系中最大的行星" - }, - { - "id": "699", - "name": "Saturn", - "name_zh": "土星", - "type": "planet", - "description": "土星,拥有美丽的光环" - }, - { - "id": "799", - "name": "Uranus", - "name_zh": "天王星", - "type": "planet", - "description": "天王星,侧躺着自转的行星" - }, - { - "id": "899", - "name": "Neptune", - "name_zh": "海王星", - "type": "planet", - "description": "海王星,太阳系最外层的行星" - }, - { - "id": "-31", - "name": "Voyager 1", - "name_zh": "旅行者1号", - "type": "probe", - "description": "离地球最远的人造物体,已进入星际空间", - "launch_date": "1977-09-05", - "status": "active" - }, - { - "id": "-32", - "name": "Voyager 2", - "name_zh": "旅行者2号", - "type": "probe", - "description": "唯一造访过天王星和海王星的探测器", - "launch_date": "1977-08-20", - "status": "active" - }, - { - "id": "-98", - "name": "New Horizons", - "name_zh": "新视野号", - "type": "probe", - "description": "飞掠冥王星,正处于柯伊伯带", - "launch_date": "2006-01-19", - "status": "active" - }, - { - "id": "-96", - "name": "Parker Solar Probe", - "name_zh": "帕克太阳探测器", - "type": "probe", - "description": "正在'触摸'太阳,速度最快的人造物体", - "launch_date": "2018-08-12", - "status": "active" - }, - { - "id": "-61", - "name": "Juno", - "name_zh": "朱诺号", - "type": "probe", - "description": "正在木星轨道运行", - "launch_date": "2011-08-05", - "status": "active" - }, - { - "id": "-82", - "name": "Cassini", - "name_zh": "卡西尼号", - "type": "probe", - "description": "土星探测器(已于2017年撞击销毁)", - "launch_date": "1997-10-15", - "status": "inactive" - }, - { - "id": "-168", - "name": "Perseverance", - "name_zh": "毅力号", - "type": "probe", - "description": "火星探测车", - "launch_date": "2020-07-30", - "status": "active" - } - ], - "metadata": { - "version": "1.0.0", - "last_updated": "2025-11-27", - "description": "Solar system celestial bodies catalog including planets, moons, and space probes. Future updates may include comets, asteroids, and other objects." - } -} diff --git a/frontend/public/models/cassini.glb b/frontend/public/models/cassini.glb deleted file mode 100644 index a531838..0000000 Binary files a/frontend/public/models/cassini.glb and /dev/null differ diff --git a/frontend/public/models/juno.glb b/frontend/public/models/juno.glb deleted file mode 100644 index fcf2c42..0000000 Binary files a/frontend/public/models/juno.glb and /dev/null differ diff --git a/frontend/public/models/parker_solar_probe.glb b/frontend/public/models/parker_solar_probe.glb deleted file mode 100644 index a405306..0000000 Binary files a/frontend/public/models/parker_solar_probe.glb and /dev/null differ diff --git a/frontend/public/models/voyager_1.glb b/frontend/public/models/voyager_1.glb deleted file mode 100644 index 3126b06..0000000 Binary files a/frontend/public/models/voyager_1.glb and /dev/null differ diff --git a/frontend/public/models/voyager_2.glb b/frontend/public/models/voyager_2.glb deleted file mode 100644 index bb997a2..0000000 Binary files a/frontend/public/models/voyager_2.glb and /dev/null differ diff --git a/frontend/public/test-models.html b/frontend/public/test-models.html new file mode 100644 index 0000000..da35f7b --- /dev/null +++ b/frontend/public/test-models.html @@ -0,0 +1,110 @@ + + + + Test Voyager Models + + + +
Loading...
+ + + + diff --git a/frontend/public/textures/2k_earth_daymap.jpg b/frontend/public/textures/2k_earth_daymap.jpg deleted file mode 100644 index 6cdcffe..0000000 Binary files a/frontend/public/textures/2k_earth_daymap.jpg and /dev/null differ diff --git a/frontend/public/textures/2k_earth_nightmap.jpg b/frontend/public/textures/2k_earth_nightmap.jpg deleted file mode 100644 index 863be2c..0000000 Binary files a/frontend/public/textures/2k_earth_nightmap.jpg and /dev/null differ diff --git a/frontend/public/textures/2k_jupiter.jpg b/frontend/public/textures/2k_jupiter.jpg deleted file mode 100644 index 21f1951..0000000 Binary files a/frontend/public/textures/2k_jupiter.jpg and /dev/null differ diff --git a/frontend/public/textures/2k_mars.jpg b/frontend/public/textures/2k_mars.jpg deleted file mode 100644 index b3654a9..0000000 Binary files a/frontend/public/textures/2k_mars.jpg and /dev/null differ diff --git a/frontend/public/textures/2k_mercury.jpg b/frontend/public/textures/2k_mercury.jpg deleted file mode 100644 index eebaed9..0000000 Binary files a/frontend/public/textures/2k_mercury.jpg and /dev/null differ diff --git a/frontend/public/textures/2k_moon.jpg b/frontend/public/textures/2k_moon.jpg deleted file mode 100644 index 8a1de86..0000000 Binary files a/frontend/public/textures/2k_moon.jpg and /dev/null differ diff --git a/frontend/public/textures/2k_neptune.jpg b/frontend/public/textures/2k_neptune.jpg deleted file mode 100644 index 30e7ac5..0000000 Binary files a/frontend/public/textures/2k_neptune.jpg and /dev/null differ diff --git a/frontend/public/textures/2k_saturn.jpg b/frontend/public/textures/2k_saturn.jpg deleted file mode 100644 index d8b23df..0000000 Binary files a/frontend/public/textures/2k_saturn.jpg and /dev/null differ diff --git a/frontend/public/textures/2k_saturn_ring_alpha.png b/frontend/public/textures/2k_saturn_ring_alpha.png deleted file mode 100644 index ce82586..0000000 Binary files a/frontend/public/textures/2k_saturn_ring_alpha.png and /dev/null differ diff --git a/frontend/public/textures/2k_stars_milky_way.jpg b/frontend/public/textures/2k_stars_milky_way.jpg deleted file mode 100644 index b7e0a1e..0000000 Binary files a/frontend/public/textures/2k_stars_milky_way.jpg and /dev/null differ diff --git a/frontend/public/textures/2k_sun.jpg b/frontend/public/textures/2k_sun.jpg deleted file mode 100644 index 9d787d8..0000000 Binary files a/frontend/public/textures/2k_sun.jpg and /dev/null differ diff --git a/frontend/public/textures/2k_uranus.jpg b/frontend/public/textures/2k_uranus.jpg deleted file mode 100644 index d0abf4c..0000000 Binary files a/frontend/public/textures/2k_uranus.jpg and /dev/null differ diff --git a/frontend/public/textures/2k_venus_atmosphere.jpg b/frontend/public/textures/2k_venus_atmosphere.jpg deleted file mode 100644 index 6448848..0000000 Binary files a/frontend/public/textures/2k_venus_atmosphere.jpg and /dev/null differ diff --git a/frontend/public/textures/2k_venus_surface.jpg b/frontend/public/textures/2k_venus_surface.jpg deleted file mode 100644 index a26a410..0000000 Binary files a/frontend/public/textures/2k_venus_surface.jpg and /dev/null differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3efdf32..26a9538 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,12 +6,16 @@ import { useState, useCallback } from 'react'; import { useSpaceData } from './hooks/useSpaceData'; import { useHistoricalData } from './hooks/useHistoricalData'; import { useTrajectory } from './hooks/useTrajectory'; +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 type { CelestialBody } from './types'; +// Timeline configuration - will be fetched from backend later +const TIMELINE_DAYS = 30; // Total days in timeline range + function App() { const [selectedDate, setSelectedDate] = useState(null); const [isTimelineMode, setIsTimelineMode] = useState(false); @@ -36,8 +40,8 @@ function App() { const toggleTimelineMode = useCallback(() => { setIsTimelineMode((prev) => !prev); if (!isTimelineMode) { - // Entering timeline mode, set initial date to Cassini launch (1997) - setSelectedDate(new Date(1997, 0, 1)); + // Entering timeline mode, set initial date to now (will play backward) + setSelectedDate(new Date()); } else { setSelectedDate(null); } @@ -45,7 +49,9 @@ function App() { // Filter probes and planets from all bodies const probes = bodies.filter((b) => b.type === 'probe'); - const planets = bodies.filter((b) => b.type === 'planet'); + const planets = bodies.filter((b) => + b.type === 'planet' || b.type === 'dwarf_planet' || b.type === 'satellite' + ); const handleBodySelect = (body: CelestialBody | null) => { setSelectedBody(body); @@ -71,24 +77,13 @@ function App() { return (
- {/* Title overlay */} -
-

Cosmo

-

深空探测器可视化

-

- {selectedBody ? `聚焦: ${selectedBody.name}` : `${bodies.length} 个天体`} -

- -
+ {/* Header with navigation and controls */} +
{/* Probe List Sidebar */} )} diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx new file mode 100644 index 0000000..1253da5 --- /dev/null +++ b/frontend/src/Router.tsx @@ -0,0 +1,51 @@ +/** + * Router configuration + */ +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { Login } from './pages/Login'; +import { AdminLayout } from './pages/admin/AdminLayout'; +import { Dashboard } from './pages/admin/Dashboard'; +import { CelestialBodies } from './pages/admin/CelestialBodies'; +import { auth } from './utils/auth'; +import App from './App'; + +// Protected Route wrapper +function ProtectedRoute({ children }: { children: React.ReactNode }) { + if (!auth.isLoggedIn()) { + return ; + } + return <>{children}; +} + +export function Router() { + return ( + + + {/* Public routes */} + } /> + + {/* Main app (3D visualization) */} + } /> + + {/* Admin routes (protected) */} + + + + } + > + } /> + } /> + } /> +

静态数据列表

开发中...

} /> +

NASA数据下载管理

开发中...

} /> +
+ + {/* Fallback */} + } /> + + + ); +} diff --git a/frontend/src/components/CameraController.tsx b/frontend/src/components/CameraController.tsx index 9a2214f..765c3e9 100644 --- a/frontend/src/components/CameraController.tsx +++ b/frontend/src/components/CameraController.tsx @@ -5,14 +5,15 @@ import { useEffect, useRef } from 'react'; import { useFrame, useThree } from '@react-three/fiber'; import { Vector3 } from 'three'; import type { CelestialBody } from '../types'; -import { scalePosition, scaleDistance } from '../utils/scaleDistance'; +import { calculateRenderPosition, findParentPlanet } from '../utils/renderPosition'; interface CameraControllerProps { focusTarget: CelestialBody | null; + allBodies: CelestialBody[]; onAnimationComplete?: () => void; } -export function CameraController({ focusTarget, onAnimationComplete }: CameraControllerProps) { +export function CameraController({ focusTarget, allBodies, onAnimationComplete }: CameraControllerProps) { const { camera } = useThree(); const targetPosition = useRef(new Vector3()); const isAnimating = useRef(false); @@ -21,13 +22,15 @@ export function CameraController({ focusTarget, onAnimationComplete }: CameraCon useEffect(() => { if (focusTarget) { - // Focus on target - use scaled position + // Focus on target - use smart rendered position const pos = focusTarget.positions[0]; - const scaledPos = scalePosition(pos.x, pos.y, pos.z); - const distance = Math.sqrt(pos.x ** 2 + pos.y ** 2 + pos.z ** 2); - const scaledDistance = scaleDistance(distance); + const renderPos = calculateRenderPosition(focusTarget, allBodies); + const scaledPos = { x: renderPos.x, y: renderPos.y, z: renderPos.z }; - // Calculate camera position based on target type + const distance = Math.sqrt(pos.x ** 2 + pos.y ** 2 + pos.z ** 2); + const parentInfo = findParentPlanet(focusTarget, allBodies); + + // Calculate camera position based on target type and context let offset: number; let heightMultiplier = 1; // For adjusting vertical position let sideMultiplier = 1; // For adjusting horizontal offset @@ -38,23 +41,27 @@ export function CameraController({ focusTarget, onAnimationComplete }: CameraCon heightMultiplier = 1.5; sideMultiplier = 1; } else if (focusTarget.type === 'probe') { - // For probes, determine view based on actual distance from Sun - if (distance < 10) { - // Very close probes (inner solar system, like Juno near Jupiter, Parker near Sun) - // Use a wide-angle side view to show both probe and nearby planet - offset = 15; - heightMultiplier = 0.4; // Lower camera for better side view - sideMultiplier = 2; // Move camera to the side - } else if (scaledDistance > 50) { - // Far probes (Voyagers, New Horizons) - offset = 20; - heightMultiplier = 1; + // For probes, determine view based on context + if (parentInfo) { + // Probe near a planet - use closer view to see both probe and planet + offset = 3; // Closer view (was 5) + heightMultiplier = 0.8; + sideMultiplier = 1.2; + } else if (distance < 10) { + // Inner solar system probe (not near a planet) + offset = 5; // Closer view (was 8) + heightMultiplier = 0.6; + sideMultiplier = 1.5; + } else if (distance > 50) { + // Far probes (Voyagers, New Horizons) - need even closer view since they're so far + offset = 4; // Much closer (was 12) + heightMultiplier = 0.8; sideMultiplier = 1; } else { // Medium distance probes - offset = 8; - heightMultiplier = 1; - sideMultiplier = 1; + offset = 6; // Closer view (was 10) + heightMultiplier = 0.8; + sideMultiplier = 1.2; } } else { offset = 10; @@ -79,7 +86,7 @@ export function CameraController({ focusTarget, onAnimationComplete }: CameraCon isAnimating.current = true; animationProgress.current = 0; } - }, [focusTarget, camera]); + }, [focusTarget, allBodies, camera]); useFrame((_, delta) => { if (isAnimating.current) { @@ -99,11 +106,10 @@ export function CameraController({ focusTarget, onAnimationComplete }: CameraCon // Interpolate camera position camera.position.lerpVectors(startPosition.current, targetPosition.current, eased); - // Look at target - use scaled position (only during animation) + // Look at target - use smart rendered position (only during animation) if (focusTarget) { - const pos = focusTarget.positions[0]; - const scaledPos = scalePosition(pos.x, pos.y, pos.z); - camera.lookAt(scaledPos.x, scaledPos.z, scaledPos.y); + const renderPos = calculateRenderPosition(focusTarget, allBodies); + camera.lookAt(renderPos.x, renderPos.z, renderPos.y); } else { camera.lookAt(0, 0, 0); } diff --git a/frontend/src/components/CelestialBody.tsx b/frontend/src/components/CelestialBody.tsx index d5158fd..1684ab3 100644 --- a/frontend/src/components/CelestialBody.tsx +++ b/frontend/src/components/CelestialBody.tsx @@ -1,15 +1,17 @@ /** * CelestialBody component - renders a planet or probe with textures */ -import { useRef, useMemo } from 'react'; +import { useRef, useMemo, useState, useEffect } from 'react'; import { Mesh, DoubleSide } from 'three'; import { useFrame } from '@react-three/fiber'; import { useTexture, Html } from '@react-three/drei'; import type { CelestialBody as CelestialBodyType } from '../types'; -import { scalePosition } from '../utils/scaleDistance'; +import { calculateRenderPosition, getOffsetDescription } from '../utils/renderPosition'; +import { fetchBodyResources } from '../utils/api'; interface CelestialBodyProps { body: CelestialBodyType; + allBodies: CelestialBodyType[]; } // Saturn Rings component - multiple rings for band effect @@ -71,51 +73,82 @@ function SaturnRings() { } // Planet component with texture -function Planet({ body, size, emissive, emissiveIntensity }: { +function Planet({ body, size, emissive, emissiveIntensity, allBodies }: { body: CelestialBodyType; size: number; emissive: string; emissiveIntensity: number; + allBodies: CelestialBodyType[]; }) { const meshRef = useRef(null); const position = body.positions[0]; + const [texturePath, setTexturePath] = useState(undefined); - // Apply non-linear distance scaling for better visualization - const scaledPos = useMemo(() => { - // Special handling for Moon - display it relative to Earth with visible offset - if (body.name === 'Moon') { - const moonScaled = scalePosition(position.x, position.y, position.z); - // Add a visual offset to make Moon visible next to Earth (2 units away) - // Moon orbits Earth at ~0.00257 AU, we'll give it a 2-unit offset from Earth's scaled position - const angle = Math.atan2(position.y, position.x); - const offset = 2.0; // Visual offset in scaled units - return { - x: moonScaled.x + Math.cos(angle) * offset, - y: moonScaled.y + Math.sin(angle) * offset, - z: moonScaled.z, - }; - } - return scalePosition(position.x, position.y, position.z); - }, [position.x, position.y, position.z, body.name]); + // Use smart render position calculation + const renderPosition = useMemo(() => { + return calculateRenderPosition(body, allBodies); + }, [position.x, position.y, position.z, body, allBodies]); - // Texture mapping for planets - const texturePath = useMemo(() => { - const textureMap: Record = { - Sun: '/textures/2k_sun.jpg', - Mercury: '/textures/2k_mercury.jpg', - Venus: '/textures/2k_venus_surface.jpg', - Earth: '/textures/2k_earth_daymap.jpg', - Moon: '/textures/2k_moon.jpg', - Mars: '/textures/2k_mars.jpg', - Jupiter: '/textures/2k_jupiter.jpg', - Saturn: '/textures/2k_saturn.jpg', - Uranus: '/textures/2k_uranus.jpg', - Neptune: '/textures/2k_neptune.jpg', - }; - return textureMap[body.name] || null; - }, [body.name]); + const scaledPos = { x: renderPosition.x, y: renderPosition.y, z: renderPosition.z }; - // Load texture - this must be at the top level, not in try-catch + // Fetch texture from backend API + useEffect(() => { + fetchBodyResources(body.id, 'texture') + .then((response) => { + // Find the main texture (not atmosphere or night layers) + const mainTexture = response.resources.find( + (r) => !r.file_path.includes('atmosphere') && !r.file_path.includes('night') + ); + if (mainTexture) { + // Construct full URL from file_path + // file_path is like "texture/2k_sun.jpg", need to add "upload/" prefix + const protocol = window.location.protocol; + const hostname = window.location.hostname; + const port = import.meta.env.VITE_API_BASE_URL ? '' : ':8000'; + setTexturePath(`${protocol}//${hostname}${port}/upload/${mainTexture.file_path}`); + } else { + setTexturePath(null); + } + }) + .catch((err) => { + console.error(`Failed to load texture for ${body.name}:`, err); + setTexturePath(null); + }); + }, [body.id, body.name]); + + // Show nothing while loading + if (texturePath === undefined) { + return null; + } + + return ; +} + +// Separate component to handle texture loading +function PlanetMesh({ body, size, emissive, emissiveIntensity, scaledPos, texturePath, position, meshRef, hasOffset, allBodies }: { + body: CelestialBodyType; + size: number; + emissive: string; + emissiveIntensity: number; + scaledPos: { x: number; y: number; z: number }; + texturePath: string | null; + position: { x: number; y: number; z: number }; + meshRef: React.RefObject; + hasOffset: boolean; + allBodies: CelestialBodyType[]; +}) { + // Load texture if path is provided const texture = texturePath ? useTexture(texturePath) : null; // Slow rotation for visual effect @@ -128,6 +161,9 @@ function Planet({ body, size, emissive, emissiveIntensity }: { // Calculate ACTUAL distance from Sun for display (not scaled) const distance = Math.sqrt(position.x ** 2 + position.y ** 2 + position.z ** 2); + // Get offset description if this body has one + const offsetDesc = hasOffset ? getOffsetDescription(body, allBodies) : null; + return ( @@ -185,6 +221,14 @@ function Planet({ body, size, emissive, emissiveIntensity }: { }} > {body.name_zh || body.name} + {offsetDesc && ( + <> +
+ + {offsetDesc} + + + )}
{distance.toFixed(2)} AU @@ -194,7 +238,7 @@ function Planet({ body, size, emissive, emissiveIntensity }: { ); } -export function CelestialBody({ body }: CelestialBodyProps) { +export function CelestialBody({ body, allBodies }: CelestialBodyProps) { // Get the current position (use the first position for now) const position = body.positions[0]; if (!position) return null; @@ -214,17 +258,30 @@ export function CelestialBody({ body }: CelestialBodyProps) { }; } + // Satellite (natural moons) - small size with slight glow for visibility + if (body.type === 'satellite') { + const satelliteSizes: Record = { + Moon: 0.15, // Small but visible + // Add other satellites here as needed + }; + return { + size: satelliteSizes[body.name] || 0.12, + emissive: '#888888', // Slight glow to make it visible + emissiveIntensity: 0.4, + }; + } + // Planet sizes - balanced for visibility with smaller probes const planetSizes: Record = { Mercury: 0.35, // Slightly larger for visibility Venus: 0.55, // Slightly larger for visibility Earth: 0.6, // Slightly larger for visibility - Moon: 0.25, // Smaller than Earth Mars: 0.45, // Slightly larger for visibility Jupiter: 1.4, // Larger gas giant Saturn: 1.2, // Larger gas giant Uranus: 0.8, // Medium outer planet Neptune: 0.8, // Medium outer planet + Pluto: 0.2, // Dwarf planet, smaller than Moon }; return { @@ -240,6 +297,7 @@ export function CelestialBody({ body }: CelestialBodyProps) { size={appearance.size} emissive={appearance.emissive} emissiveIntensity={appearance.emissiveIntensity} + allBodies={allBodies} /> ); } diff --git a/frontend/src/components/Constellations.tsx b/frontend/src/components/Constellations.tsx index 3122b84..025b639 100644 --- a/frontend/src/components/Constellations.tsx +++ b/frontend/src/components/Constellations.tsx @@ -4,6 +4,7 @@ import { useEffect, useState, useMemo } from 'react'; import { Line, Text, Billboard } from '@react-three/drei'; import * as THREE from 'three'; +import { fetchStaticData } from '../utils/api'; interface ConstellationStar { name: string; @@ -37,10 +38,18 @@ export function Constellations() { const [constellations, setConstellations] = useState([]); useEffect(() => { - // Load constellation data - fetch('/data/constellations.json') - .then((res) => res.json()) - .then((data) => setConstellations(data)) + // Load constellation data from API + fetchStaticData('constellation') + .then((response) => { + // Convert API response to Constellation format + const constellationData = response.items.map((item) => ({ + name: item.name, + name_zh: item.name_zh, + stars: item.data.stars, + lines: item.data.lines, + })); + setConstellations(constellationData); + }) .catch((err) => console.error('Failed to load constellations:', err)); }, []); diff --git a/frontend/src/components/DwarfPlanetOrbits.tsx b/frontend/src/components/DwarfPlanetOrbits.tsx new file mode 100644 index 0000000..dbccf31 --- /dev/null +++ b/frontend/src/components/DwarfPlanetOrbits.tsx @@ -0,0 +1,154 @@ +/** + * DwarfPlanetOrbits - renders orbital paths for dwarf planets using NASA data + * + * Dwarf planets have highly inclined orbits that deviate from the ecliptic plane + */ +import { useEffect, useState } from 'react'; +import { Line } from '@react-three/drei'; +import * as THREE from 'three'; +import { scalePosition } from '../utils/scaleDistance'; + +interface OrbitData { + bodyId: string; + bodyName: string; + points: THREE.Vector3[]; + color: string; +} + +export function DwarfPlanetOrbits() { + const [orbits, setOrbits] = useState([]); + const [loading, setLoading] = useState(true); + + // Helper function to get default colors + const getDefaultColor = (name: string): string => { + const colorMap: Record = { + 'Pluto': '#8B7355', + 'Ceres': '#9E9E9E', + 'Eris': '#E0E0E0', + 'Haumea': '#D4A574', + 'Makemake': '#C49A6C', + }; + return colorMap[name] || '#CCCCCC'; + }; + + useEffect(() => { + const fetchOrbits = async () => { + console.log('🌌 Fetching dwarf planet orbits from NASA...'); + const orbitData: OrbitData[] = []; + + try { + // Step 1: Get list of dwarf planets from backend + const listResponse = await fetch('http://localhost:8000/api/celestial/list?body_type=dwarf_planet'); + if (!listResponse.ok) { + console.warn('Failed to fetch dwarf planet list'); + setLoading(false); + return; + } + + const listData = await listResponse.json(); + const dwarfPlanets = listData.bodies || []; + + if (dwarfPlanets.length === 0) { + console.log('No dwarf planets found in database'); + setLoading(false); + return; + } + + console.log(`Found ${dwarfPlanets.length} dwarf planets:`, dwarfPlanets.map((p: any) => p.name_zh || p.name)); + + // Step 2: Fetch orbital data for all dwarf planets in ONE request + // Using a 10-year range with monthly samples + const startDate = new Date('2020-01-01'); + const endDate = new Date('2030-01-01'); + + // Use body_ids parameter to fetch all dwarf planets + const bodyIds = dwarfPlanets.map((p: any) => p.id).join(','); + + const response = await fetch( + `http://localhost:8000/api/celestial/positions?` + + `body_ids=${bodyIds}&` + + `start_time=${startDate.toISOString()}&` + + `end_time=${endDate.toISOString()}&` + + `step=30d` + ); + + if (!response.ok) { + console.warn('Failed to fetch dwarf planet orbits'); + setLoading(false); + return; + } + + const data = await response.json(); + + // Step 3: Process each dwarf planet's orbital data + for (const planet of dwarfPlanets) { + const bodyData = data.bodies.find((b: any) => b.id === planet.id); + + if (bodyData && bodyData.positions && bodyData.positions.length > 0) { + // Convert positions to Vector3 points with proper scaling + const points = bodyData.positions.map((pos: any) => { + // Apply the same non-linear scaling used by CelestialBody + const scaled = scalePosition(pos.x, pos.y, pos.z); + // Convert to Three.js coordinate system (x, z, y) + return new THREE.Vector3(scaled.x, scaled.z, scaled.y); + }); + + // Close the orbit loop if endpoints are close enough + const firstPoint = points[0]; + const lastPoint = points[points.length - 1]; + if (firstPoint.distanceTo(lastPoint) < 5) { + points.push(firstPoint.clone()); + } + + // Use color from database or default color + const color = planet.color || getDefaultColor(planet.name); + + orbitData.push({ + bodyId: planet.id, + bodyName: planet.name_zh || planet.name, + points, + color, + }); + + console.log(`✅ Loaded orbit for ${planet.name_zh || planet.name}: ${points.length} points`); + } + } + } catch (error) { + console.error('Error fetching dwarf planet orbits:', error); + } + + setOrbits(orbitData); + setLoading(false); + console.log(`🎉 Loaded ${orbitData.length} dwarf planet orbits`); + }; + + fetchOrbits(); + }, []); + + if (loading) { + console.log('⏳ Loading planet orbits...'); + return null; + } + + if (orbits.length === 0) { + console.warn('⚠️ No planet orbits loaded'); + return null; + } + + return ( + + {orbits.map((orbit) => ( + + {/* Orbital path */} + + + ))} + + ); +} diff --git a/frontend/src/components/Galaxies.tsx b/frontend/src/components/Galaxies.tsx index 47a1121..54712ad 100644 --- a/frontend/src/components/Galaxies.tsx +++ b/frontend/src/components/Galaxies.tsx @@ -4,6 +4,7 @@ import { useEffect, useState, useMemo } from 'react'; import { Billboard, Text, useTexture } from '@react-three/drei'; import * as THREE from 'three'; +import { fetchStaticData } from '../utils/api'; interface Galaxy { name: string; @@ -109,10 +110,23 @@ export function Galaxies() { const [galaxies, setGalaxies] = useState([]); useEffect(() => { - // Load galaxy data - fetch('/data/galaxies.json') - .then((res) => res.json()) - .then((data) => setGalaxies(data)) + // Load galaxy data from API + fetchStaticData('galaxy') + .then((response) => { + // Convert API response to Galaxy format + const galaxyData = response.items.map((item) => ({ + name: item.name, + name_zh: item.name_zh, + type: item.data.type, + distance_mly: item.data.distance_mly, + ra: item.data.ra, + dec: item.data.dec, + magnitude: item.data.magnitude, + diameter_kly: item.data.diameter_kly, + color: item.data.color, + })); + setGalaxies(galaxyData); + }) .catch((err) => console.error('Failed to load galaxies:', err)); }, []); diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx new file mode 100644 index 0000000..6cea709 --- /dev/null +++ b/frontend/src/components/Header.tsx @@ -0,0 +1,166 @@ +/** + * Header component - application header with navigation and controls + */ +import { useState } from 'react'; + +interface HeaderProps { + isTimelineMode: boolean; + onToggleTimeline: () => void; + bodyCount: number; + selectedBodyName?: string; +} + +export function Header({ + isTimelineMode, + onToggleTimeline, + bodyCount, + selectedBodyName +}: HeaderProps) { + const [showLoginModal, setShowLoginModal] = useState(false); + + return ( + <> +
+
+ {/* Left: Logo and Title */} +
+
+
+ 🌌 +
+
+

Cosmo

+

宇宙星空可视化平台

+
+
+ + {/* Status Info */} +
+

+ {selectedBodyName ? ( + <> + 聚焦: {selectedBodyName} + + ) : ( + <> + {bodyCount} 个天体 + + )} +

+
+
+ + {/* Right: Controls */} +
+ {/* Timeline Mode Toggle */} + + + {/* Login Button */} + +
+
+ +
+ + {/* Login Modal */} + {showLoginModal && ( +
setShowLoginModal(false)} + > +
e.stopPropagation()} + > +
+

登录 Cosmo

+ +
+ +
+
+ + +
+ +
+ + +
+ +
+ + + 忘记密码? + +
+ + + +
+ 还没有账号?{' '} + + 立即注册 + +
+
+
+
+ )} + + ); +} diff --git a/frontend/src/components/Nebulae.tsx b/frontend/src/components/Nebulae.tsx new file mode 100644 index 0000000..0896f19 --- /dev/null +++ b/frontend/src/components/Nebulae.tsx @@ -0,0 +1,251 @@ +/** + * Nebulae component - renders nebulae as billboards with procedural textures + */ +import { useEffect, useState, useMemo } from 'react'; +import { Billboard, Text } from '@react-three/drei'; +import * as THREE from 'three'; +import { fetchStaticData } from '../utils/api'; + +interface Nebula { + name: string; + name_zh: string; + type: string; // emission, planetary, supernova_remnant, dark + distance_ly: number; // Distance in light years + ra: number; // Right Ascension in degrees + dec: number; // Declination in degrees + magnitude: number; + diameter_ly: number; // Diameter in light years + color: string; +} + +/** + * Create a procedural nebula texture based on type + */ +function createNebulaTexture(color: string, type: string): THREE.Texture { + const canvas = document.createElement('canvas'); + canvas.width = 256; + canvas.height = 256; + const ctx = canvas.getContext('2d')!; + + const centerX = canvas.width / 2; + const centerY = canvas.height / 2; + const radius = canvas.width / 2; + + // Parse color + const tempColor = new THREE.Color(color); + const r = Math.floor(tempColor.r * 255); + const g = Math.floor(tempColor.g * 255); + const b = Math.floor(tempColor.b * 255); + + if (type === 'emission') { + // Emission nebulae: bright, colorful, wispy + const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius); + gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0.9)`); + gradient.addColorStop(0.3, `rgba(${r}, ${g}, ${b}, 0.7)`); + gradient.addColorStop(0.6, `rgba(${r}, ${g}, ${b}, 0.4)`); + gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Add wispy clouds + for (let i = 0; i < 30; i++) { + const angle = Math.random() * Math.PI * 2; + const dist = Math.random() * radius * 0.8; + const x = centerX + Math.cos(angle) * dist; + const y = centerY + Math.sin(angle) * dist; + const cloudGradient = ctx.createRadialGradient(x, y, 0, x, y, 15); + cloudGradient.addColorStop(0, `rgba(${r + 50}, ${g + 50}, ${b + 50}, 0.3)`); + cloudGradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`); + ctx.fillStyle = cloudGradient; + ctx.fillRect(x - 15, y - 15, 30, 30); + } + } else if (type === 'planetary') { + // Planetary nebulae: ring-like or spherical structure + const innerRadius = radius * 0.3; + const outerRadius = radius * 0.9; + + // Draw ring + for (let r_val = innerRadius; r_val < outerRadius; r_val += 1) { + const alpha = 1 - ((r_val - innerRadius) / (outerRadius - innerRadius)); + ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${alpha * 0.6})`; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.arc(centerX, centerY, r_val, 0, Math.PI * 2); + ctx.stroke(); + } + + // Add central star + const starGradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, 10); + starGradient.addColorStop(0, `rgba(255, 255, 255, 1.0)`); + starGradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0.5)`); + ctx.fillStyle = starGradient; + ctx.beginPath(); + ctx.arc(centerX, centerY, 10, 0, Math.PI * 2); + ctx.fill(); + } else if (type === 'supernova_remnant') { + // Supernova remnants: filamentary, expanding shell + const gradient = ctx.createRadialGradient(centerX, centerY, radius * 0.3, centerX, centerY, radius); + gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0)`); + gradient.addColorStop(0.5, `rgba(${r}, ${g}, ${b}, 0.8)`); + gradient.addColorStop(0.8, `rgba(${r}, ${g}, ${b}, 0.4)`); + gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Add filaments + for (let i = 0; i < 50; i++) { + const angle = Math.random() * Math.PI * 2; + const dist = radius * 0.5 + Math.random() * radius * 0.4; + const x = centerX + Math.cos(angle) * dist; + const y = centerY + Math.sin(angle) * dist; + const length = Math.random() * 20 + 5; + + ctx.strokeStyle = `rgba(${r + 50}, ${g + 50}, ${b + 50}, ${Math.random() * 0.6})`; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x + Math.cos(angle) * length, y + Math.sin(angle) * length); + ctx.stroke(); + } + } else if (type === 'dark') { + // Dark nebulae: darker silhouette against background + const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius); + gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0.6)`); + gradient.addColorStop(0.5, `rgba(${r}, ${g}, ${b}, 0.4)`); + gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Add darker patches + for (let i = 0; i < 15; i++) { + const x = centerX + (Math.random() - 0.5) * radius; + const y = centerY + (Math.random() - 0.5) * radius; + const patchGradient = ctx.createRadialGradient(x, y, 0, x, y, 20); + patchGradient.addColorStop(0, `rgba(${r * 0.5}, ${g * 0.5}, ${b * 0.5}, 0.5)`); + patchGradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`); + ctx.fillStyle = patchGradient; + ctx.fillRect(x - 20, y - 20, 40, 40); + } + } + + const texture = new THREE.CanvasTexture(canvas); + texture.needsUpdate = true; + return texture; +} + +/** + * Convert RA/Dec to Cartesian coordinates for celestial sphere + */ +function raDecToCartesian(ra: number, dec: number, distance: number) { + const raRad = (ra * Math.PI) / 180; + const decRad = (dec * Math.PI) / 180; + + const x = distance * Math.cos(decRad) * Math.cos(raRad); + const y = distance * Math.cos(decRad) * Math.sin(raRad); + const z = distance * Math.sin(decRad); + + return new THREE.Vector3(x, y, z); +} + +/** + * Calculate visual size based on angular diameter + * Nebulae are measured in light years of diameter and distance + */ +function calculateAngularSize(diameterLy: number, distanceLy: number): number { + // Angular diameter in radians + const angularDiameter = diameterLy / distanceLy; + // Scale for visualization (nebulae should be visible but not too large) + return Math.max(1.5, Math.min(8, angularDiameter * 3000)); +} + +export function Nebulae() { + const [nebulae, setNebulae] = useState([]); + + useEffect(() => { + // Load nebula data from API + fetchStaticData('nebula') + .then((response) => { + // Convert API response to Nebula format + const nebulaData = response.items.map((item) => ({ + name: item.name, + name_zh: item.name_zh, + type: item.data.type, + distance_ly: item.data.distance_ly, + ra: item.data.ra, + dec: item.data.dec, + magnitude: item.data.magnitude, + diameter_ly: item.data.diameter_ly, + color: item.data.color, + })); + setNebulae(nebulaData); + }) + .catch((err) => console.error('Failed to load nebulae:', err)); + }, []); + + const nebulaData = useMemo(() => { + return nebulae.map((nebula) => { + // Place nebulae on celestial sphere at fixed distance for visualization + const visualDistance = 150; // Between constellations (100) and galaxies (200) + const position = raDecToCartesian(nebula.ra, nebula.dec, visualDistance); + + // Calculate visual size based on angular diameter + const size = calculateAngularSize(nebula.diameter_ly, nebula.distance_ly); + + // Create procedural texture for this nebula + const texture = createNebulaTexture(nebula.color, nebula.type); + + return { + ...nebula, + position, + size, + texture, + }; + }); + }, [nebulae]); + + if (nebulaData.length === 0) { + return null; + } + + return ( + + {nebulaData.map((nebula) => ( + + + {/* Nebula texture */} + + + + + + + {/* Nebula name label - positioned slightly outward */} + + + {nebula.name_zh} + + + + ))} + + ); +} diff --git a/frontend/src/components/OrbitRenderer.tsx b/frontend/src/components/OrbitRenderer.tsx new file mode 100644 index 0000000..523b08f --- /dev/null +++ b/frontend/src/components/OrbitRenderer.tsx @@ -0,0 +1,125 @@ +/** + * OrbitRenderer - Unified orbit rendering component + * Renders precomputed orbital paths for all celestial bodies (planets and dwarf planets) + */ +import { useEffect, useState } from 'react'; +import { Line } from '@react-three/drei'; +import * as THREE from 'three'; +import { scalePosition } from '../utils/scaleDistance'; + +interface OrbitData { + bodyId: string; + bodyName: string; + bodyNameZh: string | null; + points: THREE.Vector3[]; + color: string; + numPoints: number; + periodDays: number | null; +} + +export function OrbitRenderer() { + const [orbits, setOrbits] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchOrbits = async () => { + console.log('🌌 Fetching orbital data from backend...'); + + try { + // Fetch precomputed orbits from backend + const response = await fetch('http://localhost:8000/api/celestial/orbits'); + + if (!response.ok) { + throw new Error(`Failed to fetch orbits: ${response.statusText}`); + } + + const data = await response.json(); + + if (!data.orbits || data.orbits.length === 0) { + console.warn('⚠️ No orbital data found in database'); + setLoading(false); + setError('No orbital data available. Please generate orbits first.'); + return; + } + + console.log(`📊 Processing ${data.orbits.length} orbits...`); + + // Convert to Three.js format + const orbitData: OrbitData[] = data.orbits.map((orbit: any) => { + // Convert position points to Vector3 with scaling + const points = orbit.points.map((p: any) => { + const scaled = scalePosition(p.x, p.y, p.z); + // Convert to Three.js coordinate system (x, z, y) + return new THREE.Vector3(scaled.x, scaled.z, scaled.y); + }); + + // Close the orbit loop if first and last points are close + if (points.length > 1) { + const firstPoint = points[0]; + const lastPoint = points[points.length - 1]; + const distance = firstPoint.distanceTo(lastPoint); + + // If endpoints are close (within 5 units), close the loop + if (distance < 5) { + points.push(firstPoint.clone()); + } + } + + console.log(` ✅ ${orbit.body_name_zh || orbit.body_name}: ${points.length} points`); + + return { + bodyId: orbit.body_id, + bodyName: orbit.body_name, + bodyNameZh: orbit.body_name_zh, + points, + color: orbit.color || '#CCCCCC', + numPoints: orbit.num_points, + periodDays: orbit.period_days, + }; + }); + + setOrbits(orbitData); + setLoading(false); + console.log(`🎉 Loaded ${orbitData.length} orbits successfully`); + + } catch (err) { + console.error('❌ Failed to load orbits:', err); + setError(err instanceof Error ? err.message : 'Unknown error'); + setLoading(false); + } + }; + + fetchOrbits(); + }, []); + + if (loading) { + console.log('⏳ Loading orbits...'); + return null; + } + + if (error) { + console.error('⚠️ Orbit rendering error:', error); + return null; + } + + if (orbits.length === 0) { + console.warn('⚠️ No orbits to render'); + return null; + } + + return ( + + {orbits.map((orbit) => ( + + ))} + + ); +} diff --git a/frontend/src/components/Probe.tsx b/frontend/src/components/Probe.tsx index 01854af..989d8ce 100644 --- a/frontend/src/components/Probe.tsx +++ b/frontend/src/components/Probe.tsx @@ -3,50 +3,86 @@ */ import { useRef, useMemo, useState, useEffect } from 'react'; import { Group } from 'three'; +import * as THREE from 'three'; import { useGLTF, Html } from '@react-three/drei'; import { useFrame } from '@react-three/fiber'; import type { CelestialBody } from '../types'; -import { scalePosition } from '../utils/scaleDistance'; +import { calculateRenderPosition, getOffsetDescription } from '../utils/renderPosition'; +import { fetchBodyResources } from '../utils/api'; interface ProbeProps { body: CelestialBody; + allBodies: CelestialBody[]; } -// Load probe model mapping from data file -const loadProbeModels = async (): Promise> => { - const response = await fetch('/data/probe-models.json'); - return response.json(); -}; - // Separate component for each probe type to properly use hooks -function ProbeModel({ body, modelPath }: { body: CelestialBody; modelPath: string }) { +function ProbeModel({ body, modelPath, allBodies, onError }: { + body: CelestialBody; + modelPath: string; + allBodies: CelestialBody[]; + onError: () => void; +}) { const groupRef = useRef(null); const position = body.positions[0]; - // Apply non-linear distance scaling - const scaledPos = useMemo(() => { - const baseScaled = scalePosition(position.x, position.y, position.z); - const distance = Math.sqrt(position.x ** 2 + position.y ** 2 + position.z ** 2); + // Use smart render position calculation + const renderPosition = useMemo(() => { + return calculateRenderPosition(body, allBodies); + }, [position.x, position.y, position.z, body, allBodies]); - // Special handling for probes very close to planets (< 10 AU from Sun) - // These probes need visual offset to avoid overlapping with planets - if (distance < 10) { - // Add a radial offset to push the probe away from the Sun (and nearby planets) - // This makes probes like Juno visible next to Jupiter - const angle = Math.atan2(position.y, position.x); - const offsetAmount = 3.0; // Visual offset in scaled units - return { - x: baseScaled.x + Math.cos(angle) * offsetAmount, - y: baseScaled.y + Math.sin(angle) * offsetAmount, - z: baseScaled.z, - }; - } - - return baseScaled; - }, [position.x, position.y, position.z]); + const scaledPos = { x: renderPosition.x, y: renderPosition.y, z: renderPosition.z }; // Load 3D model - must be at top level - const { scene } = useGLTF(modelPath); + // Add error handling and logging + let scene; + try { + const gltf = useGLTF(modelPath); + scene = gltf.scene; + console.log(`[ProbeModel ${body.name}] GLTF loaded successfully:`, { children: scene.children.length, modelPath }); + } catch (error) { + console.error(`[ProbeModel ${body.name}] Error loading GLTF:`, error); + // Call error callback and return null to trigger fallback + onError(); + return null; + } + + if (!scene || !scene.children || scene.children.length === 0) { + console.error(`[ProbeModel ${body.name}] GLTF scene is empty or invalid`); + onError(); + return null; + } + + // Calculate optimal scale based on model bounding box + const optimalScale = useMemo(() => { + // Calculate bounding box to determine model size + const box = new THREE.Box3().setFromObject(scene); + const size = new THREE.Vector3(); + box.getSize(size); + + // Get the largest dimension + const maxDimension = Math.max(size.x, size.y, size.z); + + // Target size for display (consistent visual size) + const targetSize = 0.5; // Target visual size in scene units + + // Calculate scale factor + // If model is very small, scale it up; if very large, scale it down + const calculatedScale = maxDimension > 0 ? targetSize / maxDimension : 0.2; + + // Clamp scale to reasonable range + const finalScale = Math.max(0.1, Math.min(2.0, calculatedScale)); + + console.log(`[ProbeModel ${body.name}] Model dimensions:`, { + x: size.x.toFixed(3), + y: size.y.toFixed(3), + z: size.z.toFixed(3), + maxDimension: maxDimension.toFixed(3), + calculatedScale: calculatedScale.toFixed(3), + finalScale: finalScale.toFixed(3) + }); + + return finalScale; + }, [scene, body.name]); // Configure model materials for proper rendering const configuredScene = useMemo(() => { @@ -93,17 +129,20 @@ function ProbeModel({ body, modelPath }: { body: CelestialBody; modelPath: strin // Calculate ACTUAL distance from Sun (not scaled) const distance = Math.sqrt(position.x ** 2 + position.y ** 2 + position.z ** 2); + // Get offset description if this probe has one + const offsetDesc = renderPosition.hasOffset ? getOffsetDescription(body, allBodies) : null; + return ( {/* Removed the semi-transparent sphere to avoid rendering conflicts */} - {/* Name label */} + {/* Name label - position based on model scale */} 🛰️ {body.name_zh || body.name} + {offsetDesc && ( + <> +
+ + {offsetDesc} + + + )}
{distance.toFixed(2)} AU @@ -127,31 +174,22 @@ function ProbeModel({ body, modelPath }: { body: CelestialBody; modelPath: strin } // Fallback component when model is not available -function ProbeFallback({ body }: { body: CelestialBody }) { +function ProbeFallback({ body, allBodies }: { body: CelestialBody; allBodies: CelestialBody[] }) { const position = body.positions[0]; - // Apply non-linear distance scaling - const scaledPos = useMemo(() => { - const baseScaled = scalePosition(position.x, position.y, position.z); - const distance = Math.sqrt(position.x ** 2 + position.y ** 2 + position.z ** 2); + // Use smart render position calculation + const renderPosition = useMemo(() => { + return calculateRenderPosition(body, allBodies); + }, [position.x, position.y, position.z, body, allBodies]); - // Special handling for probes very close to planets (< 10 AU from Sun) - if (distance < 10) { - const angle = Math.atan2(position.y, position.x); - const offsetAmount = 3.0; // Visual offset in scaled units - return { - x: baseScaled.x + Math.cos(angle) * offsetAmount, - y: baseScaled.y + Math.sin(angle) * offsetAmount, - z: baseScaled.z, - }; - } - - return baseScaled; - }, [position.x, position.y, position.z]); + const scaledPos = { x: renderPosition.x, y: renderPosition.y, z: renderPosition.z }; // Calculate ACTUAL distance from Sun (not scaled) const distance = Math.sqrt(position.x ** 2 + position.y ** 2 + position.z ** 2); + // Get offset description if this probe has one + const offsetDesc = renderPosition.hasOffset ? getOffsetDescription(body, allBodies) : null; + return ( @@ -175,6 +213,14 @@ function ProbeFallback({ body }: { body: CelestialBody }) { }} > 🛰️ {body.name_zh || body.name} + {offsetDesc && ( + <> +
+ + {offsetDesc} + + + )}
{distance.toFixed(2)} AU @@ -184,36 +230,65 @@ function ProbeFallback({ body }: { body: CelestialBody }) { ); } -export function Probe({ body }: ProbeProps) { +export function Probe({ body, allBodies }: ProbeProps) { const position = body.positions[0]; - const [modelMap, setModelMap] = useState>({}); - const [isLoading, setIsLoading] = useState(true); + const [modelPath, setModelPath] = useState(undefined); + const [loadError, setLoadError] = useState(false); - // Load model mapping on mount + // Fetch model from backend API useEffect(() => { - loadProbeModels().then((data) => { - setModelMap(data); - setIsLoading(false); - }); - }, []); + console.log(`[Probe ${body.name}] Fetching resources...`); + setLoadError(false); // Reset error state - if (!position) return null; - if (isLoading) return null; // Wait for model map to load + fetchBodyResources(body.id, 'model') + .then((response) => { + console.log(`[Probe ${body.name}] Resources response:`, response); + if (response.resources.length > 0) { + // Get the first model resource + const modelResource = response.resources[0]; + // Construct full URL from file_path + const protocol = window.location.protocol; + const hostname = window.location.hostname; + const port = import.meta.env.VITE_API_BASE_URL ? '' : ':8000'; + const fullPath = `${protocol}//${hostname}${port}/upload/${modelResource.file_path}`; + console.log(`[Probe ${body.name}] Model path:`, fullPath); - const modelPath = modelMap[body.name]; + // Preload the model before setting the path + useGLTF.preload(fullPath); + console.log(`[Probe ${body.name}] Model preloaded`); - // Use model if available, otherwise use fallback - if (modelPath) { - return ; + setModelPath(fullPath); + } else { + console.log(`[Probe ${body.name}] No resources found, using fallback`); + setModelPath(null); + } + }) + .catch((err) => { + console.error(`[Probe ${body.name}] Failed to load model:`, err); + setLoadError(true); + setModelPath(null); + }); + }, [body.id, body.name]); + + if (!position) { + console.log(`[Probe ${body.name}] No position data`); + return null; } - return ; -} + if (modelPath === undefined) { + console.log(`[Probe ${body.name}] Waiting for model path...`); + return null; // Wait for model to load + } -// Preload available models from data file -loadProbeModels().then((modelMap) => { - const modelsToPreload = Object.values(modelMap).filter((path): path is string => path !== null); - modelsToPreload.forEach((path) => { - useGLTF.preload(path); - }); -}); + console.log(`[Probe ${body.name}] Rendering with modelPath:`, modelPath, 'loadError:', loadError); + + // Use model if available and no load error, otherwise use fallback + if (modelPath && !loadError) { + return { + console.error(`[Probe ${body.name}] ProbeModel rendering failed, switching to fallback`); + setLoadError(true); + }} />; + } + + return ; +} diff --git a/frontend/src/components/ProbeList.tsx b/frontend/src/components/ProbeList.tsx index fd3b4d7..423569b 100644 --- a/frontend/src/components/ProbeList.tsx +++ b/frontend/src/components/ProbeList.tsx @@ -34,27 +34,24 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody }: Probe probesWithDistance.sort((a, b) => a.distance - b.distance); planetsWithDistance.sort((a, b) => a.distance - b.distance); - const totalCount = probes.length + planets.length - 1; // -1 for Sun - // Collapsed state - show only toggle button if (!isExpanded) { return ( ); } return ( -
+

🌍 天体列表 @@ -89,7 +86,7 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody }: Probe >
-
{body.name}
+
{body.name_zh || body.name}
{distance.toFixed(2)} AU
@@ -113,30 +110,41 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody }: Probe
加载中...
) : (
- {probesWithDistance.map(({ body, distance }) => ( -
- - ))} + + ); + })}
)}
diff --git a/frontend/src/components/Scene.tsx b/frontend/src/components/Scene.tsx index 0bc863f..3eb07a9 100644 --- a/frontend/src/components/Scene.tsx +++ b/frontend/src/components/Scene.tsx @@ -8,10 +8,11 @@ import { CelestialBody } from './CelestialBody'; import { Probe } from './Probe'; import { CameraController } from './CameraController'; import { Trajectory } from './Trajectory'; -import { Orbit } from './Orbit'; +import { OrbitRenderer } from './OrbitRenderer'; import { Stars } from './Stars'; import { Constellations } from './Constellations'; import { Galaxies } from './Galaxies'; +import { Nebulae } from './Nebulae'; import { scalePosition } from '../utils/scaleDistance'; import type { CelestialBody as CelestialBodyType, Position } from '../types'; @@ -22,14 +23,12 @@ interface SceneProps { } export function Scene({ bodies, selectedBody, trajectoryPositions = [] }: SceneProps) { - // Separate planets/stars from probes - const planets = bodies.filter((b) => b.type !== 'probe'); + // Separate natural celestial bodies (planets/dwarf planets/satellites/stars) from probes + const celestialBodies = bodies.filter((b) => b.type !== 'probe'); const probes = bodies.filter((b) => b.type === 'probe'); - // Filter probes to display based on focus - const visibleProbes = selectedBody?.type === 'probe' - ? probes.filter((p) => p.id === selectedBody.id) // Only show focused probe - : []; // In overview mode, hide all probes + // Always show all probes (changed from previous behavior) + const visibleProbes = probes; // Calculate target position for OrbitControls const controlsTarget = useMemo(() => { @@ -59,7 +58,7 @@ export function Scene({ bodies, selectedBody, trajectoryPositions = [] }: SceneP }} > {/* Camera controller for smooth transitions */} - + {/* Increase ambient light to see textures better */} @@ -83,29 +82,23 @@ export function Scene({ bodies, selectedBody, trajectoryPositions = [] }: SceneP {/* Major constellations */} + {/* Nebulae */} + + {/* Distant galaxies */} - {/* Render planets and stars */} - {planets.map((body) => ( - + {/* Render all celestial bodies: planets, dwarf planets, satellites, and stars */} + {celestialBodies.map((body) => ( + ))} - {/* Render planet orbits */} - {planets.map((body) => { - const pos = body.positions[0]; - const distance = Math.sqrt(pos.x ** 2 + pos.y ** 2 + pos.z ** 2); - // Only render orbits for planets (not Sun or Moon) - // Moon is too close to Earth, skip its orbit - if (body.type === 'planet' && distance > 0.1 && body.name !== 'Moon') { - return ; - } - return null; - })} + {/* Unified orbit renderer for all celestial bodies (planets and dwarf planets) */} + {/* Render visible probes with 3D models */} {visibleProbes.map((body) => ( - + ))} {/* Render trajectory for selected probe */} diff --git a/frontend/src/components/Stars.tsx b/frontend/src/components/Stars.tsx index eecb2e0..0f93515 100644 --- a/frontend/src/components/Stars.tsx +++ b/frontend/src/components/Stars.tsx @@ -49,10 +49,27 @@ export function Stars() { const [stars, setStars] = useState([]); useEffect(() => { - // Load star data - fetch('/data/nearby-stars.json') - .then((res) => res.json()) - .then((data) => setStars(data)) + // Load star data from API + fetch('http://localhost:8000/api/celestial/static/star') + .then((res) => { + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } + return res.json(); + }) + .then((data) => { + // API returns { category, items: [{ id, name, name_zh, data: {...} }] } + const starData = data.items.map((item: any) => ({ + name: item.name, + name_zh: item.name_zh, + distance_ly: item.data.distance_ly, + ra: item.data.ra, + dec: item.data.dec, + magnitude: item.data.magnitude, + color: item.data.color, + })); + setStars(starData); + }) .catch((err) => console.error('Failed to load stars:', err)); }, []); diff --git a/frontend/src/components/TimelineController.tsx b/frontend/src/components/TimelineController.tsx index e61e528..1f06239 100644 --- a/frontend/src/components/TimelineController.tsx +++ b/frontend/src/components/TimelineController.tsx @@ -18,12 +18,13 @@ interface TimelineControllerProps { } export function TimelineController({ onTimeChange, minDate, maxDate }: TimelineControllerProps) { - const startDate = minDate || new Date(Date.now() - 365 * 24 * 60 * 60 * 1000); // 1 year ago - const endDate = maxDate || new Date(); + // Swap: startDate is now (maxDate), endDate is past (minDate) + const startDate = maxDate || new Date(); // Start from now + const endDate = minDate || new Date(Date.now() - 365 * 24 * 60 * 60 * 1000); // End at past - const [currentDate, setCurrentDate] = useState(startDate); // Start from minDate instead of maxDate + const [currentDate, setCurrentDate] = useState(startDate); // Start from now const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(30); // 30 days per second + const [speed, setSpeed] = useState(1); // 1 day per second const animationFrameRef = useRef(); const lastUpdateRef = useRef(Date.now()); @@ -42,10 +43,10 @@ export function TimelineController({ onTimeChange, minDate, maxDate }: TimelineC lastUpdateRef.current = now; setCurrentDate((prev) => { - const newDate = new Date(prev.getTime() + speed * deltaSeconds * 24 * 60 * 60 * 1000); + const newDate = new Date(prev.getTime() - speed * deltaSeconds * 24 * 60 * 60 * 1000); // Subtract to go backward in time - // Loop back to start if we reach the end - if (newDate > endDate) { + // Loop back to start (now) if we reach the end (past) + if (newDate < endDate) { return new Date(startDate); } @@ -65,9 +66,20 @@ export function TimelineController({ onTimeChange, minDate, maxDate }: TimelineC }; }, [isPlaying, speed, startDate, endDate]); - // Notify parent of time changes + // Notify parent of time changes (debounced to avoid excessive updates) + const lastNotifiedDateRef = useRef(null); + useEffect(() => { - onTimeChange(currentDate); + // 圆整到天,避免同一天被通知多次 + const roundedDate = new Date(currentDate); + roundedDate.setUTCHours(0, 0, 0, 0); + const dateKey = roundedDate.toISOString(); + + // 只在日期真正变化时通知父组件 + if (lastNotifiedDateRef.current !== dateKey) { + lastNotifiedDateRef.current = dateKey; + onTimeChange(roundedDate); + } }, [currentDate, onTimeChange]); const handlePlayPause = useCallback(() => { @@ -80,22 +92,43 @@ export function TimelineController({ onTimeChange, minDate, maxDate }: TimelineC const handleSliderChange = useCallback((e: React.ChangeEvent) => { const value = parseFloat(e.target.value); - const totalRange = endDate.getTime() - startDate.getTime(); - const newDate = new Date(startDate.getTime() + (value / 100) * totalRange); + const totalRange = startDate.getTime() - endDate.getTime(); // Now - Past (positive) + const newDate = new Date(startDate.getTime() - (value / 100) * totalRange); // Start - progress setCurrentDate(newDate); setIsPlaying(false); }, [startDate, endDate]); - const currentProgress = ((currentDate.getTime() - startDate.getTime()) / (endDate.getTime() - startDate.getTime())) * 100; + const currentProgress = ((startDate.getTime() - currentDate.getTime()) / (startDate.getTime() - endDate.getTime())) * 100; + + // Format date as YYYY-MM-DD for top display + const formatFullDate = (date: Date) => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + }; + + // Format date as MM/DD for range labels + const formatShortDate = (date: Date) => { + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${month}/${day}`; + }; return (
-
-
时间轴
-
{currentDate.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' })}
+ {/* Current date display - full format */} +
+ {formatFullDate(currentDate)}
- {/* Progress bar */} + {/* Date range labels */} +
+ {formatShortDate(startDate)} + {formatShortDate(endDate)} +
+ + {/* Progress bar (standard slider) */} { - setCurrentDate(new Date(startDate)); + setCurrentDate(new Date(startDate)); // Reset to start (now) setIsPlaying(false); }} className="bg-gray-600 hover:bg-gray-700 text-white px-3 py-2 rounded text-xs" diff --git a/frontend/src/hooks/useHistoricalData.ts b/frontend/src/hooks/useHistoricalData.ts index 01ad514..9f71224 100644 --- a/frontend/src/hooks/useHistoricalData.ts +++ b/frontend/src/hooks/useHistoricalData.ts @@ -1,7 +1,7 @@ /** * Custom hook for fetching historical celestial data */ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { fetchCelestialPositions } from '../utils/api'; import type { CelestialBody } from '../types'; @@ -10,37 +10,52 @@ export function useHistoricalData(selectedDate: Date | null) { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const loadHistoricalData = useCallback(async (date: Date) => { - try { - setLoading(true); - setError(null); - - // For historical data, we just need a single snapshot at the given date - // Set start and end to the same date, or use a small range - const startDate = new Date(date); - const endDate = new Date(date); - endDate.setDate(endDate.getDate() + 1); // Add 1 day to ensure valid range - - const data = await fetchCelestialPositions( - startDate.toISOString(), - endDate.toISOString(), - '1d' - ); - - setBodies(data.bodies); - } catch (err) { - console.error('Failed to fetch historical data:', err); - setError(err instanceof Error ? err.message : 'Unknown error'); - } finally { - setLoading(false); - } - }, []); + // 使用 ref 跟踪上次请求的时间,避免重复请求 + const lastFetchedDateRef = useRef(null); useEffect(() => { - if (selectedDate) { - loadHistoricalData(selectedDate); + if (!selectedDate) { + return; } - }, [selectedDate, loadHistoricalData]); + + // 创建午夜时间戳 + const targetDate = new Date(selectedDate); + targetDate.setUTCHours(0, 0, 0, 0); + const dateKey = targetDate.toISOString(); + + // 如果是同一个时间点,不重复请求 + if (lastFetchedDateRef.current === dateKey) { + return; + } + + const loadHistoricalData = async () => { + try { + setLoading(true); + setError(null); + + console.log(`[useHistoricalData] Fetching data for ${dateKey}`); + + // Set start and end to the same time to get a single snapshot + const data = await fetchCelestialPositions( + targetDate.toISOString(), + targetDate.toISOString(), // Same as start - single point in time + '1d' + ); + + setBodies(data.bodies); + lastFetchedDateRef.current = dateKey; // 记录已请求的时间 + + console.log(`[useHistoricalData] Loaded ${data.bodies.length} bodies`); + } catch (err) { + console.error('Failed to fetch historical data:', err); + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + setLoading(false); + } + }; + + loadHistoricalData(); + }, [selectedDate]); return { bodies, loading, error }; } diff --git a/frontend/src/hooks/useSpaceData.ts b/frontend/src/hooks/useSpaceData.ts index 229e39c..43e5c5e 100644 --- a/frontend/src/hooks/useSpaceData.ts +++ b/frontend/src/hooks/useSpaceData.ts @@ -16,8 +16,17 @@ export function useSpaceData() { setLoading(true); setError(null); - // Fetch current positions - const data = await fetchCelestialPositions(); + // Fetch current position - single point in time at current hour + // Round to current hour (00 minutes, 00 seconds) + const now = new Date(); + now.setMinutes(0, 0, 0); + + const data = await fetchCelestialPositions( + now.toISOString(), + now.toISOString(), // Same as start - single point in time + '1h' // 1 hour step (though doesn't matter for single point) + ); + setBodies(data.bodies); } catch (err) { console.error('Failed to fetch celestial data:', err); diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index bef5202..949049a 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,10 +1,10 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' -import App from './App.tsx' +import { Router } from './Router' createRoot(document.getElementById('root')!).render( - + , ) diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx new file mode 100644 index 0000000..2bf7204 --- /dev/null +++ b/frontend/src/pages/Login.tsx @@ -0,0 +1,98 @@ +/** + * Login Page + */ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Form, Input, Button, Card, message } from 'antd'; +import { UserOutlined, LockOutlined } from '@ant-design/icons'; +import { authAPI } from '../utils/request'; +import { auth } from '../utils/auth'; + +export function Login() { + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + + const onFinish = async (values: { username: string; password: string }) => { + setLoading(true); + try { + const { data } = await authAPI.login(values.username, values.password); + + // Save token and user info + auth.setToken(data.access_token); + auth.setUser(data.user); + + message.success('登录成功!'); + + // Redirect to admin dashboard + navigate('/admin'); + } catch (error: any) { + message.error(error.response?.data?.detail || '登录失败,请检查用户名和密码'); + } finally { + setLoading(false); + } + }; + + return ( +
+ +
+

Cosmo 后台管理

+

宇宙星空可视化平台

+
+ +
+ + } + placeholder="用户名" + /> + + + + } + placeholder="密码" + /> + + + + + +
+ +
+

默认账号: cosmo / cosmo

+
+
+
+ ); +} diff --git a/frontend/src/pages/admin/AdminLayout.tsx b/frontend/src/pages/admin/AdminLayout.tsx new file mode 100644 index 0000000..285a36b --- /dev/null +++ b/frontend/src/pages/admin/AdminLayout.tsx @@ -0,0 +1,169 @@ +/** + * Admin Layout with Sidebar + */ +import { useState, useEffect } from 'react'; +import { Outlet, useNavigate, useLocation } from 'react-router-dom'; +import { Layout, Menu, Avatar, Dropdown, message } from 'antd'; +import { + MenuFoldOutlined, + MenuUnfoldOutlined, + DashboardOutlined, + DatabaseOutlined, + DownloadOutlined, + UserOutlined, + LogoutOutlined, + RocketOutlined, + AppstoreOutlined, +} from '@ant-design/icons'; +import type { MenuProps } from 'antd'; +import { authAPI } from '../../utils/request'; +import { auth } from '../../utils/auth'; + +const { Header, Sider, Content } = Layout; + +// Icon mapping +const iconMap: Record = { + dashboard: , + database: , + planet: , + data: , + download: , +}; + +export function AdminLayout() { + const [collapsed, setCollapsed] = useState(false); + const [menus, setMenus] = useState([]); + const [loading, setLoading] = useState(true); + const navigate = useNavigate(); + const location = useLocation(); + const user = auth.getUser(); + + // Load menus from backend + useEffect(() => { + loadMenus(); + }, []); + + const loadMenus = async () => { + try { + const { data } = await authAPI.getMenus(); + setMenus(data); + } catch (error) { + message.error('加载菜单失败'); + } finally { + setLoading(false); + } + }; + + // Convert backend menu to Ant Design menu format + const convertMenus = (menus: any[]): MenuProps['items'] => { + return menus.map((menu) => { + const item: any = { + key: menu.path || menu.name, + icon: iconMap[menu.icon || ''] || null, + label: menu.title, + }; + + if (menu.children && menu.children.length > 0) { + item.children = convertMenus(menu.children); + } + + return item; + }); + }; + + const handleMenuClick: MenuProps['onClick'] = ({ key }) => { + navigate(key); + }; + + const handleLogout = async () => { + try { + await authAPI.logout(); + auth.logout(); + message.success('登出成功'); + navigate('/login'); + } catch (error) { + // Even if API fails, clear local auth + auth.logout(); + navigate('/login'); + } + }; + + const userMenuItems: MenuProps['items'] = [ + { + key: 'profile', + icon: , + label: '个人信息', + }, + { + type: 'divider', + }, + { + key: 'logout', + icon: , + label: '退出登录', + onClick: handleLogout, + }, + ]; + + return ( + + +
+ {collapsed ? '🌌' : '🌌 Cosmo'} +
+ + + +
+
setCollapsed(!collapsed)} + style={{ fontSize: 18, cursor: 'pointer' }} + > + {collapsed ? : } +
+ + +
+ } style={{ marginRight: 8 }} /> + {user?.username || 'User'} +
+
+
+ + + +
+ + ); +} diff --git a/frontend/src/pages/admin/CelestialBodies.tsx b/frontend/src/pages/admin/CelestialBodies.tsx new file mode 100644 index 0000000..56272f9 --- /dev/null +++ b/frontend/src/pages/admin/CelestialBodies.tsx @@ -0,0 +1,93 @@ +/** + * Celestial Bodies Management Page + */ +import { useState, useEffect } from 'react'; +import { Table, Button, message } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { request } from '../../utils/request'; + +interface CelestialBody { + id: string; + name: string; + name_zh: string; + type: string; + description: string; +} + +export function CelestialBodies() { + const [loading, setLoading] = useState(false); + const [data, setData] = useState([]); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + setLoading(true); + try { + const { data: result } = await request.get('/celestial/list'); + setData(result.bodies || []); + } catch (error) { + message.error('加载数据失败'); + } finally { + setLoading(false); + } + }; + + const columns: ColumnsType = [ + { + title: 'ID', + dataIndex: 'id', + key: 'id', + width: 100, + }, + { + title: '英文名', + dataIndex: 'name', + key: 'name', + }, + { + title: '中文名', + dataIndex: 'name_zh', + key: 'name_zh', + }, + { + title: '类型', + dataIndex: 'type', + key: 'type', + render: (type: string) => { + const typeMap: Record = { + star: '恒星', + planet: '行星', + dwarf_planet: '矮行星', + probe: '探测器', + }; + return typeMap[type] || type; + }, + }, + { + title: '描述', + dataIndex: 'description', + key: 'description', + ellipsis: true, + }, + ]; + + return ( +
+
+

天体数据列表

+ +
+ + + ); +} diff --git a/frontend/src/pages/admin/Dashboard.tsx b/frontend/src/pages/admin/Dashboard.tsx new file mode 100644 index 0000000..7b7ba06 --- /dev/null +++ b/frontend/src/pages/admin/Dashboard.tsx @@ -0,0 +1,42 @@ +/** + * Dashboard Page + */ +import { Card, Row, Col, Statistic } from 'antd'; +import { DatabaseOutlined, GlobalOutlined, RocketOutlined } from '@ant-design/icons'; + +export function Dashboard() { + return ( +
+

控制台

+ +
+ + } + /> + + + + + } + /> + + + + + } + /> + + + + + ); +} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 87f48c9..eb7aa89 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -2,7 +2,7 @@ * TypeScript type definitions for Cosmo application */ -export type CelestialBodyType = 'planet' | 'probe' | 'star'; +export type CelestialBodyType = 'planet' | 'probe' | 'star' | 'dwarf_planet' | 'satellite'; export interface Position { time: string; @@ -18,6 +18,7 @@ export interface CelestialBody { type: CelestialBodyType; positions: Position[]; description?: string; + is_active?: boolean; // Probe status: true = active, false = inactive } export interface CelestialDataResponse { diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts index ccd48e1..dbd7b93 100644 --- a/frontend/src/utils/api.ts +++ b/frontend/src/utils/api.ts @@ -26,7 +26,7 @@ console.log('[API] Final API_BASE_URL:', API_BASE_URL); export const api = axios.create({ baseURL: API_BASE_URL, - timeout: 30000, + timeout: 120000, // Increase timeout to 120 seconds for historical data queries }); // Add request interceptor for debugging @@ -91,3 +91,50 @@ export async function fetchAllBodies(): Promise<{ bodies: BodyInfo[] }> { const response = await api.get('/celestial/list'); return response.data; } + +/** + * Fetch static data by category (constellation, galaxy, nebula, star, cluster) + */ +export async function fetchStaticData(category: string): Promise<{ + category: string; + items: Array<{ + id: number; + name: string; + name_zh: string; + data: any; + }>; +}> { + const response = await api.get(`/celestial/static/${category}`); + return response.data; +} + +/** + * Get resource URL from backend + */ +export function getResourceUrl(type: 'texture' | 'model' | 'icon' | 'thumbnail' | 'data', filename: string): string { + const protocol = window.location.protocol; + const hostname = window.location.hostname; + const port = import.meta.env.VITE_API_BASE_URL ? '' : ':8000'; + return `${protocol}//${hostname}${port}/upload/${type}/${filename}`; +} + +/** + * Fetch resources for a celestial body + */ +export async function fetchBodyResources(bodyId: string, resourceType?: string): Promise<{ + body_id: string; + resources: Array<{ + id: number; + resource_type: string; + file_path: string; + file_size: number; + mime_type: string; + created_at: string; + }>; +}> { + const params: Record = {}; + if (resourceType) params.resource_type = resourceType; + + const response = await api.get(`/celestial/resources/${bodyId}`, { params }); + return response.data; +} diff --git a/frontend/src/utils/auth.ts b/frontend/src/utils/auth.ts new file mode 100644 index 0000000..cc292a2 --- /dev/null +++ b/frontend/src/utils/auth.ts @@ -0,0 +1,50 @@ +/** + * Authentication utilities + */ + +const TOKEN_KEY = 'cosmo_token'; +const USER_KEY = 'cosmo_user'; + +export const auth = { + // Get token from localStorage + getToken(): string | null { + return localStorage.getItem(TOKEN_KEY); + }, + + // Save token to localStorage + setToken(token: string): void { + localStorage.setItem(TOKEN_KEY, token); + }, + + // Remove token from localStorage + removeToken(): void { + localStorage.removeItem(TOKEN_KEY); + }, + + // Check if user is logged in + isLoggedIn(): boolean { + return !!this.getToken(); + }, + + // Get user info from localStorage + getUser(): any { + const userStr = localStorage.getItem(USER_KEY); + return userStr ? JSON.parse(userStr) : null; + }, + + // Save user info to localStorage + setUser(user: any): void { + localStorage.setItem(USER_KEY, JSON.stringify(user)); + }, + + // Remove user info from localStorage + removeUser(): void { + localStorage.removeItem(USER_KEY); + }, + + // Logout - clear all auth data + logout(): void { + this.removeToken(); + this.removeUser(); + }, +}; diff --git a/frontend/src/utils/renderPosition.ts b/frontend/src/utils/renderPosition.ts new file mode 100644 index 0000000..59d5c3a --- /dev/null +++ b/frontend/src/utils/renderPosition.ts @@ -0,0 +1,92 @@ +/** + * Position calculator for celestial bodies + * Returns the scaled rendering position without artificial offsets + */ + +import { scalePosition } from './scaleDistance'; +import type { CelestialBody } from '../types'; + +/** + * Calculate rendering position using true scaled coordinates + * For satellites, add a radial offset to avoid being hidden by parent planet + */ +export function calculateRenderPosition( + body: CelestialBody, + allBodies: CelestialBody[] +): { x: number; y: number; z: number } { + const pos = body.positions[0]; + if (!pos) { + return { x: 0, y: 0, z: 0 }; + } + + // Use improved scaling that handles near-Earth objects properly + const scaled = scalePosition(pos.x, pos.y, pos.z); + + // For satellites, add a radial offset to separate from parent planet + if (body.type === 'satellite') { + const distance = Math.sqrt(scaled.x ** 2 + scaled.y ** 2 + scaled.z ** 2); + if (distance > 0) { + // Add fixed offset: push satellite 1.2 units further from Sun + // This ensures it's always visible outside the parent planet's visual radius + const fixedOffset = 1.05; + const direction = { + x: scaled.x / distance, + y: scaled.y / distance, + z: scaled.z / distance + }; + + return { + x: scaled.x + direction.x * fixedOffset, + y: scaled.y + direction.y * fixedOffset, + z: scaled.z + direction.z * fixedOffset + }; + } + } + + return { x: scaled.x, y: scaled.y, z: scaled.z }; +} + +/** + * Find the parent planet for a celestial body (e.g., Moon orbits Earth) + * Returns the planet that this body is closest to, if within a threshold + */ +export function findParentPlanet( + body: CelestialBody, + allBodies: CelestialBody[] +): CelestialBody | null { + const pos = body.positions[0]; + if (!pos) return null; + + const planets = allBodies.filter(b => b.type === 'planet' || b.type === 'dwarf_planet'); + + let closestPlanet: CelestialBody | null = null; + let minDistance = Infinity; + + for (const planet of planets) { + const planetPos = planet.positions[0]; + if (!planetPos) continue; + + const distance = Math.sqrt( + Math.pow(pos.x - planetPos.x, 2) + + Math.pow(pos.y - planetPos.y, 2) + + Math.pow(pos.z - planetPos.z, 2) + ); + + // Consider as "near" if within 0.05 AU (~7.5 million km) + if (distance < 0.05 && distance < minDistance) { + closestPlanet = planet; + minDistance = distance; + } + } + + return closestPlanet; +} + +/** + * Get description for bodies that are close to planets + */ +export function getOffsetDescription(body: CelestialBody, allBodies: CelestialBody[]): string | null { + // This function is kept for compatibility but no longer calculates offsets + // Could be used to show proximity information in the future + return null; +} diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts new file mode 100644 index 0000000..ccc3adb --- /dev/null +++ b/frontend/src/utils/request.ts @@ -0,0 +1,68 @@ +/** + * Axios request configuration with authentication + */ +import axios from 'axios'; +import { auth } from './auth'; + +const API_BASE_URL = 'http://localhost:8000/api'; + +// Create axios instance +export const request = axios.create({ + baseURL: API_BASE_URL, + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Request interceptor - add token to headers +request.interceptors.request.use( + (config) => { + const token = auth.getToken(); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +// Response interceptor - handle auth errors +request.interceptors.response.use( + (response) => { + return response; + }, + (error) => { + if (error.response?.status === 401) { + // Unauthorized - clear auth and redirect to login + auth.logout(); + window.location.href = '/login'; + } + return Promise.reject(error); + } +); + +// API functions +export const authAPI = { + // Login + login(username: string, password: string) { + return request.post('/auth/login', { username, password }); + }, + + // Logout + logout() { + return request.post('/auth/logout'); + }, + + // Get current user info + getCurrentUser() { + return request.get('/auth/me'); + }, + + // Get user menus + getMenus() { + return request.get('/auth/menus'); + }, +}; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 97c0dd5..5a699df 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -7,6 +7,66 @@ resolved "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== +"@ant-design/colors@^8.0.0": + version "8.0.0" + resolved "https://registry.npmmirror.com/@ant-design/colors/-/colors-8.0.0.tgz#92b5aa1cd44896b62c7b67133b4d5a6a00266162" + integrity sha512-6YzkKCw30EI/E9kHOIXsQDHmMvTllT8STzjMb4K2qzit33RW2pqCJP0sk+hidBntXxE+Vz4n1+RvCTfBw6OErw== + dependencies: + "@ant-design/fast-color" "^3.0.0" + +"@ant-design/cssinjs-utils@^2.0.0": + version "2.0.2" + resolved "https://registry.npmmirror.com/@ant-design/cssinjs-utils/-/cssinjs-utils-2.0.2.tgz#5613c65e0ecb3ce354b9e2164ce91125bd2b5bdc" + integrity sha512-Mq3Hm6fJuQeFNKSp3+yT4bjuhVbdrsyXE2RyfpJFL0xiYNZdaJ6oFaE3zFrzmHbmvTd2Wp3HCbRtkD4fU+v2ZA== + dependencies: + "@ant-design/cssinjs" "^2.0.1" + "@babel/runtime" "^7.23.2" + "@rc-component/util" "^1.4.0" + +"@ant-design/cssinjs@^2.0.0", "@ant-design/cssinjs@^2.0.1": + version "2.0.1" + resolved "https://registry.npmmirror.com/@ant-design/cssinjs/-/cssinjs-2.0.1.tgz#a7742deba17d613769db6d1aa4cfa46222ccec45" + integrity sha512-Lw1Z4cUQxdMmTNir67gU0HCpTl5TtkKCJPZ6UBvCqzcOTl/QmMFB6qAEoj8qFl0CuZDX9qQYa3m9+rEKfaBSbA== + dependencies: + "@babel/runtime" "^7.11.1" + "@emotion/hash" "^0.8.0" + "@emotion/unitless" "^0.7.5" + "@rc-component/util" "^1.4.0" + clsx "^2.1.1" + csstype "^3.1.3" + stylis "^4.3.4" + +"@ant-design/fast-color@^3.0.0": + version "3.0.0" + resolved "https://registry.npmmirror.com/@ant-design/fast-color/-/fast-color-3.0.0.tgz#fb5178203de825f284809538f5142203d0ef3d80" + integrity sha512-eqvpP7xEDm2S7dUzl5srEQCBTXZMmY3ekf97zI+M2DHOYyKdJGH0qua0JACHTqbkRnD/KHFQP9J1uMJ/XWVzzA== + +"@ant-design/icons-svg@^4.4.0": + version "4.4.2" + resolved "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz#ed2be7fb4d82ac7e1d45a54a5b06d6cecf8be6f6" + integrity sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA== + +"@ant-design/icons@^6.1.0": + version "6.1.0" + resolved "https://registry.npmmirror.com/@ant-design/icons/-/icons-6.1.0.tgz#97cc14a3c0528b8e2b37f41f232b019f2ca38c2c" + integrity sha512-KrWMu1fIg3w/1F2zfn+JlfNDU8dDqILfA5Tg85iqs1lf8ooyGlbkA+TkwfOKKgqpUmAiRY1PTFpuOU2DAIgSUg== + dependencies: + "@ant-design/colors" "^8.0.0" + "@ant-design/icons-svg" "^4.4.0" + "@rc-component/util" "^1.3.0" + clsx "^2.1.1" + +"@ant-design/react-slick@~1.1.2": + version "1.1.2" + resolved "https://registry.npmmirror.com/@ant-design/react-slick/-/react-slick-1.1.2.tgz#f84ce3e4d0dc941f02b16f1d1d6d7a371ffbb4f1" + integrity sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA== + dependencies: + "@babel/runtime" "^7.10.4" + classnames "^2.2.5" + json2mq "^0.2.0" + resize-observer-polyfill "^1.5.1" + throttle-debounce "^5.0.0" + "@babel/code-frame@^7.27.1": version "7.27.1" resolved "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" @@ -135,7 +195,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/runtime@^7.17.8", "@babel/runtime@^7.26.0": +"@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.7", "@babel/runtime@^7.26.0": version "7.28.4" resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== @@ -175,6 +235,16 @@ resolved "https://registry.npmmirror.com/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz#7b3365e1dfdc5cd957b45afe920b4ac06c7cd389" integrity sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow== +"@emotion/hash@^0.8.0": + version "0.8.0" + resolved "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" + integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== + +"@emotion/unitless@^0.7.5": + version "0.7.5" + resolved "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" + integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== + "@esbuild/aix-ppc64@0.25.12": version "0.25.12" resolved "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz#80fcbe36130e58b7670511e888b8e88a259ed76c" @@ -463,6 +533,377 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@rc-component/async-validator@^5.0.3": + version "5.0.4" + resolved "https://registry.npmmirror.com/@rc-component/async-validator/-/async-validator-5.0.4.tgz#5291ad92f00a14b6766fc81735c234277f83e948" + integrity sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg== + dependencies: + "@babel/runtime" "^7.24.4" + +"@rc-component/cascader@~1.7.0": + version "1.7.0" + resolved "https://registry.npmmirror.com/@rc-component/cascader/-/cascader-1.7.0.tgz#1f6c07d26d1cc784938fd628f0aede75e731241b" + integrity sha512-Cg8AlH+9N7vht7n+bKMkJCP5ERn9HJXMYLuaLC2wVq+Fapzr+3Ei7lNr7F4OjLkXdtMhkgiX4AZBEqja8+goxw== + dependencies: + "@rc-component/select" "~1.2.0" + "@rc-component/tree" "~1.0.0" + "@rc-component/util" "^1.3.0" + clsx "^2.1.1" + +"@rc-component/checkbox@~1.0.0": + version "1.0.1" + resolved "https://registry.npmmirror.com/@rc-component/checkbox/-/checkbox-1.0.1.tgz#d9ff0e67e5e55be0cad597083d5e9d972cc4e567" + integrity sha512-08yTH8m+bSm8TOqbybbJ9KiAuIATti6bDs2mVeSfu4QfEnyeF6X0enHVvD1NEAyuBWEAo56QtLe++MYs2D9XiQ== + dependencies: + "@rc-component/util" "^1.3.0" + clsx "^2.1.1" + +"@rc-component/collapse@~1.1.1": + version "1.1.2" + resolved "https://registry.npmmirror.com/@rc-component/collapse/-/collapse-1.1.2.tgz#7e0ff96a8292600bf774584257c9dc283891aea3" + integrity sha512-ilBYk1dLLJHu5Q74dF28vwtKUYQ42ZXIIDmqTuVy4rD8JQVvkXOs+KixVNbweyuIEtJYJ7+t+9GVD9dPc6N02w== + dependencies: + "@babel/runtime" "^7.10.1" + "@rc-component/motion" "^1.1.4" + "@rc-component/util" "^1.3.0" + clsx "^2.1.1" + +"@rc-component/color-picker@~3.0.2": + version "3.0.3" + resolved "https://registry.npmmirror.com/@rc-component/color-picker/-/color-picker-3.0.3.tgz#5e37251efed0d7e82ce71b6a9a9d6b0aa9bd10f2" + integrity sha512-V7gFF9O7o5XwIWafdbOtqI4BUUkEUkgdBwp6favy3xajMX/2dDqytFaiXlcwrpq6aRyPLp5dKLAG5RFKLXMeGA== + dependencies: + "@ant-design/fast-color" "^3.0.0" + "@rc-component/util" "^1.3.0" + clsx "^2.1.1" + +"@rc-component/context@^2.0.1": + version "2.0.1" + resolved "https://registry.npmmirror.com/@rc-component/context/-/context-2.0.1.tgz#88c7a565ae92c34a7f02f33c34b145e4039deed0" + integrity sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw== + dependencies: + "@rc-component/util" "^1.3.0" + +"@rc-component/dialog@~1.5.0": + version "1.5.1" + resolved "https://registry.npmmirror.com/@rc-component/dialog/-/dialog-1.5.1.tgz#c02f6109d4fdd3cdf4e74384837171342685f223" + integrity sha512-by4Sf/a3azcb89WayWuwG19/Y312xtu8N81HoVQQtnsBDylfs+dog98fTAvLinnpeoWG52m/M7QLRW6fXR3l1g== + dependencies: + "@rc-component/motion" "^1.1.3" + "@rc-component/portal" "^2.0.0" + "@rc-component/util" "^1.0.1" + clsx "^2.1.1" + +"@rc-component/drawer@~1.2.0": + version "1.2.0" + resolved "https://registry.npmmirror.com/@rc-component/drawer/-/drawer-1.2.0.tgz#4e1b08beed21f02a8e31f47ea741ef14dcf5eff4" + integrity sha512-RZ8IoNUv/soNVMYIWdjelKXX/3LWhVrKUQAeoc966Y55cIGc+PQKni025xshsvTY/+ntq10wqlBw1WCi77MvYQ== + dependencies: + "@rc-component/motion" "^1.1.4" + "@rc-component/portal" "^2.0.0" + "@rc-component/util" "^1.2.1" + classnames "^2.2.6" + +"@rc-component/dropdown@~1.0.0": + version "1.0.2" + resolved "https://registry.npmmirror.com/@rc-component/dropdown/-/dropdown-1.0.2.tgz#c6010dac9e3ce0d7cf305523083d499dc779819e" + integrity sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg== + dependencies: + "@rc-component/trigger" "^3.0.0" + "@rc-component/util" "^1.2.1" + clsx "^2.1.1" + +"@rc-component/form@~1.4.0": + version "1.4.0" + resolved "https://registry.npmmirror.com/@rc-component/form/-/form-1.4.0.tgz#bee504c182bbb768b5fb68809e82b69deef9aec0" + integrity sha512-C8MN/2wIaW9hSrCCtJmcgCkWTQNIspN7ARXLFA4F8PGr8Qxk39U5pS3kRK51/bUJNhb/fEtdFnaViLlISGKI2A== + dependencies: + "@rc-component/async-validator" "^5.0.3" + "@rc-component/util" "^1.3.0" + clsx "^2.1.1" + +"@rc-component/image@~1.5.1": + version "1.5.2" + resolved "https://registry.npmmirror.com/@rc-component/image/-/image-1.5.2.tgz#46cd467466f8b5c9a682bbc96a04f15ad3688af6" + integrity sha512-SIbYLy0IrXqyhccpKktQEvpbBti/KwgG8V/E8GJa8ycwOQmuZaCP7b/C+eQlivn4KDWpfKfoOrLKHXmVlljDgg== + dependencies: + "@rc-component/motion" "^1.0.0" + "@rc-component/portal" "^2.0.0" + "@rc-component/util" "^1.3.0" + clsx "^2.1.1" + +"@rc-component/input-number@~1.6.2": + version "1.6.2" + resolved "https://registry.npmmirror.com/@rc-component/input-number/-/input-number-1.6.2.tgz#ae04e1ee69393fc047588c632e7ce6e19faf617f" + integrity sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w== + dependencies: + "@rc-component/mini-decimal" "^1.0.1" + "@rc-component/util" "^1.4.0" + clsx "^2.1.1" + +"@rc-component/input@~1.1.0": + version "1.1.2" + resolved "https://registry.npmmirror.com/@rc-component/input/-/input-1.1.2.tgz#5fdb55741c012a3f8847d7bd24e318ed1d02cc05" + integrity sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg== + dependencies: + "@rc-component/util" "^1.4.0" + clsx "^2.1.1" + +"@rc-component/mentions@~1.5.5": + version "1.5.5" + resolved "https://registry.npmmirror.com/@rc-component/mentions/-/mentions-1.5.5.tgz#3fbe90d929951dde410fe7f43a697399883dcce4" + integrity sha512-m39JW6ZyR0+foE1ojgOx2+GH8kMaJS279A2cI0vV0gIEZMp+2hOpPhJgKR7vMOGdhvkiXwgfM49EaPw30NonNw== + dependencies: + "@rc-component/input" "~1.1.0" + "@rc-component/menu" "~1.1.0" + "@rc-component/textarea" "~1.1.0" + "@rc-component/trigger" "^3.0.0" + "@rc-component/util" "^1.3.0" + clsx "^2.1.1" + +"@rc-component/menu@~1.1.0", "@rc-component/menu@~1.1.4": + version "1.1.5" + resolved "https://registry.npmmirror.com/@rc-component/menu/-/menu-1.1.5.tgz#cee27d828867ba90c8a5cf17be2feae7dab4abb8" + integrity sha512-+TlOCjrvm0JFk3OtSbOLX4klXK1bBdwTSJeEg1lM8P0BfdJOYxLFmKyAoUILioP4dO2A9u+lZENZOleAmA4g+A== + dependencies: + "@rc-component/motion" "^1.1.4" + "@rc-component/trigger" "^3.0.0" + "@rc-component/util" "^1.3.0" + clsx "^2.1.1" + rc-overflow "^1.3.1" + +"@rc-component/mini-decimal@^1.0.1": + version "1.1.0" + resolved "https://registry.npmmirror.com/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz#7b7a362b14a0a54cb5bc6fd2b82731f29f11d9b0" + integrity sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ== + dependencies: + "@babel/runtime" "^7.18.0" + +"@rc-component/motion@^1.0.0", "@rc-component/motion@^1.1.3", "@rc-component/motion@^1.1.4", "@rc-component/motion@~1.1.4": + version "1.1.5" + resolved "https://registry.npmmirror.com/@rc-component/motion/-/motion-1.1.5.tgz#76e083491371cf2c5e415ae9b0c186f4970d85d9" + integrity sha512-IokOGL+aDqG+GDZjPDN4IFSVrThSbBB2CyHl0F58vOio+5ujLesZ1hL83/hMkazqlGM5zOF7QSbR9cRxnsy8hQ== + dependencies: + "@rc-component/util" "^1.2.0" + clsx "^2.1.1" + +"@rc-component/mutate-observer@^2.0.0": + version "2.0.1" + resolved "https://registry.npmmirror.com/@rc-component/mutate-observer/-/mutate-observer-2.0.1.tgz#78f54a23bff7c62b2137dfb67e063c1be6ac0652" + integrity sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w== + dependencies: + "@rc-component/util" "^1.2.0" + +"@rc-component/notification@~1.2.0": + version "1.2.0" + resolved "https://registry.npmmirror.com/@rc-component/notification/-/notification-1.2.0.tgz#dd7c7d50f1d3217bfbc75bc46259e212096855c5" + integrity sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA== + dependencies: + "@rc-component/motion" "^1.1.4" + "@rc-component/util" "^1.2.1" + clsx "^2.1.1" + +"@rc-component/pagination@~1.2.0": + version "1.2.0" + resolved "https://registry.npmmirror.com/@rc-component/pagination/-/pagination-1.2.0.tgz#3a97abda8f1077f514e03a74b3b9c77f9e68499a" + integrity sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw== + dependencies: + "@rc-component/util" "^1.3.0" + clsx "^2.1.1" + +"@rc-component/picker@~1.6.0": + version "1.6.0" + resolved "https://registry.npmmirror.com/@rc-component/picker/-/picker-1.6.0.tgz#d394a41862c27d7cd887ef85114cf583b341d493" + integrity sha512-5gmNlnsK18Xu8W9xqluz8JzfRBHwPKfdUnkTwMmhGg7P8vjVUveYRHGQbyPZAE2Q11maE42x457l36FlXi4Hyw== + dependencies: + "@rc-component/resize-observer" "^1.0.0" + "@rc-component/trigger" "^3.6.15" + "@rc-component/util" "^1.3.0" + clsx "^2.1.1" + rc-overflow "^1.3.2" + +"@rc-component/portal@^2.0.0": + version "2.0.1" + resolved "https://registry.npmmirror.com/@rc-component/portal/-/portal-2.0.1.tgz#330bc21e6f9c513a8b685615222c22e03ac7af1b" + integrity sha512-46KYuA7Udb1LAaLIdDrfmDz3wzyeEZxIURJCn+heoQVbhtW5PQkhBSQtRus+DUdsknmTFQulxSnqrbX3CI4yXw== + dependencies: + "@rc-component/util" "^1.2.1" + clsx "^2.1.1" + +"@rc-component/progress@~1.0.1": + version "1.0.2" + resolved "https://registry.npmmirror.com/@rc-component/progress/-/progress-1.0.2.tgz#9aba5e24d3ca73a61a451fd041f5d03ca8907c62" + integrity sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ== + dependencies: + "@rc-component/util" "^1.2.1" + clsx "^2.1.1" + +"@rc-component/qrcode@~1.1.0": + version "1.1.1" + resolved "https://registry.npmmirror.com/@rc-component/qrcode/-/qrcode-1.1.1.tgz#909f181bb9a7469d32a6e96c7f35476d4bd92008" + integrity sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA== + dependencies: + "@babel/runtime" "^7.24.7" + +"@rc-component/rate@~1.0.0": + version "1.0.1" + resolved "https://registry.npmmirror.com/@rc-component/rate/-/rate-1.0.1.tgz#836c3c0bea69047f4234383e2ce6ab83a02ee26a" + integrity sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw== + dependencies: + "@rc-component/util" "^1.3.0" + clsx "^2.1.1" + +"@rc-component/resize-observer@^1.0.0": + version "1.0.1" + resolved "https://registry.npmmirror.com/@rc-component/resize-observer/-/resize-observer-1.0.1.tgz#bd07c2ab29baa019bd83a0870c07f6902d2241a3" + integrity sha512-r+w+Mz1EiueGk1IgjB3ptNXLYSLZ5vnEfKHH+gfgj7JMupftyzvUUl3fRcMZe5uMM04x0n8+G2o/c6nlO2+Wag== + dependencies: + "@rc-component/util" "^1.2.0" + +"@rc-component/segmented@~1.2.2": + version "1.2.3" + resolved "https://registry.npmmirror.com/@rc-component/segmented/-/segmented-1.2.3.tgz#3a1b8d5daa2ecba6876062abb01424bbd512ef79" + integrity sha512-L7G4S6zUpqHclOXK0wKKN2/VyqHa9tfDNxkoFjWOTPtQ0ROFaBwZhbf1+9sdZfIFkxJkpcShAmDOMEIBaFFqkw== + dependencies: + "@babel/runtime" "^7.11.1" + "@rc-component/motion" "^1.1.4" + "@rc-component/util" "^1.3.0" + clsx "^2.1.1" + +"@rc-component/select@~1.2.0", "@rc-component/select@~1.2.1": + version "1.2.3" + resolved "https://registry.npmmirror.com/@rc-component/select/-/select-1.2.3.tgz#7fc552962bea074552bb0a8742c92ab34e872c75" + integrity sha512-Hr5E5CyCfnhOlzm6QSxiltjZW4QYcAC4lbTJLthTM7TRVJ6Z7Gi3V6Pu4PrPyZn/r3FOnFh1OLI8ZhrK6r4Bkg== + dependencies: + "@rc-component/trigger" "^3.0.0" + "@rc-component/util" "^1.3.0" + clsx "^2.1.1" + rc-overflow "^1.5.0" + rc-virtual-list "^3.5.2" + +"@rc-component/slider@~1.0.0": + version "1.0.1" + resolved "https://registry.npmmirror.com/@rc-component/slider/-/slider-1.0.1.tgz#a869eb09be343cfc580b28608edb0b230ceb1f04" + integrity sha512-uDhEPU1z3WDfCJhaL9jfd2ha/Eqpdfxsn0Zb0Xcq1NGQAman0TWaR37OWp2vVXEOdV2y0njSILTMpTfPV1454g== + dependencies: + "@rc-component/util" "^1.3.0" + clsx "^2.1.1" + +"@rc-component/steps@~1.2.1": + version "1.2.2" + resolved "https://registry.npmmirror.com/@rc-component/steps/-/steps-1.2.2.tgz#8440329540e987ccaed252e008972d0b63723d6f" + integrity sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw== + dependencies: + "@rc-component/util" "^1.2.1" + clsx "^2.1.1" + +"@rc-component/switch@~1.0.2": + version "1.0.3" + resolved "https://registry.npmmirror.com/@rc-component/switch/-/switch-1.0.3.tgz#d6efa8a17ca9c35f0838321c1cfe0b9adb954523" + integrity sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw== + dependencies: + "@rc-component/util" "^1.3.0" + clsx "^2.1.1" + +"@rc-component/table@~1.8.1": + version "1.8.2" + resolved "https://registry.npmmirror.com/@rc-component/table/-/table-1.8.2.tgz#021755c329bae6988141f9be46646a7cdc784e9e" + integrity sha512-GUuuXIGx2M3KVEcqhze8cDs0cwkSby9VRnOrm6zbnryMFUr+WUL1eu7NA1j4Gi43Rd3/CIL8OmXhRdUz1L/Xug== + dependencies: + "@rc-component/context" "^2.0.1" + "@rc-component/resize-observer" "^1.0.0" + "@rc-component/util" "^1.1.0" + clsx "^2.1.1" + rc-virtual-list "^3.14.2" + +"@rc-component/tabs@~1.6.0": + version "1.6.0" + resolved "https://registry.npmmirror.com/@rc-component/tabs/-/tabs-1.6.0.tgz#8beb3dc4bed77e6eed592a36df70ff39a6f07269" + integrity sha512-2OY02yhS7E0y0Yr5LBI3o5KdM7h4yJ5lBR6V4PEC1dx/sUZggEw7vAHGCArqCcpsZ6pzjOGJbGiVhz7dSMiehA== + dependencies: + "@rc-component/dropdown" "~1.0.0" + "@rc-component/menu" "~1.1.0" + "@rc-component/motion" "^1.1.3" + "@rc-component/resize-observer" "^1.0.0" + "@rc-component/util" "^1.3.0" + clsx "^2.1.1" + +"@rc-component/textarea@~1.1.0", "@rc-component/textarea@~1.1.2": + version "1.1.2" + resolved "https://registry.npmmirror.com/@rc-component/textarea/-/textarea-1.1.2.tgz#2daa5dcb997840040fb8892b0d601ef28d9d1f37" + integrity sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A== + dependencies: + "@rc-component/input" "~1.1.0" + "@rc-component/resize-observer" "^1.0.0" + "@rc-component/util" "^1.3.0" + clsx "^2.1.1" + +"@rc-component/tooltip@~1.3.3": + version "1.3.4" + resolved "https://registry.npmmirror.com/@rc-component/tooltip/-/tooltip-1.3.4.tgz#2e7a0d841bdee81695a66be3b9d2286545e0ceb5" + integrity sha512-wbxvH/UBVgGnpivBPDiGirNr2B9BhUBF4QJTWHK8hOMh6qWg/yf0g4UspH9+GlnSwSLoYOhcChmdLLFxSULBDQ== + dependencies: + "@rc-component/trigger" "^3.6.15" + "@rc-component/util" "^1.3.0" + clsx "^2.1.1" + +"@rc-component/tour@~2.2.0": + version "2.2.1" + resolved "https://registry.npmmirror.com/@rc-component/tour/-/tour-2.2.1.tgz#608c5270443e329d13251072845e75b5e64b3067" + integrity sha512-BUCrVikGJsXli38qlJ+h2WyDD6dYxzDA9dV3o0ij6gYhAq6ooT08SUMWOikva9v4KZ2BEuluGl5bPcsjrSoBgQ== + dependencies: + "@rc-component/portal" "^2.0.0" + "@rc-component/trigger" "^3.0.0" + "@rc-component/util" "^1.3.0" + clsx "^2.1.1" + +"@rc-component/tree-select@~1.3.0": + version "1.3.1" + resolved "https://registry.npmmirror.com/@rc-component/tree-select/-/tree-select-1.3.1.tgz#d8a21f56f7f5287e7021565be72020bf81746839" + integrity sha512-aWbsJ0c7Saqu4Fpn0RPx0EeprttyBbAeH1HQ8cG8vPHOrkG+kg4Wg3TWB+e5uVo36dneH8NJHfOICLzdblQEhA== + dependencies: + "@rc-component/select" "~1.2.0" + "@rc-component/tree" "~1.0.1" + "@rc-component/util" "^1.3.0" + clsx "^2.1.1" + +"@rc-component/tree@~1.0.0", "@rc-component/tree@~1.0.1": + version "1.0.2" + resolved "https://registry.npmmirror.com/@rc-component/tree/-/tree-1.0.2.tgz#f2c163831a49e3363eb0667b2bd96a70db804d36" + integrity sha512-h4P2P3N004VmUonkBzhisjrwME2njSxmUzZPhkFHGllAzbcRUSUGSx0dvRr0FCxYYittpMpGUHE6OMvbVX2c8Q== + dependencies: + "@rc-component/motion" "^1.0.0" + "@rc-component/util" "^1.2.1" + clsx "^2.1.1" + rc-virtual-list "^3.5.1" + +"@rc-component/trigger@^3.0.0", "@rc-component/trigger@^3.6.15": + version "3.7.1" + resolved "https://registry.npmmirror.com/@rc-component/trigger/-/trigger-3.7.1.tgz#3b84eb77a6ea99f240b5fa4c06a2dea34b65d3d5" + integrity sha512-+YNP8FywxKJpdqzlAp6TN8UbSK6YsQtIs3kI13mHfm87qi3qUd5Q9AGW8Unfv76kXFUSu7U7D0FygRsGH+6MiA== + dependencies: + "@rc-component/motion" "^1.1.4" + "@rc-component/portal" "^2.0.0" + "@rc-component/resize-observer" "^1.0.0" + "@rc-component/util" "^1.2.1" + clsx "^2.1.1" + +"@rc-component/upload@~1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@rc-component/upload/-/upload-1.1.0.tgz#cb634587ffdf8a8a4a26a279fac06989fb47f593" + integrity sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw== + dependencies: + "@rc-component/util" "^1.3.0" + clsx "^2.1.1" + +"@rc-component/util@^1.0.1", "@rc-component/util@^1.1.0", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0": + version "1.4.0" + resolved "https://registry.npmmirror.com/@rc-component/util/-/util-1.4.0.tgz#7509c47b2f17e370be65c05e0e8c1aa743d674db" + integrity sha512-LQlShcJKu0p3JUTAenKrWtqVW0+c4PJKedOqEaef9gTVL70O3cG4xZJ7VXfm0blGzORKFEkd3oQGalaUBNZ3Lg== + dependencies: + is-mobile "^5.0.0" + react-is "^18.2.0" + "@react-three/drei@^10.7.7": version "10.7.7" resolved "https://registry.npmmirror.com/@react-three/drei/-/drei-10.7.7.tgz#7ac029ace001307dfc71c61b6284b1c12efe8b80" @@ -886,6 +1327,59 @@ ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +antd@^6.0.0: + version "6.0.0" + resolved "https://registry.npmmirror.com/antd/-/antd-6.0.0.tgz#d194fb05a4c7f56767380ba1d50d9e55be0af6ce" + integrity sha512-OoalcsmgsLFI8UWLkfDJftABP2KmNDiU9REaTApb0s7cd3vZfIok7OnHKuNGQ3tCNY1NKPDvoRtWKXlpaq7zWQ== + dependencies: + "@ant-design/colors" "^8.0.0" + "@ant-design/cssinjs" "^2.0.0" + "@ant-design/cssinjs-utils" "^2.0.0" + "@ant-design/fast-color" "^3.0.0" + "@ant-design/icons" "^6.1.0" + "@ant-design/react-slick" "~1.1.2" + "@rc-component/cascader" "~1.7.0" + "@rc-component/checkbox" "~1.0.0" + "@rc-component/collapse" "~1.1.1" + "@rc-component/color-picker" "~3.0.2" + "@rc-component/dialog" "~1.5.0" + "@rc-component/drawer" "~1.2.0" + "@rc-component/dropdown" "~1.0.0" + "@rc-component/form" "~1.4.0" + "@rc-component/image" "~1.5.1" + "@rc-component/input" "~1.1.0" + "@rc-component/input-number" "~1.6.2" + "@rc-component/mentions" "~1.5.5" + "@rc-component/menu" "~1.1.4" + "@rc-component/motion" "~1.1.4" + "@rc-component/mutate-observer" "^2.0.0" + "@rc-component/notification" "~1.2.0" + "@rc-component/pagination" "~1.2.0" + "@rc-component/picker" "~1.6.0" + "@rc-component/progress" "~1.0.1" + "@rc-component/qrcode" "~1.1.0" + "@rc-component/rate" "~1.0.0" + "@rc-component/resize-observer" "^1.0.0" + "@rc-component/segmented" "~1.2.2" + "@rc-component/select" "~1.2.1" + "@rc-component/slider" "~1.0.0" + "@rc-component/steps" "~1.2.1" + "@rc-component/switch" "~1.0.2" + "@rc-component/table" "~1.8.1" + "@rc-component/tabs" "~1.6.0" + "@rc-component/textarea" "~1.1.2" + "@rc-component/tooltip" "~1.3.3" + "@rc-component/tour" "~2.2.0" + "@rc-component/tree" "~1.0.1" + "@rc-component/tree-select" "~1.3.0" + "@rc-component/trigger" "^3.6.15" + "@rc-component/upload" "~1.1.0" + "@rc-component/util" "^1.4.0" + clsx "^2.1.1" + dayjs "^1.11.11" + scroll-into-view-if-needed "^3.1.0" + throttle-debounce "^5.0.2" + any-promise@^1.0.0: version "1.3.0" resolved "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -1054,6 +1548,16 @@ chokidar@^3.6.0: optionalDependencies: fsevents "~2.3.2" +classnames@^2.2.1, classnames@^2.2.5, classnames@^2.2.6: + version "2.5.1" + resolved "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + +clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -1078,6 +1582,11 @@ commander@^4.0.0: resolved "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +compute-scroll-into-view@^3.0.2: + version "3.1.1" + resolved "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz#02c3386ec531fb6a9881967388e53e8564f3e9aa" + integrity sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -1088,6 +1597,11 @@ convert-source-map@^2.0.0: resolved "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +cookie@^1.0.1: + version "1.1.1" + resolved "https://registry.npmmirror.com/cookie/-/cookie-1.1.1.tgz#3bb9bdfc82369db9c2f69c93c9c3ceb310c88b3c" + integrity sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ== + cross-env@^7.0.3: version "7.0.3" resolved "https://registry.npmmirror.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" @@ -1109,11 +1623,16 @@ cssesc@^3.0.0: resolved "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -csstype@^3.2.2: +csstype@^3.1.3, csstype@^3.2.2: version "3.2.3" resolved "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== +dayjs@^1.11.11: + version "1.11.19" + resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz#15dc98e854bb43917f12021806af897c58ae2938" + integrity sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw== + debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.4.3" resolved "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" @@ -1625,6 +2144,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-mobile@^5.0.0: + version "5.0.0" + resolved "https://registry.npmmirror.com/is-mobile/-/is-mobile-5.0.0.tgz#1e08a0ef2c38a67bff84a52af68d67bcef445333" + integrity sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ== + is-number@^7.0.0: version "7.0.0" resolved "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -1684,6 +2208,13 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json2mq@^0.2.0: + version "0.2.0" + resolved "https://registry.npmmirror.com/json2mq/-/json2mq-0.2.0.tgz#b637bd3ba9eabe122c83e9720483aeb10d2c904a" + integrity sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA== + dependencies: + string-convert "^0.2.0" + json5@^2.2.3: version "2.2.3" resolved "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" @@ -2006,6 +2537,44 @@ queue-microtask@^1.2.2: resolved "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +rc-overflow@^1.3.1, rc-overflow@^1.3.2, rc-overflow@^1.5.0: + version "1.5.0" + resolved "https://registry.npmmirror.com/rc-overflow/-/rc-overflow-1.5.0.tgz#02e58a15199e392adfcc87e0d6e9e7c8e57f2771" + integrity sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg== + dependencies: + "@babel/runtime" "^7.11.1" + classnames "^2.2.1" + rc-resize-observer "^1.0.0" + rc-util "^5.37.0" + +rc-resize-observer@^1.0.0: + version "1.4.3" + resolved "https://registry.npmmirror.com/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz#4fd41fa561ba51362b5155a07c35d7c89a1ea569" + integrity sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ== + dependencies: + "@babel/runtime" "^7.20.7" + classnames "^2.2.1" + rc-util "^5.44.1" + resize-observer-polyfill "^1.5.1" + +rc-util@^5.36.0, rc-util@^5.37.0, rc-util@^5.44.1: + version "5.44.4" + resolved "https://registry.npmmirror.com/rc-util/-/rc-util-5.44.4.tgz#89ee9037683cca01cd60f1a6bbda761457dd6ba5" + integrity sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w== + dependencies: + "@babel/runtime" "^7.18.3" + react-is "^18.2.0" + +rc-virtual-list@^3.14.2, rc-virtual-list@^3.5.1, rc-virtual-list@^3.5.2: + version "3.19.2" + resolved "https://registry.npmmirror.com/rc-virtual-list/-/rc-virtual-list-3.19.2.tgz#1dd2d782c9a3ccbe537bb873447d73f83af8de0f" + integrity sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA== + dependencies: + "@babel/runtime" "^7.20.0" + classnames "^2.2.6" + rc-resize-observer "^1.0.0" + rc-util "^5.36.0" + react-dom@^19.2.0: version "19.2.0" resolved "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.0.tgz#00ed1e959c365e9a9d48f8918377465466ec3af8" @@ -2013,6 +2582,11 @@ react-dom@^19.2.0: dependencies: scheduler "^0.27.0" +react-is@^18.2.0: + version "18.3.1" + resolved "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + react-reconciler@^0.31.0: version "0.31.0" resolved "https://registry.npmmirror.com/react-reconciler/-/react-reconciler-0.31.0.tgz#6b7390fe8fab59210daf523d7400943973de1458" @@ -2025,6 +2599,21 @@ react-refresh@^0.18.0: resolved "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.18.0.tgz#2dce97f4fe932a4d8142fa1630e475c1729c8062" integrity sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw== +react-router-dom@^7.9.6: + version "7.9.6" + resolved "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-7.9.6.tgz#f2a0d12961d67bd87ab48e5ef42fa1f45beae357" + integrity sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA== + dependencies: + react-router "7.9.6" + +react-router@7.9.6: + version "7.9.6" + resolved "https://registry.npmmirror.com/react-router/-/react-router-7.9.6.tgz#003c8de335fdd7390286a478dcfd9579c1826137" + integrity sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA== + dependencies: + cookie "^1.0.1" + set-cookie-parser "^2.6.0" + react-use-measure@^2.1.7: version "2.1.7" resolved "https://registry.npmmirror.com/react-use-measure/-/react-use-measure-2.1.7.tgz#36b8a2e7fd2fa58109ab851b3addcb0aad66ad1d" @@ -2054,6 +2643,11 @@ require-from-string@^2.0.2: resolved "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== +resize-observer-polyfill@^1.5.1: + version "1.5.1" + resolved "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -2121,6 +2715,13 @@ scheduler@^0.27.0: resolved "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd" integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q== +scroll-into-view-if-needed@^3.1.0: + version "3.1.0" + resolved "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz#fa9524518c799b45a2ef6bbffb92bcad0296d01f" + integrity sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ== + dependencies: + compute-scroll-into-view "^3.0.2" + semver@^6.3.1: version "6.3.1" resolved "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" @@ -2131,6 +2732,11 @@ semver@^7.6.0: resolved "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== +set-cookie-parser@^2.6.0: + version "2.7.2" + resolved "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz#ccd08673a9ae5d2e44ea2a2de25089e67c7edf68" + integrity sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw== + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -2161,11 +2767,21 @@ stats.js@^0.17.0: resolved "https://registry.npmmirror.com/stats.js/-/stats.js-0.17.0.tgz#b1c3dc46d94498b578b7fd3985b81ace7131cc7d" integrity sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw== +string-convert@^0.2.0: + version "0.2.1" + resolved "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97" + integrity sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A== + strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +stylis@^4.3.4: + version "4.3.6" + resolved "https://registry.npmmirror.com/stylis/-/stylis-4.3.6.tgz#7c7b97191cb4f195f03ecab7d52f7902ed378320" + integrity sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ== + sucrase@^3.35.0: version "3.35.1" resolved "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.1.tgz#4619ea50393fe8bd0ae5071c26abd9b2e346bfe1" @@ -2265,6 +2881,11 @@ three@^0.181.2: resolved "https://registry.npmmirror.com/three/-/three-0.181.2.tgz#d54a8c8b4a409e346cbc60fed58244f1b382d6ea" integrity sha512-k/CjiZ80bYss6Qs7/ex1TBlPD11whT9oKfT8oTGiHa34W4JRd1NiH/Tr1DbHWQ2/vMUypxksLnF2CfmlmM5XFQ== +throttle-debounce@^5.0.0, throttle-debounce@^5.0.2: + version "5.0.2" + resolved "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz#ec5549d84e053f043c9fd0f2a6dd892ff84456b1" + integrity sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A== + tinyglobby@^0.2.11, tinyglobby@^0.2.15: version "0.2.15" resolved "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"