main
mula.liu 2025-12-28 10:35:34 +08:00
parent 72d1fd5ef2
commit 246ae12287
11 changed files with 249 additions and 126 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -23,6 +23,7 @@ class CelestialBodyCreate(BaseModel):
id: str id: str
name: str name: str
name_zh: Optional[str] = None name_zh: Optional[str] = None
short_name: Optional[str] = None
type: str type: str
system_id: Optional[int] = None system_id: Optional[int] = None
description: Optional[str] = None description: Optional[str] = None
@ -34,6 +35,7 @@ class CelestialBodyCreate(BaseModel):
class CelestialBodyUpdate(BaseModel): class CelestialBodyUpdate(BaseModel):
name: Optional[str] = None name: Optional[str] = None
name_zh: Optional[str] = None name_zh: Optional[str] = None
short_name: Optional[str] = None
type: Optional[str] = None type: Optional[str] = None
system_id: Optional[int] = None system_id: Optional[int] = None
description: Optional[str] = None description: Optional[str] = None
@ -224,6 +226,7 @@ async def list_bodies(
"id": body.id, "id": body.id,
"name": body.name, "name": body.name,
"name_zh": body.name_zh, "name_zh": body.name_zh,
"short_name": body.short_name,
"type": body.type, "type": body.type,
"system_id": body.system_id, # Add system_id field "system_id": body.system_id, # Add system_id field
"description": body.description, "description": body.description,

View File

@ -60,6 +60,43 @@ async def get_orbits(
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@router.get("/orbits/{body_id}")
async def get_orbit_by_id(
body_id: str,
db: AsyncSession = Depends(get_db)
):
"""
Get orbit data for a specific celestial body
Path parameters:
- body_id: Celestial body ID
Returns:
- Orbit data with points, num_points, period_days, and color
"""
logger.info(f"Fetching orbit for body {body_id}")
try:
orbit = await orbit_service.get_orbit(body_id, db)
if not orbit:
raise HTTPException(status_code=404, detail=f"Orbit not found for body {body_id}")
return {
"body_id": orbit.body_id,
"num_points": orbit.num_points,
"period_days": orbit.period_days,
"color": orbit.color,
"updated_at": orbit.updated_at.isoformat() if orbit.updated_at else None
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to fetch orbit for {body_id}: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/admin/orbits/generate") @router.post("/admin/orbits/generate")
async def generate_orbits( async def generate_orbits(
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,

BIN
data/.DS_Store vendored

Binary file not shown.

View File

@ -0,0 +1,48 @@
# 页面加载速度优化方案
## 当前加载流程分析
1. **数据获取顺序**
- useDataCutoffDate hook 获取截止日期
- 等待截止日期返回
- useSpaceData hook 获取天体位置数据
- 渲染 3D 场景
2. **潜在优化点**
### 优化1并行加载数据推荐
当前是串行加载(先获取日期,再获取位置),可以改为并行或使用默认日期。
### 优化2减少不必要的日志输出
大量 console.log 会影响性能。
### 优化3延迟加载非关键组件
- InterstellarTicker音效
- MessageBoard留言板
- BodyDetailOverlay详情覆盖层
### 优化4优化 API 响应
后端可以:
- 减少返回字段
- 只返回活跃天体
- 启用 gzip 压缩
### 优化5前端缓存
- 使用 React Query 或 SWR 缓存 API 响应
- 使用 localStorage 缓存最近的数据
## 实施建议
### 立即可做(不需要大改):
1. ✅ 删除大量 console.log
2. ✅ 使用 lazy loading 加载非关键组件
3. ✅ 添加骨架屏提升体验
### 需要架构调整:
1. 重构数据获取流程(使用 React Query
2. 实现前端缓存策略
3. 优化后端 API 响应
## 当前最快优化方案
删除冗余日志 + 延迟加载非关键组件:

View File

@ -1,4 +1,4 @@
import { useState, useCallback, useEffect } from 'react'; import { useState, useCallback, useEffect, lazy, Suspense } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useSpaceData } from './hooks/useSpaceData'; import { useSpaceData } from './hooks/useSpaceData';
import { useHistoricalData } from './hooks/useHistoricalData'; import { useHistoricalData } from './hooks/useHistoricalData';
@ -12,15 +12,17 @@ import { GalaxyScene } from './components/GalaxyScene';
import { ProbeList } from './components/ProbeList'; import { ProbeList } from './components/ProbeList';
import { TimelineController } from './components/TimelineController'; import { TimelineController } from './components/TimelineController';
import { Loading } from './components/Loading'; import { Loading } from './components/Loading';
import { InterstellarTicker } from './components/InterstellarTicker';
import { ControlPanel } from './components/ControlPanel'; import { ControlPanel } from './components/ControlPanel';
import { AuthModal } from './components/AuthModal'; import { AuthModal } from './components/AuthModal';
import { MessageBoard } from './components/MessageBoard';
import { BodyDetailOverlay } from './components/BodyDetailOverlay';
import { auth } from './utils/auth'; import { auth } from './utils/auth';
import type { CelestialBody } from './types'; import type { CelestialBody } from './types';
import { useToast } from './contexts/ToastContext'; import { useToast } from './contexts/ToastContext';
// Lazy load non-critical components for better performance
const InterstellarTicker = lazy(() => import('./components/InterstellarTicker').then(m => ({ default: m.InterstellarTicker })));
const MessageBoard = lazy(() => import('./components/MessageBoard').then(m => ({ default: m.MessageBoard })));
const BodyDetailOverlay = lazy(() => import('./components/BodyDetailOverlay').then(m => ({ default: m.BodyDetailOverlay })));
// Timeline configuration - will be fetched from backend later // Timeline configuration - will be fetched from backend later
const TIMELINE_DAYS = 30; // Total days in timeline range const TIMELINE_DAYS = 30; // Total days in timeline range
@ -62,7 +64,6 @@ function App() {
// Use system setting if available, otherwise use localStorage, finally fallback to 'solar' // Use system setting if available, otherwise use localStorage, finally fallback to 'solar'
const initialViewMode = systemViewMode || localViewMode || 'solar'; const initialViewMode = systemViewMode || localViewMode || 'solar';
setViewMode(initialViewMode); setViewMode(initialViewMode);
console.log('[App] Initial viewMode:', initialViewMode, '(system:', systemViewMode, ', local:', localViewMode, ')');
} }
}, [systemViewMode, systemViewModeLoading]); }, [systemViewMode, systemViewModeLoading]);
@ -95,19 +96,6 @@ function App() {
const loading = isTimelineMode ? historicalLoading : realTimeLoading; const loading = isTimelineMode ? historicalLoading : realTimeLoading;
const error = isTimelineMode ? historicalError : realTimeError; const error = isTimelineMode ? historicalError : realTimeError;
// Debug: log bodies when they change
useEffect(() => {
console.log('[App] Bodies updated:', {
isTimelineMode,
totalBodies: bodies.length,
bodiesWithPositions: bodies.filter(b => b.positions && b.positions.length > 0).length,
bodyTypes: bodies.reduce((acc, b) => {
acc[b.type] = (acc[b.type] || 0) + 1;
return acc;
}, {} as Record<string, number>)
});
}, [bodies, isTimelineMode]);
const [selectedBody, setSelectedBody] = useState<CelestialBody | null>(null); const [selectedBody, setSelectedBody] = useState<CelestialBody | null>(null);
const { trajectoryPositions } = useTrajectory(selectedBody); const { trajectoryPositions } = useTrajectory(selectedBody);
@ -226,17 +214,19 @@ function App() {
/> />
{/* Auth Modal */} {/* Auth Modal */}
<AuthModal <AuthModal
isOpen={showAuthModal} isOpen={showAuthModal}
onClose={() => setShowAuthModal(false)} onClose={() => setShowAuthModal(false)}
onLoginSuccess={handleLoginSuccess} onLoginSuccess={handleLoginSuccess}
/> />
{/* Message Board */} {/* Message Board - Lazy Loaded */}
<MessageBoard <Suspense fallback={null}>
open={showMessageBoard} <MessageBoard
onClose={() => setShowMessageBoard(false)} open={showMessageBoard}
/> onClose={() => setShowMessageBoard(false)}
/>
</Suspense>
{/* View Mode Rendering */} {/* View Mode Rendering */}
{viewMode === 'solar' ? ( {viewMode === 'solar' ? (
@ -290,14 +280,18 @@ function App() {
<GalaxyScene /> <GalaxyScene />
)} )}
{/* Interstellar Ticker Sound (Controlled) */} {/* Interstellar Ticker Sound (Controlled) - Lazy Loaded */}
<InterstellarTicker isPlaying={isSoundOn} /> <Suspense fallback={null}>
<InterstellarTicker isPlaying={isSoundOn} />
</Suspense>
{/* Body Detail Overlay */} {/* Body Detail Overlay - Lazy Loaded */}
<BodyDetailOverlay <Suspense fallback={null}>
bodyId={showDetailOverlayId} <BodyDetailOverlay
onClose={() => setShowDetailOverlayId(null)} bodyId={showDetailOverlayId}
/> onClose={() => setShowDetailOverlayId(null)}
/>
</Suspense>
</div> </div>
); );
} }

View File

@ -29,8 +29,6 @@ export function OrbitRenderer({ visible = true }: OrbitRendererProps) {
useEffect(() => { useEffect(() => {
const fetchOrbits = async () => { const fetchOrbits = async () => {
console.log('🌌 Fetching orbital data from backend...');
try { try {
// Fetch precomputed orbits from backend // Fetch precomputed orbits from backend
const response = await request.get('/celestial/orbits'); const response = await request.get('/celestial/orbits');
@ -43,8 +41,6 @@ export function OrbitRenderer({ visible = true }: OrbitRendererProps) {
return; return;
} }
console.log(`📊 Processing ${data.orbits.length} orbits...`);
// Convert to Three.js format // Convert to Three.js format
const orbitData: OrbitData[] = data.orbits.map((orbit: any) => { const orbitData: OrbitData[] = data.orbits.map((orbit: any) => {
// Convert position points to Vector3 with scaling // Convert position points to Vector3 with scaling
@ -66,8 +62,6 @@ export function OrbitRenderer({ visible = true }: OrbitRendererProps) {
} }
} }
console.log(`${orbit.body_name_zh || orbit.body_name}: ${points.length} points`);
return { return {
bodyId: orbit.body_id, bodyId: orbit.body_id,
bodyName: orbit.body_name, bodyName: orbit.body_name,
@ -81,7 +75,6 @@ export function OrbitRenderer({ visible = true }: OrbitRendererProps) {
setOrbits(orbitData); setOrbits(orbitData);
setLoading(false); setLoading(false);
console.log(`🎉 Loaded ${orbitData.length} orbits successfully`);
} catch (err) { } catch (err) {
console.error('❌ Failed to load orbits:', err); console.error('❌ Failed to load orbits:', err);
@ -94,7 +87,6 @@ export function OrbitRenderer({ visible = true }: OrbitRendererProps) {
}, []); }, []);
if (loading) { if (loading) {
console.log('⏳ Loading orbits...');
return null; return null;
} }
@ -104,7 +96,6 @@ export function OrbitRenderer({ visible = true }: OrbitRendererProps) {
} }
if (orbits.length === 0) { if (orbits.length === 0) {
console.warn('⚠️ No orbits to render');
return null; return null;
} }

View File

@ -11,14 +11,18 @@ interface ProbeListProps {
} }
export function ProbeList({ probes, planets, onBodySelect, selectedBody, onResetCamera }: ProbeListProps) { export function ProbeList({ probes, planets, onBodySelect, selectedBody, onResetCamera }: ProbeListProps) {
const [isCollapsed, setIsCollapsed] = useState(false); const [isCollapsed, setIsCollapsed] = useState(true); // 默认关闭左侧面板
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [expandedGroup, setExpandedGroup] = useState<string | null>('planet'); // Default expand planets const [expandedGroup, setExpandedGroup] = useState<string | null>(null); // 默认不展开任何分类
// Auto-collapse when a body is selected (focus mode) // Auto-expand the group when a body is selected from the 3D scene
useEffect(() => { useEffect(() => {
if (selectedBody) { if (selectedBody) {
// Auto-collapse panel for focus mode
setIsCollapsed(true); setIsCollapsed(true);
// Auto-expand the group that contains the selected body
setExpandedGroup(selectedBody.type);
} }
}, [selectedBody]); }, [selectedBody]);
@ -202,20 +206,6 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
/> />
)} )}
{/* Probes Group */}
{groups.probe.length > 0 && (
<BodyGroup
title="探测器"
icon={<Rocket size={14} />}
count={groups.probe.length}
bodies={groups.probe}
isExpanded={expandedGroup === 'probe'}
onToggle={() => toggleGroup('probe')}
selectedBody={selectedBody}
onBodySelect={onBodySelect}
/>
)}
{/* Comets Group */} {/* Comets Group */}
{groups.comet.length > 0 && ( {groups.comet.length > 0 && (
<BodyGroup <BodyGroup
@ -230,6 +220,20 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
/> />
)} )}
{/* Probes Group */}
{groups.probe.length > 0 && (
<BodyGroup
title="探测器"
icon={<Rocket size={14} />}
count={groups.probe.length}
bodies={groups.probe}
isExpanded={expandedGroup === 'probe'}
onToggle={() => toggleGroup('probe')}
selectedBody={selectedBody}
onBodySelect={onBodySelect}
/>
)}
{/* No results message */} {/* No results message */}
{allBodies.length === 0 && ( {allBodies.length === 0 && (
<div className="text-center py-8 text-gray-500 text-xs"> <div className="text-center py-8 text-gray-500 text-xs">

View File

@ -36,8 +36,6 @@ export function useHistoricalData(selectedDate: Date | null) {
setLoading(true); setLoading(true);
setError(null); setError(null);
console.log(`[useHistoricalData] Fetching data for ${dateKey}`);
// Set start and end to the same time to get a single snapshot // Set start and end to the same time to get a single snapshot
const data = await fetchCelestialPositions( const data = await fetchCelestialPositions(
targetDate.toISOString(), targetDate.toISOString(),
@ -49,10 +47,7 @@ export function useHistoricalData(selectedDate: Date | null) {
if (isActive) { if (isActive) {
setBodies(data.bodies); setBodies(data.bodies);
lastFetchedDateRef.current = dateKey; // 记录已请求的时间 lastFetchedDateRef.current = dateKey; // 记录已请求的时间
console.log(`[useHistoricalData] Loaded ${data.bodies.length} bodies`);
setLoading(false); setLoading(false);
} else {
console.log(`[useHistoricalData] Ignored stale data for ${dateKey}`);
} }
} catch (err) { } catch (err) {
if (isActive) { if (isActive) {

View File

@ -30,41 +30,13 @@ export function useSpaceData() {
const targetDate = new Date(cutoffDate!); const targetDate = new Date(cutoffDate!);
targetDate.setUTCHours(0, 0, 0, 0); targetDate.setUTCHours(0, 0, 0, 0);
console.log('[useSpaceData] Loading data for date:', targetDate.toISOString());
const data = await fetchCelestialPositions( const data = await fetchCelestialPositions(
targetDate.toISOString(), targetDate.toISOString(),
targetDate.toISOString(), // Same as start - single point in time targetDate.toISOString(), // Same as start - single point in time
'1d' // Use 1d step for consistency '1d' // Use 1d step for consistency
); );
console.log('[useSpaceData] API response:', {
totalBodies: data.bodies.length,
bodiesWithPositions: data.bodies.filter(b => b.positions && b.positions.length > 0).length,
sample: data.bodies.slice(0, 2).map(b => ({
id: b.id,
name: b.name,
type: b.type,
hasPositions: b.positions && b.positions.length > 0,
positionCount: b.positions?.length || 0,
firstPosition: b.positions?.[0]
}))
});
// Check if positions have the expected structure
const firstBody = data.bodies[0];
if (firstBody && firstBody.positions && firstBody.positions.length > 0) {
console.log('[useSpaceData] First body position structure:', {
body: firstBody.name,
position: firstBody.positions[0],
hasX: 'x' in firstBody.positions[0],
hasY: 'y' in firstBody.positions[0],
hasZ: 'z' in firstBody.positions[0]
});
}
setBodies(data.bodies); setBodies(data.bodies);
console.log('[useSpaceData] State updated with', data.bodies.length, 'bodies');
} catch (err) { } catch (err) {
console.error('Failed to fetch celestial data:', err); console.error('Failed to fetch celestial data:', err);
setError(err instanceof Error ? err.message : 'Unknown error'); setError(err instanceof Error ? err.message : 'Unknown error');

View File

@ -17,6 +17,7 @@ interface CelestialBody {
id: string; id: string;
name: string; name: string;
name_zh: string; name_zh: string;
short_name?: string; // NASA SBDB API short name
type: string; type: string;
system_id?: number; system_id?: number;
description: string; description: string;
@ -36,6 +37,10 @@ interface CelestialBody {
}>; }>;
}; };
has_resources?: boolean; has_resources?: boolean;
orbit_info?: {
num_points: number;
period_days?: number;
};
} }
interface StarSystem { interface StarSystem {
@ -208,7 +213,7 @@ export function CelestialBodies() {
}; };
// Edit handler // Edit handler
const handleEdit = (record: CelestialBody) => { const handleEdit = async (record: CelestialBody) => {
setEditingRecord(record); setEditingRecord(record);
// Parse extra_data if it's a string (from backend JSON field) // Parse extra_data if it's a string (from backend JSON field)
@ -222,10 +227,27 @@ export function CelestialBodies() {
} }
} }
// Properly set form values including nested extra_data // Fetch orbit information if exists
let orbitInfo = null;
try {
const { data: orbitData } = await request.get(`/celestial/orbits/${record.id}`);
if (orbitData) {
orbitInfo = {
num_points: orbitData.num_points,
period_days: orbitData.period_days
};
console.log('Loaded orbit info for', record.id, orbitInfo);
}
} catch (e) {
// Orbit not found or error - this is fine, not all bodies have orbits
console.log('No orbit data for', record.id);
}
// Properly set form values including nested extra_data and orbit info
const formValues = { const formValues = {
...record, ...record,
extra_data: extraData || {}, // Ensure extra_data is an object extra_data: extraData || {}, // Ensure extra_data is an object
orbit_info: orbitInfo
}; };
form.setFieldsValue(formValues); form.setFieldsValue(formValues);
@ -596,6 +618,18 @@ export function CelestialBodies() {
</Col> </Col>
</Row> </Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="short_name"
label="简称"
tooltip="NASA SBDB API使用的短名称如Jupiter的简称是Juptr"
>
<Input placeholder="例如Juptr" />
</Form.Item>
</Col>
</Row>
<Form.Item <Form.Item
name="description" name="description"
label="描述" label="描述"
@ -603,46 +637,79 @@ export function CelestialBodies() {
<Input.TextArea rows={2} /> <Input.TextArea rows={2} />
</Form.Item> </Form.Item>
{/* Orbit parameters for planets and dwarf planets */} {/* Orbit parameters and info for planets and dwarf planets */}
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.type !== currentValues.type}> <Form.Item noStyle shouldUpdate={(prevValues, currentValues) =>
prevValues.type !== currentValues.type || prevValues.orbit_info !== currentValues.orbit_info
}>
{({ getFieldValue }) => { {({ getFieldValue }) => {
const bodyType = getFieldValue('type'); const bodyType = getFieldValue('type');
const orbitInfo = getFieldValue('orbit_info');
if (!['planet', 'dwarf_planet'].includes(bodyType)) { if (!['planet', 'dwarf_planet'].includes(bodyType)) {
return null; return null;
} }
return ( return (
<Alert <Alert
message="轨道参数" message="轨道参数与信息"
description={ description={
<Row gutter={16}> <div>
<Col span={12}> {/* Editable orbit parameters */}
<Form.Item <Row gutter={16}>
name={['extra_data', 'orbit_period_days']} <Col span={12}>
label="轨道周期(天)" <Form.Item
tooltip="完整公转一周所需的天数" name={['extra_data', 'orbit_period_days']}
> label="轨道周期(天)"
<InputNumber tooltip="完整公转一周所需的天数"
style={{ width: '100%' }} >
min={0} <InputNumber
step={1} style={{ width: '100%' }}
placeholder="例如365.25(地球)" min={0}
/> step={1}
</Form.Item> placeholder="例如365.25(地球)"
</Col> />
<Col span={12}> </Form.Item>
<Form.Item </Col>
name={['extra_data', 'orbit_color']} <Col span={12}>
label="轨道颜色" <Form.Item
tooltip="轨道线的显示颜色HEX格式" name={['extra_data', 'orbit_color']}
> label="轨道颜色"
<Input tooltip="轨道线的显示颜色HEX格式"
type="color" >
placeholder="#4A90E2" <Input
/> type="color"
</Form.Item> placeholder="#4A90E2"
</Col> />
</Row> </Form.Item>
</Col>
</Row>
{/* Display orbit info if exists (read-only) */}
{editingRecord && orbitInfo && orbitInfo.num_points && (
<div style={{
marginTop: 16,
paddingTop: 16,
borderTop: '1px solid #d9d9d9',
background: '#f0f9ff',
padding: '12px',
borderRadius: '4px'
}}>
<div style={{ fontWeight: 600, marginBottom: 8, color: '#1890ff' }}>
</div>
<div style={{ display: 'flex', gap: '24px' }}>
<div>
<strong>:</strong> {orbitInfo.num_points?.toLocaleString()}
</div>
{orbitInfo.period_days && (
<div>
<strong>:</strong> {orbitInfo.period_days.toFixed(2)}
</div>
)}
</div>
</div>
)}
</div>
} }
type="info" type="info"
style={{ marginBottom: 16 }} style={{ marginBottom: 16 }}
@ -769,6 +836,18 @@ export function CelestialBodies() {
</Col> </Col>
</Row> </Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="short_name"
label="简称"
tooltip="NASA SBDB API使用的短名称如Jupiter的简称是Juptr"
>
<Input placeholder="例如Juptr" />
</Form.Item>
</Col>
</Row>
<Form.Item <Form.Item
name="description" name="description"
label="描述" label="描述"