From 3cdaaa2943b72b21396f4bb17519d4840d8e10a1 Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Sat, 6 Dec 2025 17:06:39 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E5=AE=9E=E7=8E=B0=E4=BA=86?= =?UTF-8?q?=E4=B8=89=E9=98=B6=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tsx | 142 +++-- frontend/src/Router.tsx | 2 + frontend/src/components/BodyDetailOverlay.tsx | 97 +++- frontend/src/components/BodyViewer.tsx | 18 +- frontend/src/components/ControlPanel.tsx | 19 +- frontend/src/components/GalaxyScene.tsx | 289 +++++++++ frontend/src/components/Stars.tsx | 225 +++++-- frontend/src/hooks/useSystemSetting.ts | 32 + frontend/src/pages/admin/CelestialBodies.tsx | 263 +++++++-- frontend/src/pages/admin/NASADownload.tsx | 12 +- frontend/src/pages/admin/StarSystems.tsx | 548 ++++++++++++++++++ frontend/src/pages/admin/StaticData.tsx | 2 + frontend/src/types/index.ts | 15 + 13 files changed, 1497 insertions(+), 167 deletions(-) create mode 100644 frontend/src/components/GalaxyScene.tsx create mode 100644 frontend/src/hooks/useSystemSetting.ts create mode 100644 frontend/src/pages/admin/StarSystems.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e3d9788..35dae5a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,8 +5,10 @@ import { useHistoricalData } from './hooks/useHistoricalData'; import { useTrajectory } from './hooks/useTrajectory'; import { useScreenshot } from './hooks/useScreenshot'; import { useDataCutoffDate } from './hooks/useDataCutoffDate'; +import { useSystemSetting } from './hooks/useSystemSetting'; import { Header } from './components/Header'; import { Scene } from './components/Scene'; +import { GalaxyScene } from './components/GalaxyScene'; import { ProbeList } from './components/ProbeList'; import { TimelineController } from './components/TimelineController'; import { Loading } from './components/Loading'; @@ -14,7 +16,7 @@ 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 the new overlay component +import { BodyDetailOverlay } from './components/BodyDetailOverlay'; import { auth } from './utils/auth'; import type { CelestialBody } from './types'; import { useToast } from './contexts/ToastContext'; @@ -33,30 +35,46 @@ function App() { const [showOrbits, setShowOrbits] = useState(true); const [isSoundOn, setIsSoundOn] = useState(false); const [showMessageBoard, setShowMessageBoard] = useState(false); - const [showDetailOverlayId, setShowDetailOverlayId] = useState(null); // State for detail overlay + const [showDetailOverlayId, setShowDetailOverlayId] = useState(null); - // Initialize state from localStorage + // Fetch view_mode from system settings (with fallback to 'solar') + const [systemViewMode, systemViewModeLoading] = useSystemSetting<'solar' | 'galaxy'>('view_mode', 'solar'); + const [viewMode, setViewMode] = useState<'solar' | 'galaxy'>('solar'); + + // Initialize state from system settings first, then localStorage useEffect(() => { - const savedPrefs = localStorage.getItem(PREFS_KEY); - if (savedPrefs) { - try { - const prefs = JSON.parse(savedPrefs); - if (prefs.showOrbits !== undefined) setShowOrbits(prefs.showOrbits); - if (prefs.isSoundOn !== undefined) setIsSoundOn(prefs.isSoundOn); - } catch (e) { - console.error('Failed to parse preferences:', e); + if (!systemViewModeLoading) { + // Priority: system settings > localStorage + const savedPrefs = localStorage.getItem(PREFS_KEY); + let localViewMode: 'solar' | 'galaxy' | undefined; + + if (savedPrefs) { + try { + const prefs = JSON.parse(savedPrefs); + if (prefs.showOrbits !== undefined) setShowOrbits(prefs.showOrbits); + if (prefs.isSoundOn !== undefined) setIsSoundOn(prefs.isSoundOn); + localViewMode = prefs.viewMode; + } catch (e) { + console.error('Failed to parse preferences:', e); + } } + + // 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]); // Persist preferences useEffect(() => { const prefs = { showOrbits, isSoundOn, + viewMode, }; localStorage.setItem(PREFS_KEY, JSON.stringify(prefs)); - }, [showOrbits, isSoundOn]); + }, [showOrbits, isSoundOn, viewMode]); const [selectedDate, setSelectedDate] = useState(null); const { takeScreenshot } = useScreenshot(); @@ -150,11 +168,11 @@ function App() { // Only show full screen loading when we have no data // This prevents flashing when timeline is playing and fetching new data - if (loading && bodies.length === 0) { + if (loading && bodies.length === 0 && viewMode === 'solar') { return ; } - if (error) { + if (error && viewMode === 'solar') { return (
@@ -200,6 +218,11 @@ function App() { } }} onScreenshot={handleScreenshot} + viewMode={viewMode} + onToggleViewMode={() => { + setViewMode(prev => prev === 'solar' ? 'galaxy' : 'solar'); + setSelectedBody(null); // Reset selection when switching + }} /> {/* Auth Modal */} @@ -215,54 +238,61 @@ function App() { onClose={() => setShowMessageBoard(false)} /> - {/* Probe List Sidebar */} - setResetTrigger(prev => prev + 1)} - /> + {/* View Mode Rendering */} + {viewMode === 'solar' ? ( + <> + {/* Probe List Sidebar */} + setResetTrigger(prev => prev + 1)} + /> - {/* 3D Scene */} - + {/* 3D Scene */} + - {/* Timeline Controller */} - {isTimelineMode && cutoffDate && ( - + {/* Timeline Controller */} + {isTimelineMode && cutoffDate && ( + + )} + + {/* Instructions overlay (Only show when exploring freely) */} + {!selectedBody && ( +
+
+ 左键 旋转 +
+
+ 右键 平移 +
+
+ 滚轮 缩放 +
+
+ )} + + ) : ( + )} {/* Interstellar Ticker Sound (Controlled) */} - {/* Instructions overlay (Only show when exploring freely) */} - {!selectedBody && ( -
-
- 左键 旋转 -
-
- 右键 平移 -
-
- 滚轮 缩放 -
-
- )} - {/* Body Detail Overlay */} } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/BodyDetailOverlay.tsx b/frontend/src/components/BodyDetailOverlay.tsx index e6ae149..2020f41 100644 --- a/frontend/src/components/BodyDetailOverlay.tsx +++ b/frontend/src/components/BodyDetailOverlay.tsx @@ -1,10 +1,11 @@ -import { useRef, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import { XCircle } from 'lucide-react'; -import { Canvas, useFrame } from '@react-three/fiber'; +import { Canvas } from '@react-three/fiber'; import { OrbitControls } from '@react-three/drei'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; +import { Descriptions, Tag } from 'antd'; import { request } from '../utils/request'; import { useToast } from '../contexts/ToastContext'; @@ -13,6 +14,7 @@ import type { CelestialBody as CelestialBodyType } from '../types'; interface BodyDetailOverlayProps { bodyId: string | null; + preloadedData?: CelestialBodyType; // Optional preloaded data (for interstellar stars) onClose: () => void; } @@ -20,12 +22,18 @@ interface BodyDetailOverlayProps { // Removed AutoRotateCamera to allow user to control the view angle -export function BodyDetailOverlay({ bodyId, onClose }: BodyDetailOverlayProps) { +export function BodyDetailOverlay({ bodyId, preloadedData, onClose }: BodyDetailOverlayProps) { const [bodyData, setBodyData] = useState(null); const [loading, setLoading] = useState(false); const toast = useToast(); useEffect(() => { + // If preloadedData is provided, use it directly + if (preloadedData) { + setBodyData(preloadedData); + return; + } + if (!bodyId) { setBodyData(null); return; @@ -44,9 +52,9 @@ export function BodyDetailOverlay({ bodyId, onClose }: BodyDetailOverlayProps) { .finally(() => { setLoading(false); }); - }, [bodyId, onClose, toast]); + }, [bodyId, preloadedData, onClose, toast]); - if (!bodyId || !bodyData) { + if ((!bodyId && !preloadedData) || !bodyData) { return null; } @@ -57,7 +65,7 @@ export function BodyDetailOverlay({ bodyId, onClose }: BodyDetailOverlayProps) { {/* Close Button */} @@ -83,7 +91,7 @@ export function BodyDetailOverlay({ bodyId, onClose }: BodyDetailOverlayProps) { maxPolarAngle={Math.PI} minPolarAngle={0} /> - + )}
@@ -98,7 +106,74 @@ export function BodyDetailOverlay({ bodyId, onClose }: BodyDetailOverlayProps) { {bodyData.description && <> | {bodyData.description}}
- {bodyData.details ? ( + {/* Star System Data - Use Descriptions format (like admin panel) */} + {bodyData.starSystemData ? ( +
+ + {bodyData.starSystemData.system_id} + {bodyData.starSystemData.host_star_name} + + {bodyData.starSystemData.distance_pc + ? `${bodyData.starSystemData.distance_pc.toFixed(2)} pc (~${(bodyData.starSystemData.distance_ly || bodyData.starSystemData.distance_pc * 3.26).toFixed(2)} ly)` + : '-'} + + {bodyData.starSystemData.spectral_type || '-'} + + {bodyData.starSystemData.radius_solar ? `${bodyData.starSystemData.radius_solar.toFixed(2)} R☉` : '-'} + + + {bodyData.starSystemData.mass_solar ? `${bodyData.starSystemData.mass_solar.toFixed(2)} M☉` : '-'} + + + {bodyData.starSystemData.temperature_k ? `${bodyData.starSystemData.temperature_k.toFixed(0)} K` : '-'} + + {bodyData.starSystemData.planet_count || 0} + + + {/* Planet List */} + {bodyData.starSystemData.planets && bodyData.starSystemData.planets.length > 0 && ( +
+

天体列表

+
+ {bodyData.starSystemData.planets.map((planet: any) => ( +
+
+
+
{planet.name_zh || planet.name}
+
{planet.id}
+
+ {planet.type} +
+ {planet.description && ( +
{planet.description}
+ )} + {planet.extra_data && ( +
+ {planet.extra_data.semi_major_axis_au && ( +
半长轴: {planet.extra_data.semi_major_axis_au.toFixed(4)} AU
+ )} + {planet.extra_data.period_days && ( +
周期: {planet.extra_data.period_days.toFixed(2)} 天
+ )} + {planet.extra_data.radius_earth && ( +
半径: {planet.extra_data.radius_earth.toFixed(2)} R⊕
+ )} +
+ )} +
+ ))} +
+
+ )} +
+ ) : bodyData.details ? ( + /* Regular body details - Use Markdown rendering (for solar system bodies) */
, blockquote: ({node, ...props}) =>
, a: ({node, ...props}) => , - table: ({node, ...props}) => , - th: ({node, ...props}) =>
, - td: ({node, ...props}) => , + table: ({node, ...props}) => , + th: ({node, ...props}) => + {/* Hidden system_id field - auto-set from selector */} + + + ) : ( + // 新增模式:只显示基础信息 + <> + +

推荐使用 JPL Horizons 数字 ID 进行搜索,可获得最准确的结果。

+

+ 示例:Hubble 的 ID 是 -48,Voyager 1 的 ID 是 -31 +

+

+ 不知道 ID?可以先用名称搜索,系统会尽量提取 ID,或提示您手动确认。 +

+ + } + type="info" + showIcon + style={{ marginBottom: 16 }} + /> + + + + setSearchQuery(e.target.value)} + onPressEnter={handleNASASearch} + /> + + + + + +
+ + + + + + + + + + + + {/* Hidden system_id field - auto-set from selector */} + + + + + + + + + + + + + + + + + + + + )} diff --git a/frontend/src/pages/admin/NASADownload.tsx b/frontend/src/pages/admin/NASADownload.tsx index cd6c21e..f6a7d46 100644 --- a/frontend/src/pages/admin/NASADownload.tsx +++ b/frontend/src/pages/admin/NASADownload.tsx @@ -107,7 +107,17 @@ export function NASADownload() { setLoading(true); try { const { data } = await request.get('/celestial/positions/download/bodies'); - setBodies(data.bodies || {}); + + // 只显示激活的天体 (is_active = true) + const activeBodies: GroupedBodies = {}; + Object.keys(data.bodies || {}).forEach((type) => { + const bodiesOfType = data.bodies[type].filter((body: CelestialBody) => body.is_active); + if (bodiesOfType.length > 0) { + activeBodies[type] = bodiesOfType; + } + }); + + setBodies(activeBodies); } catch (error) { toast.error('加载天体列表失败'); } finally { diff --git a/frontend/src/pages/admin/StarSystems.tsx b/frontend/src/pages/admin/StarSystems.tsx new file mode 100644 index 0000000..d63973d --- /dev/null +++ b/frontend/src/pages/admin/StarSystems.tsx @@ -0,0 +1,548 @@ +import { useState, useEffect } from 'react'; +import { Modal, Form, Input, InputNumber, Select, Button, Space, Popconfirm, Tag, Descriptions, Tabs } from 'antd'; +import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons'; +import type { ColumnsType } from 'antd/es/table'; +import MdEditor from 'react-markdown-editor-lite'; +import MarkdownIt from 'markdown-it'; +import 'react-markdown-editor-lite/lib/index.css'; + +import { DataTable } from '../../components/admin/DataTable'; +import { request } from '../../utils/request'; +import { useToast } from '../../contexts/ToastContext'; + +const MdEditorParser = new MarkdownIt(); + +interface StarSystem { + id: number; + name: string; + name_zh: string; + host_star_name: string; + distance_pc: number | null; + distance_ly: number | null; + ra: number | null; + dec: number | null; + position_x: number | null; + position_y: number | null; + position_z: number | null; + spectral_type: string | null; + radius_solar: number | null; + mass_solar: number | null; + temperature_k: number | null; + magnitude: number | null; + luminosity_solar: number | null; + color: string | null; + planet_count: number; + description: string | null; + details: string | null; + created_at: string; + updated_at: string; +} + +interface StarSystemWithBodies extends StarSystem { + bodies: any[]; + body_count: number; +} + +export function StarSystems() { + const [loading, setLoading] = useState(false); + const [data, setData] = useState([]); + const [total, setTotal] = useState(0); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(20); + const [searchKeyword, setSearchKeyword] = useState(''); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isDetailModalOpen, setIsDetailModalOpen] = useState(false); + const [editingRecord, setEditingRecord] = useState(null); + const [detailData, setDetailData] = useState(null); + const [activeTabKey, setActiveTabKey] = useState('basic'); // 添加tab状态 + const [form] = Form.useForm(); + const toast = useToast(); + + useEffect(() => { + loadData(); + }, [currentPage, pageSize, searchKeyword]); + + const loadData = async () => { + setLoading(true); + try { + const { data: result } = await request.get('/star-systems', { + params: { + skip: (currentPage - 1) * pageSize, + limit: pageSize, + search: searchKeyword || undefined, + exclude_solar: false, + }, + }); + setData(result.systems || []); + setTotal(result.total || 0); + } catch (error) { + toast.error('加载恒星系统数据失败'); + } finally { + setLoading(false); + } + }; + + const handleAdd = () => { + setEditingRecord(null); + form.resetFields(); + form.setFieldsValue({ + color: '#FFFFFF', + planet_count: 0, + }); + setActiveTabKey('basic'); // 重置到基础信息tab + setIsModalOpen(true); + }; + + const handleEdit = (record: StarSystem) => { + setEditingRecord(record); + form.setFieldsValue(record); + setActiveTabKey('basic'); // 重置到基础信息tab + setIsModalOpen(true); + }; + + const handleViewDetails = async (record: StarSystem) => { + try { + const { data: result } = await request.get(`/star-systems/${record.id}/bodies`); + setDetailData(result); + setIsDetailModalOpen(true); + } catch (error) { + toast.error('加载恒星系统详情失败'); + } + }; + + const handleDelete = async (id: number) => { + if (id === 1) { + toast.error('不能删除太阳系'); + return; + } + + try { + await request.delete(`/star-systems/${id}`); + toast.success('删除成功'); + loadData(); + } catch (error: any) { + toast.error(error.response?.data?.detail || '删除失败'); + } + }; + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + + if (editingRecord) { + // Update + await request.put(`/star-systems/${editingRecord.id}`, values); + toast.success('更新成功'); + } else { + // Create + await request.post('/star-systems', values); + toast.success('创建成功'); + } + + setIsModalOpen(false); + loadData(); + } catch (error: any) { + if (error.errorFields) { + toast.error('请填写必填字段'); + } else { + toast.error(error.response?.data?.detail || '操作失败'); + } + } + }; + + const handleSearch = (value: string) => { + setSearchKeyword(value); + setCurrentPage(1); + }; + + const columns: ColumnsType = [ + { + title: 'ID', + dataIndex: 'id', + key: 'id', + width: 60, + }, + { + title: '系统名称', + dataIndex: 'name', + key: 'name', + width: 200, + render: (text, record) => ( +
+
{text}
+ {record.name_zh &&
{record.name_zh}
} +
+ ), + }, + { + title: '主恒星', + dataIndex: 'host_star_name', + key: 'host_star_name', + width: 150, + }, + { + title: '距离', + dataIndex: 'distance_pc', + key: 'distance', + width: 120, + render: (pc, record) => { + if (!pc) return '-'; + const ly = record.distance_ly || pc * 3.26; + return ( +
+
{pc.toFixed(2)} pc
+
{ly.toFixed(2)} ly
+
+ ); + }, + }, + { + title: '光谱类型', + dataIndex: 'spectral_type', + key: 'spectral_type', + width: 100, + render: (text) => text || '-', + }, + { + title: '恒星参数', + key: 'stellar_params', + width: 150, + render: (_, record) => ( +
+ {record.radius_solar &&
R: {record.radius_solar.toFixed(2)} R☉
} + {record.mass_solar &&
M: {record.mass_solar.toFixed(2)} M☉
} + {record.temperature_k &&
T: {record.temperature_k.toFixed(0)} K
} +
+ ), + }, + { + title: '行星数量', + dataIndex: 'planet_count', + key: 'planet_count', + width: 100, + render: (count) => ( + 0 ? 'green' : 'default'}>{count} + ), + }, + { + title: '操作', + key: 'actions', + fixed: 'right', + width: 180, + render: (_, record) => ( + + + + {record.id !== 1 && ( + handleDelete(record.id)} + okText="删除" + cancelText="取消" + okButtonProps={{ danger: true }} + > + + + )} + + ), + }, + ]; + + return ( +
+ { + setCurrentPage(page); + setPageSize(size); + }} + onAdd={handleAdd} + onSearch={handleSearch} + searchPlaceholder="搜索恒星系统名称..." + /> + + {/* 创建/编辑 Modal */} + setIsModalOpen(false)} + width={800} + okText="保存" + cancelText="取消" + > +
+ {editingRecord ? ( + // 编辑模式:双tab + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + MdEditorParser.render(text)} + onChange={({ text }) => form.setFieldValue('details', text)} + /> + + +
+ ) : ( + // 新增模式:只显示基础信息 + <> + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + )} + +
+ + {/* 详情 Modal */} + setIsDetailModalOpen(false)} + footer={null} + width={900} + > + {detailData && ( +
+ + {detailData.id} + {detailData.host_star_name} + + {detailData.distance_pc ? `${detailData.distance_pc.toFixed(2)} pc (~${(detailData.distance_ly || detailData.distance_pc * 3.26).toFixed(2)} ly)` : '-'} + + {detailData.spectral_type || '-'} + {detailData.radius_solar ? `${detailData.radius_solar.toFixed(2)} R☉` : '-'} + {detailData.mass_solar ? `${detailData.mass_solar.toFixed(2)} M☉` : '-'} + {detailData.temperature_k ? `${detailData.temperature_k.toFixed(0)} K` : '-'} + {detailData.body_count} + + + {detailData.bodies && detailData.bodies.length > 0 && ( +
+

天体列表

+
+ {detailData.bodies.map((body: any) => ( +
+
+
+
{body.name_zh || body.name}
+
{body.id}
+
+ {body.type} +
+ {body.description && ( +
{body.description}
+ )} + {body.extra_data && ( +
+ {body.extra_data.semi_major_axis_au &&
半长轴: {body.extra_data.semi_major_axis_au.toFixed(4)} AU
} + {body.extra_data.period_days &&
周期: {body.extra_data.period_days.toFixed(2)} 天
} + {body.extra_data.radius_earth &&
半径: {body.extra_data.radius_earth.toFixed(2)} R⊕
} +
+ )} +
+ ))} +
+
+ )} +
+ )} +
+
+ ); +} diff --git a/frontend/src/pages/admin/StaticData.tsx b/frontend/src/pages/admin/StaticData.tsx index 7052c80..3d64e6d 100644 --- a/frontend/src/pages/admin/StaticData.tsx +++ b/frontend/src/pages/admin/StaticData.tsx @@ -125,6 +125,7 @@ export function StaticData() { { text: '星云', value: 'nebula' }, { text: '小行星带', value: 'asteroid_belt' }, { text: '柯伊伯带', value: 'kuiper_belt' }, + { text: '恒星际', value: 'interstellar' }, ], onFilter: (value, record) => record.category === value, }, @@ -184,6 +185,7 @@ export function StaticData() { 星云 (Nebula) 小行星带 (Asteroid Belt) 柯伊伯带 (Kuiper Belt) + 恒星际 (Interstellar) diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index bd0c0d2..a458a6b 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -18,7 +18,22 @@ export interface CelestialBody { type: CelestialBodyType; positions: Position[]; description?: string; + details?: string; // Markdown content for detailed view is_active?: boolean; // Probe status: true = active, false = inactive + // Star system specific data + starSystemData?: { + system_id: number; + host_star_name: string; + distance_pc?: number; + distance_ly?: number; + spectral_type?: string; + radius_solar?: number; + mass_solar?: number; + temperature_k?: number; + planet_count?: number; + planets?: any[]; // Array of planet data + color?: string; // Star color from database + }; } export interface CelestialDataResponse {
, + td: ({node, ...props}) => , }} > {bodyData.details} diff --git a/frontend/src/components/BodyViewer.tsx b/frontend/src/components/BodyViewer.tsx index 912537b..4afb147 100644 --- a/frontend/src/components/BodyViewer.tsx +++ b/frontend/src/components/BodyViewer.tsx @@ -1,5 +1,5 @@ import { useRef, useMemo, useState, useEffect, Suspense } from 'react'; -import { Mesh, Group } from 'three'; +import { Mesh } from 'three'; import * as THREE from 'three'; import { useGLTF, useTexture } from '@react-three/drei'; import { useFrame } from '@react-three/fiber'; @@ -9,12 +9,12 @@ import { getCelestialSize } from '../config/celestialSizes'; interface BodyViewerProps { body: CelestialBodyType; + disableGlow?: boolean; // 禁用光晕效果(用于详情视图) } // Reusable component to render just the 3D model/mesh of a celestial body -export function BodyViewer({ body }: BodyViewerProps) { +export function BodyViewer({ body, disableGlow = false }: BodyViewerProps) { const meshRef = useRef(null); - const groupRef = useRef(null); const [texturePath, setTexturePath] = useState(undefined); const [modelPath, setModelPath] = useState(undefined); const [modelScale, setModelScale] = useState(1.0); @@ -44,7 +44,9 @@ export function BodyViewer({ body }: BodyViewerProps) { } if (body.type === 'star') { - return { size: finalSize, emissive: '#FDB813', emissiveIntensity: 1.5 }; + // Use database color if available (for interstellar stars), otherwise default to yellow + const starColor = body.starSystemData?.color || '#FDB813'; + return { size: finalSize, emissive: starColor, emissiveIntensity: 1.5 }; } if (body.type === 'comet') { return { size: finalSize, emissive: '#000000', emissiveIntensity: 0 }; @@ -114,6 +116,7 @@ export function BodyViewer({ body }: BodyViewerProps) { emissiveIntensity={appearance.emissiveIntensity} texturePath={texturePath} meshRef={meshRef} + disableGlow={disableGlow} /> ); } @@ -176,13 +179,14 @@ function ProbeModelViewer({ modelPath, modelScale }: { modelPath: string; modelS } // Sub-component for Planet models -function PlanetModelViewer({ body, size, emissive, emissiveIntensity, texturePath, meshRef }: { +function PlanetModelViewer({ body, size, emissive, emissiveIntensity, texturePath, meshRef, disableGlow = false }: { body: CelestialBodyType; size: number; emissive: string; emissiveIntensity: number; texturePath: string | null; meshRef: React.RefObject; + disableGlow?: boolean; }) { const texture = texturePath ? useTexture(texturePath) : null; @@ -394,8 +398,8 @@ function PlanetModelViewer({ body, size, emissive, emissiveIntensity, texturePat {/* Saturn Rings */} {body.id === '699' && } - {/* Sun glow effect - multi-layer scattered light */} - {body.type === 'star' && ( + {/* Sun glow effect - multi-layer scattered light (仅在非禁用光晕模式下显示) */} + {body.type === 'star' && !disableGlow && ( <> diff --git a/frontend/src/components/ControlPanel.tsx b/frontend/src/components/ControlPanel.tsx index 8995f51..833063f 100644 --- a/frontend/src/components/ControlPanel.tsx +++ b/frontend/src/components/ControlPanel.tsx @@ -6,7 +6,9 @@ import { MessageSquare, Eye, EyeOff, - Camera + Camera, + Globe, + Sparkles } from 'lucide-react'; interface ControlPanelProps { @@ -19,6 +21,8 @@ interface ControlPanelProps { showMessageBoard: boolean; onToggleMessageBoard: () => void; onScreenshot: () => void; + viewMode: 'solar' | 'galaxy'; + onToggleViewMode: () => void; } export function ControlPanel({ @@ -31,6 +35,8 @@ export function ControlPanel({ showMessageBoard, onToggleMessageBoard, onScreenshot, + viewMode, + onToggleViewMode, }: ControlPanelProps) { const buttonClass = (isActive: boolean) => ` p-2 rounded-lg transition-all duration-200 relative group @@ -44,6 +50,17 @@ export function ControlPanel({ return (
+ {/* View Mode Toggle */} + + {/* Timeline Toggle */}
- } - type="info" - showIcon - style={{ marginBottom: 16 }} - /> - - - - setSearchQuery(e.target.value)} - onPressEnter={handleNASASearch} - /> - - - - - )} - - + {editingRecord ? ( + // 编辑模式:显示双tab + + +