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

View File

@ -60,6 +60,43 @@ async def get_orbits(
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")
async def generate_orbits(
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 { useSpaceData } from './hooks/useSpaceData';
import { useHistoricalData } from './hooks/useHistoricalData';
@ -12,15 +12,17 @@ import { GalaxyScene } from './components/GalaxyScene';
import { ProbeList } from './components/ProbeList';
import { TimelineController } from './components/TimelineController';
import { Loading } from './components/Loading';
import { InterstellarTicker } from './components/InterstellarTicker';
import { ControlPanel } from './components/ControlPanel';
import { AuthModal } from './components/AuthModal';
import { MessageBoard } from './components/MessageBoard';
import { BodyDetailOverlay } from './components/BodyDetailOverlay';
import { auth } from './utils/auth';
import type { CelestialBody } from './types';
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
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'
const initialViewMode = systemViewMode || localViewMode || 'solar';
setViewMode(initialViewMode);
console.log('[App] Initial viewMode:', initialViewMode, '(system:', systemViewMode, ', local:', localViewMode, ')');
}
}, [systemViewMode, systemViewModeLoading]);
@ -95,19 +96,6 @@ function App() {
const loading = isTimelineMode ? historicalLoading : realTimeLoading;
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 { trajectoryPositions } = useTrajectory(selectedBody);
@ -226,17 +214,19 @@ function App() {
/>
{/* Auth Modal */}
<AuthModal
isOpen={showAuthModal}
onClose={() => setShowAuthModal(false)}
onLoginSuccess={handleLoginSuccess}
<AuthModal
isOpen={showAuthModal}
onClose={() => setShowAuthModal(false)}
onLoginSuccess={handleLoginSuccess}
/>
{/* Message Board */}
<MessageBoard
open={showMessageBoard}
onClose={() => setShowMessageBoard(false)}
/>
{/* Message Board - Lazy Loaded */}
<Suspense fallback={null}>
<MessageBoard
open={showMessageBoard}
onClose={() => setShowMessageBoard(false)}
/>
</Suspense>
{/* View Mode Rendering */}
{viewMode === 'solar' ? (
@ -290,14 +280,18 @@ function App() {
<GalaxyScene />
)}
{/* Interstellar Ticker Sound (Controlled) */}
<InterstellarTicker isPlaying={isSoundOn} />
{/* Interstellar Ticker Sound (Controlled) - Lazy Loaded */}
<Suspense fallback={null}>
<InterstellarTicker isPlaying={isSoundOn} />
</Suspense>
{/* Body Detail Overlay */}
<BodyDetailOverlay
bodyId={showDetailOverlayId}
onClose={() => setShowDetailOverlayId(null)}
/>
{/* Body Detail Overlay - Lazy Loaded */}
<Suspense fallback={null}>
<BodyDetailOverlay
bodyId={showDetailOverlayId}
onClose={() => setShowDetailOverlayId(null)}
/>
</Suspense>
</div>
);
}

View File

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

View File

@ -11,14 +11,18 @@ interface 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 [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(() => {
if (selectedBody) {
// Auto-collapse panel for focus mode
setIsCollapsed(true);
// Auto-expand the group that contains the selected body
setExpandedGroup(selectedBody.type);
}
}, [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 */}
{groups.comet.length > 0 && (
<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 */}
{allBodies.length === 0 && (
<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);
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(),
@ -49,10 +47,7 @@ export function useHistoricalData(selectedDate: Date | null) {
if (isActive) {
setBodies(data.bodies);
lastFetchedDateRef.current = dateKey; // 记录已请求的时间
console.log(`[useHistoricalData] Loaded ${data.bodies.length} bodies`);
setLoading(false);
} else {
console.log(`[useHistoricalData] Ignored stale data for ${dateKey}`);
}
} catch (err) {
if (isActive) {

View File

@ -30,41 +30,13 @@ export function useSpaceData() {
const targetDate = new Date(cutoffDate!);
targetDate.setUTCHours(0, 0, 0, 0);
console.log('[useSpaceData] Loading data for date:', targetDate.toISOString());
const data = await fetchCelestialPositions(
targetDate.toISOString(),
targetDate.toISOString(), // Same as start - single point in time
'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);
console.log('[useSpaceData] State updated with', data.bodies.length, 'bodies');
} catch (err) {
console.error('Failed to fetch celestial data:', err);
setError(err instanceof Error ? err.message : 'Unknown error');

View File

@ -17,6 +17,7 @@ interface CelestialBody {
id: string;
name: string;
name_zh: string;
short_name?: string; // NASA SBDB API short name
type: string;
system_id?: number;
description: string;
@ -36,6 +37,10 @@ interface CelestialBody {
}>;
};
has_resources?: boolean;
orbit_info?: {
num_points: number;
period_days?: number;
};
}
interface StarSystem {
@ -208,7 +213,7 @@ export function CelestialBodies() {
};
// Edit handler
const handleEdit = (record: CelestialBody) => {
const handleEdit = async (record: CelestialBody) => {
setEditingRecord(record);
// 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 = {
...record,
extra_data: extraData || {}, // Ensure extra_data is an object
orbit_info: orbitInfo
};
form.setFieldsValue(formValues);
@ -596,6 +618,18 @@ export function CelestialBodies() {
</Col>
</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
name="description"
label="描述"
@ -603,46 +637,79 @@ export function CelestialBodies() {
<Input.TextArea rows={2} />
</Form.Item>
{/* Orbit parameters for planets and dwarf planets */}
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.type !== currentValues.type}>
{/* Orbit parameters and info for planets and dwarf planets */}
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) =>
prevValues.type !== currentValues.type || prevValues.orbit_info !== currentValues.orbit_info
}>
{({ getFieldValue }) => {
const bodyType = getFieldValue('type');
const orbitInfo = getFieldValue('orbit_info');
if (!['planet', 'dwarf_planet'].includes(bodyType)) {
return null;
}
return (
<Alert
message="轨道参数"
message="轨道参数与信息"
description={
<Row gutter={16}>
<Col span={12}>
<Form.Item
name={['extra_data', 'orbit_period_days']}
label="轨道周期(天)"
tooltip="完整公转一周所需的天数"
>
<InputNumber
style={{ width: '100%' }}
min={0}
step={1}
placeholder="例如365.25(地球)"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name={['extra_data', 'orbit_color']}
label="轨道颜色"
tooltip="轨道线的显示颜色HEX格式"
>
<Input
type="color"
placeholder="#4A90E2"
/>
</Form.Item>
</Col>
</Row>
<div>
{/* Editable orbit parameters */}
<Row gutter={16}>
<Col span={12}>
<Form.Item
name={['extra_data', 'orbit_period_days']}
label="轨道周期(天)"
tooltip="完整公转一周所需的天数"
>
<InputNumber
style={{ width: '100%' }}
min={0}
step={1}
placeholder="例如365.25(地球)"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name={['extra_data', 'orbit_color']}
label="轨道颜色"
tooltip="轨道线的显示颜色HEX格式"
>
<Input
type="color"
placeholder="#4A90E2"
/>
</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"
style={{ marginBottom: 16 }}
@ -769,6 +836,18 @@ export function CelestialBodies() {
</Col>
</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
name="description"
label="描述"