前端功能基本完成

main
mula.liu 2025-11-29 23:10:00 +08:00
parent 514f6e51ab
commit 73246d116d
55 changed files with 2654 additions and 695 deletions

View File

@ -1,10 +1,10 @@
<!doctype html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
<title>Cosmo - 深空探测器可视化</title>
</head>
<body>
<div id="root"></div>

View File

@ -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": {

View File

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

View File

@ -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"
}
]

View File

@ -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"
}
]

View File

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

View File

@ -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."
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,110 @@
<!DOCTYPE html>
<html>
<head>
<title>Test Voyager Models</title>
<style>
body { margin: 0; }
canvas { display: block; }
#info {
position: absolute;
top: 10px;
left: 10px;
color: white;
background: rgba(0,0,0,0.7);
padding: 10px;
font-family: monospace;
}
</style>
</head>
<body>
<div id="info">Loading...</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(5, 5, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(5, 5, 5);
scene.add(directionalLight);
const loader = new GLTFLoader();
const info = document.getElementById('info');
// Test Voyager 1
info.textContent = 'Loading Voyager 1...';
loader.load(
'http://localhost:8000/upload/model/voyager_1.glb',
(gltf) => {
gltf.scene.position.set(-3, 0, 0);
gltf.scene.scale.set(0.5, 0.5, 0.5);
scene.add(gltf.scene);
info.textContent += '\nVoyager 1: ✓ Loaded';
console.log('Voyager 1 loaded:', gltf);
},
(progress) => {
console.log('Voyager 1 progress:', (progress.loaded / progress.total * 100) + '%');
},
(error) => {
info.textContent += '\nVoyager 1: ✗ Error - ' + error.message;
console.error('Voyager 1 error:', error);
}
);
// Test Voyager 2
info.textContent += '\nLoading Voyager 2...';
loader.load(
'http://localhost:8000/upload/model/voyager_2.glb',
(gltf) => {
gltf.scene.position.set(3, 0, 0);
gltf.scene.scale.set(0.5, 0.5, 0.5);
scene.add(gltf.scene);
info.textContent += '\nVoyager 2: ✓ Loaded';
console.log('Voyager 2 loaded:', gltf);
},
(progress) => {
console.log('Voyager 2 progress:', (progress.loaded / progress.total * 100) + '%');
},
(error) => {
info.textContent += '\nVoyager 2: ✗ Error - ' + error.message;
console.error('Voyager 2 error:', error);
}
);
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 733 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 852 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 803 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 864 KiB

View File

@ -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<Date | null>(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 (
<div className="w-full h-full relative">
{/* Title overlay */}
<div className="absolute top-4 left-4 z-50 text-white">
<h1 className="text-3xl font-bold mb-1">Cosmo</h1>
<p className="text-sm text-gray-300"></p>
<p className="text-xs text-gray-400 mt-1">
{selectedBody ? `聚焦: ${selectedBody.name}` : `${bodies.length} 个天体`}
</p>
<button
onClick={toggleTimelineMode}
className={`mt-2 px-4 py-2 rounded text-sm font-medium transition-colors ${
isTimelineMode
? 'bg-blue-600 hover:bg-blue-700 text-white'
: 'bg-gray-700 hover:bg-gray-600 text-gray-300'
}`}
>
{isTimelineMode ? '🕐 时间轴模式 (点击退出)' : '📅 切换到时间轴模式'}
</button>
</div>
{/* Header with navigation and controls */}
<Header
isTimelineMode={isTimelineMode}
onToggleTimeline={toggleTimelineMode}
bodyCount={bodies.length}
selectedBodyName={selectedBody?.name}
/>
{/* Probe List Sidebar */}
<ProbeList
@ -109,8 +104,8 @@ function App() {
{isTimelineMode && (
<TimelineController
onTimeChange={handleTimeChange}
minDate={new Date(1997, 0, 1)} // Cassini launch date
maxDate={new Date()}
maxDate={new Date()} // Start point (now)
minDate={new Date(Date.now() - TIMELINE_DAYS * 24 * 60 * 60 * 1000)} // End point (past)
/>
)}

View File

@ -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 <Navigate to="/login" replace />;
}
return <>{children}</>;
}
export function Router() {
return (
<BrowserRouter>
<Routes>
{/* Public routes */}
<Route path="/login" element={<Login />} />
{/* Main app (3D visualization) */}
<Route path="/" element={<App />} />
{/* Admin routes (protected) */}
<Route
path="/admin"
element={
<ProtectedRoute>
<AdminLayout />
</ProtectedRoute>
}
>
<Route index element={<Navigate to="/admin/dashboard" replace />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="celestial-bodies" element={<CelestialBodies />} />
<Route path="static-data" element={<div><h1></h1><p>...</p></div>} />
<Route path="nasa-data" element={<div><h1>NASA</h1><p>...</p></div>} />
</Route>
{/* Fallback */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</BrowserRouter>
);
}

View File

@ -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);
}

View File

@ -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<Mesh>(null);
const position = body.positions[0];
const [texturePath, setTexturePath] = useState<string | null | undefined>(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<string, string> = {
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 <PlanetMesh
body={body}
size={size}
emissive={emissive}
emissiveIntensity={emissiveIntensity}
scaledPos={scaledPos}
texturePath={texturePath}
position={position}
meshRef={meshRef}
hasOffset={renderPosition.hasOffset}
allBodies={allBodies}
/>;
}
// 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<Mesh>;
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 (
<group position={[scaledPos.x, scaledPos.z, scaledPos.y]}>
<mesh ref={meshRef} renderOrder={0}>
@ -185,6 +221,14 @@ function Planet({ body, size, emissive, emissiveIntensity }: {
}}
>
{body.name_zh || body.name}
{offsetDesc && (
<>
<br />
<span style={{ fontSize: '9px', color: '#ffaa00', opacity: 0.9 }}>
{offsetDesc}
</span>
</>
)}
<br />
<span style={{ fontSize: '8px', opacity: 0.7 }}>
{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<string, number> = {
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<string, number> = {
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}
/>
);
}

View File

@ -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<Constellation[]>([]);
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));
}, []);

View File

@ -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<OrbitData[]>([]);
const [loading, setLoading] = useState(true);
// Helper function to get default colors
const getDefaultColor = (name: string): string => {
const colorMap: Record<string, string> = {
'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 (
<group>
{orbits.map((orbit) => (
<group key={orbit.bodyId}>
{/* Orbital path */}
<Line
points={orbit.points}
color={orbit.color}
lineWidth={1.5}
opacity={0.4}
transparent
/>
</group>
))}
</group>
);
}

View File

@ -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<Galaxy[]>([]);
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));
}, []);

View File

@ -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 (
<>
<header className="absolute top-0 left-0 right-0 z-50 bg-gradient-to-b from-black via-black/80 to-transparent backdrop-blur-sm">
<div className="flex items-center justify-between px-6 py-4">
{/* Left: Logo and Title */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center">
<span className="text-2xl">🌌</span>
</div>
<div>
<h1 className="text-2xl font-bold text-white tracking-tight">Cosmo</h1>
<p className="text-xs text-gray-400"></p>
</div>
</div>
{/* Status Info */}
<div className="ml-6 px-3 py-1.5 bg-white/5 rounded-lg border border-white/10">
<p className="text-xs text-gray-300">
{selectedBodyName ? (
<>
<span className="text-cyan-400"></span> : <span className="text-white font-medium">{selectedBodyName}</span>
</>
) : (
<>
<span className="text-green-400"></span> {bodyCount}
</>
)}
</p>
</div>
</div>
{/* Right: Controls */}
<div className="flex items-center gap-3">
{/* Timeline Mode Toggle */}
<button
onClick={onToggleTimeline}
className={`
group relative px-4 py-2 rounded-lg font-medium text-sm transition-all duration-200
${
isTimelineMode
? 'bg-blue-600 text-white border-2 border-blue-400 shadow-lg shadow-blue-500/50'
: 'bg-white/5 text-gray-300 border border-white/10 hover:bg-white/10 hover:border-white/20'
}
`}
>
<div className="flex items-center gap-2">
<span className="text-lg">📅</span>
<span>{isTimelineMode ? '时间轴已启用' : '启用时间轴'}</span>
</div>
{/* Tooltip */}
{!isTimelineMode && (
<div className="absolute top-full right-0 mt-2 px-3 py-2 bg-gray-900 text-white text-xs rounded-lg shadow-xl opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap">
</div>
)}
</button>
{/* Login Button */}
<button
onClick={() => setShowLoginModal(true)}
className="px-4 py-2 rounded-lg font-medium text-sm bg-white/5 text-gray-300 hover:bg-white/10 border border-white/10 transition-all duration-200"
>
<div className="flex items-center gap-2">
<span className="text-lg">👤</span>
<span></span>
</div>
</button>
</div>
</div>
</header>
{/* Login Modal */}
{showLoginModal && (
<div
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/70 backdrop-blur-sm"
onClick={() => setShowLoginModal(false)}
>
<div
className="bg-gray-900 rounded-2xl p-8 max-w-md w-full mx-4 shadow-2xl border border-gray-700"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-white"> Cosmo</h2>
<button
onClick={() => setShowLoginModal(false)}
className="text-gray-400 hover:text-white transition-colors"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
</label>
<input
type="text"
placeholder="请输入邮箱或用户名"
className="w-full px-4 py-3 bg-gray-800 border border-gray-600 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-blue-500 transition-colors"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
</label>
<input
type="password"
placeholder="请输入密码"
className="w-full px-4 py-3 bg-gray-800 border border-gray-600 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-blue-500 transition-colors"
/>
</div>
<div className="flex items-center justify-between text-sm">
<label className="flex items-center text-gray-300 cursor-pointer">
<input type="checkbox" className="mr-2 rounded" />
</label>
<a href="#" className="text-blue-400 hover:text-blue-300">
?
</a>
</div>
<button className="w-full py-3 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-lg font-medium hover:from-blue-700 hover:to-blue-800 transition-all duration-200">
</button>
<div className="text-center text-sm text-gray-400">
{' '}
<a href="#" className="text-blue-400 hover:text-blue-300">
</a>
</div>
</div>
</div>
</div>
)}
</>
);
}

View File

@ -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<Nebula[]>([]);
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 (
<group>
{nebulaData.map((nebula) => (
<group key={nebula.name}>
<Billboard
position={nebula.position}
follow={true}
lockX={false}
lockY={false}
lockZ={false}
>
{/* Nebula texture */}
<mesh>
<planeGeometry args={[nebula.size * 2, nebula.size * 2]} />
<meshBasicMaterial
map={nebula.texture}
transparent
opacity={nebula.type === 'dark' ? 0.5 : 0.7}
blending={nebula.type === 'dark' ? THREE.NormalBlending : THREE.AdditiveBlending}
depthWrite={false}
/>
</mesh>
</Billboard>
{/* Nebula name label - positioned slightly outward */}
<Billboard position={nebula.position.clone().multiplyScalar(1.02)}>
<Text
fontSize={1.2}
color="#FFAADD"
anchorX="center"
anchorY="middle"
outlineWidth={0.1}
outlineColor="#000000"
>
{nebula.name_zh}
</Text>
</Billboard>
</group>
))}
</group>
);
}

View File

@ -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<OrbitData[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 (
<group>
{orbits.map((orbit) => (
<Line
key={orbit.bodyId}
points={orbit.points}
color={orbit.color}
lineWidth={1.5}
opacity={0.4}
transparent
/>
))}
</group>
);
}

View File

@ -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<Record<string, string | null>> => {
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<Group>(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 (
<group position={[scaledPos.x, scaledPos.z, scaledPos.y]} ref={groupRef}>
<primitive
object={configuredScene}
scale={0.2}
scale={optimalScale}
/>
{/* Removed the semi-transparent sphere to avoid rendering conflicts */}
{/* Name label */}
{/* Name label - position based on model scale */}
<Html
position={[0, 1, 0]}
position={[0, optimalScale * 2, 0]}
center
distanceFactor={15}
style={{
@ -117,6 +156,14 @@ function ProbeModel({ body, modelPath }: { body: CelestialBody; modelPath: strin
}}
>
🛰 {body.name_zh || body.name}
{offsetDesc && (
<>
<br />
<span style={{ fontSize: '10px', color: '#ffaa00', opacity: 0.9 }}>
{offsetDesc}
</span>
</>
)}
<br />
<span style={{ fontSize: '10px', opacity: 0.8 }}>
{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 (
<group position={[scaledPos.x, scaledPos.z, scaledPos.y]}>
<mesh>
@ -175,6 +213,14 @@ function ProbeFallback({ body }: { body: CelestialBody }) {
}}
>
🛰 {body.name_zh || body.name}
{offsetDesc && (
<>
<br />
<span style={{ fontSize: '10px', color: '#ffaa00', opacity: 0.9 }}>
{offsetDesc}
</span>
</>
)}
<br />
<span style={{ fontSize: '10px', opacity: 0.8 }}>
{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<Record<string, string | null>>({});
const [isLoading, setIsLoading] = useState(true);
const [modelPath, setModelPath] = useState<string | null | undefined>(undefined);
const [loadError, setLoadError] = useState<boolean>(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 <ProbeModel body={body} modelPath={modelPath} />;
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 <ProbeFallback body={body} />;
}
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 <ProbeModel body={body} modelPath={modelPath} allBodies={allBodies} onError={() => {
console.error(`[Probe ${body.name}] ProbeModel rendering failed, switching to fallback`);
setLoadError(true);
}} />;
}
return <ProbeFallback body={body} allBodies={allBodies} />;
}

View File

@ -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 (
<button
onClick={() => setIsExpanded(true)}
className="absolute top-36 left-4 z-50 bg-black bg-opacity-80 backdrop-blur-sm rounded-lg p-3 hover:bg-opacity-90 transition-all"
className="absolute top-24 left-4 z-50 bg-black bg-opacity-80 backdrop-blur-sm rounded-lg p-3 hover:bg-opacity-90 transition-all"
title="展开天体列表"
>
<div className="flex items-center gap-2 text-white">
<span className="text-lg">🌍</span>
<span className="text-sm font-medium"></span>
<span className="text-gray-400">({totalCount})</span>
</div>
</button>
);
}
return (
<div className="absolute top-36 left-4 z-50 bg-black bg-opacity-80 backdrop-blur-sm rounded-lg p-4 max-w-xs">
<div className="absolute top-24 left-4 z-50 bg-black bg-opacity-80 backdrop-blur-sm rounded-lg p-4 max-w-xs">
<div className="flex items-center justify-between mb-3">
<h2 className="text-white text-lg font-bold flex items-center gap-2">
🌍
@ -89,7 +86,7 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody }: Probe
>
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="text-white font-medium text-sm">{body.name}</div>
<div className="text-white font-medium text-sm">{body.name_zh || body.name}</div>
<div className="text-gray-400 text-xs mt-0.5">
{distance.toFixed(2)} AU
</div>
@ -113,30 +110,41 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody }: Probe
<div className="text-gray-500 text-xs p-2">...</div>
) : (
<div className="space-y-2 max-h-64 overflow-y-auto">
{probesWithDistance.map(({ body, distance }) => (
<button
key={body.id}
onClick={() => onBodySelect(body)}
className={`w-full text-left p-2 rounded transition-all ${
selectedBody?.id === body.id
? 'bg-cyan-500 bg-opacity-30 border-2 border-cyan-400'
: 'bg-gray-800 bg-opacity-50 border border-gray-600 hover:bg-gray-700'
}`}
>
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="text-white font-medium text-sm">{body.name}</div>
<div className="text-gray-400 text-xs mt-0.5">
{distance.toFixed(2)} AU
{distance > 30 && ' (遥远)'}
{probesWithDistance.map(({ body, distance }) => {
const isInactive = body.is_active === false;
const isSelected = selectedBody?.id === body.id;
return (
<button
key={body.id}
onClick={() => !isInactive && onBodySelect(body)}
disabled={isInactive}
className={`w-full text-left p-2 rounded transition-all ${
isInactive
? 'bg-gray-900 bg-opacity-50 border border-gray-700 cursor-not-allowed opacity-50'
: isSelected
? 'bg-cyan-500 bg-opacity-30 border-2 border-cyan-400'
: 'bg-gray-800 bg-opacity-50 border border-gray-600 hover:bg-gray-700'
}`}
>
<div className="flex items-center justify-between">
<div className="flex-1">
<div className={`font-medium text-sm ${isInactive ? 'text-gray-500' : 'text-white'}`}>
{body.name_zh || body.name}
{isInactive && <span className="ml-2 text-xs">()</span>}
</div>
<div className={`text-xs mt-0.5 ${isInactive ? 'text-gray-600' : 'text-gray-400'}`}>
{distance.toFixed(2)} AU
{distance > 30 && ' (遥远)'}
</div>
</div>
{isSelected && !isInactive && (
<div className="text-cyan-400 text-xs"> </div>
)}
</div>
{selectedBody?.id === body.id && (
<div className="text-cyan-400 text-xs"> </div>
)}
</div>
</button>
))}
</button>
);
})}
</div>
)}
</div>

View File

@ -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 */}
<CameraController focusTarget={selectedBody} />
<CameraController focusTarget={selectedBody} allBodies={bodies} />
{/* Increase ambient light to see textures better */}
<ambientLight intensity={0.5} />
@ -83,29 +82,23 @@ export function Scene({ bodies, selectedBody, trajectoryPositions = [] }: SceneP
{/* Major constellations */}
<Constellations />
{/* Nebulae */}
<Nebulae />
{/* Distant galaxies */}
<Galaxies />
{/* Render planets and stars */}
{planets.map((body) => (
<CelestialBody key={body.id} body={body} />
{/* Render all celestial bodies: planets, dwarf planets, satellites, and stars */}
{celestialBodies.map((body) => (
<CelestialBody key={body.id} body={body} allBodies={bodies} />
))}
{/* 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 <Orbit key={`orbit-${body.id}`} distance={distance} color="#ffffff" lineWidth={1} />;
}
return null;
})}
{/* Unified orbit renderer for all celestial bodies (planets and dwarf planets) */}
<OrbitRenderer />
{/* Render visible probes with 3D models */}
{visibleProbes.map((body) => (
<Probe key={body.id} body={body} />
<Probe key={body.id} body={body} allBodies={bodies} />
))}
{/* Render trajectory for selected probe */}

View File

@ -49,10 +49,27 @@ export function Stars() {
const [stars, setStars] = useState<Star[]>([]);
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));
}, []);

View File

@ -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<Date>(startDate); // Start from minDate instead of maxDate
const [currentDate, setCurrentDate] = useState<Date>(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<number>();
const lastUpdateRef = useRef<number>(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<string | null>(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<HTMLInputElement>) => {
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 (
<div className="fixed bottom-20 left-1/2 transform -translate-x-1/2 z-50 bg-black bg-opacity-80 p-4 rounded-lg shadow-lg min-w-96">
<div className="text-white text-center mb-2">
<div className="text-sm font-bold mb-1"></div>
<div className="text-xs text-gray-300">{currentDate.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' })}</div>
{/* Current date display - full format */}
<div className="text-center text-white text-sm mb-2 font-mono">
{formatFullDate(currentDate)}
</div>
{/* Progress bar */}
{/* Date range labels */}
<div className="flex justify-between text-white text-xs mb-1 px-1">
<span className="text-gray-400">{formatShortDate(startDate)}</span>
<span className="text-gray-400">{formatShortDate(endDate)}</span>
</div>
{/* Progress bar (standard slider) */}
<input
type="range"
min="0"
@ -134,7 +167,7 @@ export function TimelineController({ onTimeChange, minDate, maxDate }: TimelineC
{/* Reset button */}
<button
onClick={() => {
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"

View File

@ -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<string | null>(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<string | null>(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 };
}

View File

@ -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);

View File

@ -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(
<StrictMode>
<App />
<Router />
</StrictMode>,
)

View File

@ -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 (
<div style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
}}>
<Card
style={{
width: 400,
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
}}
>
<div style={{ textAlign: 'center', marginBottom: 24 }}>
<h1 style={{ fontSize: 28, marginBottom: 8 }}>Cosmo </h1>
<p style={{ color: '#666' }}></p>
</div>
<Form
name="login"
onFinish={onFinish}
autoComplete="off"
size="large"
>
<Form.Item
name="username"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input
prefix={<UserOutlined />}
placeholder="用户名"
/>
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: '请输入密码' }]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="密码"
/>
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
loading={loading}
block
>
</Button>
</Form.Item>
</Form>
<div style={{ textAlign: 'center', color: '#999', fontSize: 12 }}>
<p>: cosmo / cosmo</p>
</div>
</Card>
</div>
);
}

View File

@ -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<string, any> = {
dashboard: <DashboardOutlined />,
database: <DatabaseOutlined />,
planet: <RocketOutlined />,
data: <DatabaseOutlined />,
download: <DownloadOutlined />,
};
export function AdminLayout() {
const [collapsed, setCollapsed] = useState(false);
const [menus, setMenus] = useState<any[]>([]);
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: <UserOutlined />,
label: '个人信息',
},
{
type: 'divider',
},
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录',
onClick: handleLogout,
},
];
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider trigger={null} collapsible collapsed={collapsed}>
<div
style={{
height: 64,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 20,
fontWeight: 'bold',
color: '#fff',
}}
>
{collapsed ? '🌌' : '🌌 Cosmo'}
</div>
<Menu
theme="dark"
mode="inline"
selectedKeys={[location.pathname]}
items={convertMenus(menus)}
onClick={handleMenuClick}
/>
</Sider>
<Layout>
<Header
style={{
padding: '0 16px',
background: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<div
onClick={() => setCollapsed(!collapsed)}
style={{ fontSize: 18, cursor: 'pointer' }}
>
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</div>
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
<div style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
<Avatar icon={<UserOutlined />} style={{ marginRight: 8 }} />
<span>{user?.username || 'User'}</span>
</div>
</Dropdown>
</Header>
<Content
style={{
margin: '16px',
padding: 24,
background: '#fff',
minHeight: 280,
}}
>
<Outlet />
</Content>
</Layout>
</Layout>
);
}

View File

@ -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<CelestialBody[]>([]);
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<CelestialBody> = [
{
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<string, string> = {
star: '恒星',
planet: '行星',
dwarf_planet: '矮行星',
probe: '探测器',
};
return typeMap[type] || type;
},
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
ellipsis: true,
},
];
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<h1></h1>
<Button type="primary" onClick={loadData}>
</Button>
</div>
<Table
columns={columns}
dataSource={data}
rowKey="id"
loading={loading}
pagination={{ pageSize: 20 }}
/>
</div>
);
}

View File

@ -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 (
<div>
<h1></h1>
<Row gutter={16} style={{ marginTop: 24 }}>
<Col span={8}>
<Card>
<Statistic
title="天体总数"
value={18}
prefix={<GlobalOutlined />}
/>
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic
title="探测器"
value={7}
prefix={<RocketOutlined />}
/>
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic
title="数据记录"
value={1245}
prefix={<DatabaseOutlined />}
/>
</Card>
</Col>
</Row>
</div>
);
}

View File

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

View File

@ -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<string, string> = {};
if (resourceType) params.resource_type = resourceType;
const response = await api.get(`/celestial/resources/${bodyId}`, { params });
return response.data;
}

View File

@ -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();
},
};

View File

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

View File

@ -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');
},
};

View File

@ -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"