前端功能基本完成
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 452 KiB |
|
Before Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 487 KiB |
|
Before Width: | Height: | Size: 733 KiB |
|
Before Width: | Height: | Size: 852 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 236 KiB |
|
Before Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 803 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 864 KiB |
|
|
@ -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)
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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));
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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');
|
||||
},
|
||||
};
|
||||
|
|
@ -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"
|
||||
|
|
|
|||