diff --git a/.DS_Store b/.DS_Store
index 52f6c40..d968aa9 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index df9873d..263430f 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -49,7 +49,8 @@
"Bash(tee:*)",
"Bash(kill:*)",
"Bash(./venv/bin/python3:*)",
- "WebSearch"
+ "WebSearch",
+ "Bash(PYTHONPATH=/Users/jiliu/WorkSpace/cosmo/backend ./venv/bin/python:*)"
],
"deny": [],
"ask": []
diff --git a/LOGIN_REDESIGN.md b/LOGIN_REDESIGN.md
new file mode 100644
index 0000000..cd9087c
--- /dev/null
+++ b/LOGIN_REDESIGN.md
@@ -0,0 +1,175 @@
+# 登录界面统一风格改造
+
+**日期**: 2025-12-10
+**状态**: ✅ 完成
+
+---
+
+## 改造目标
+
+将登录界面与平台首页风格统一,使用相同的星空背景和组件化设计。
+
+---
+
+## 实施内容
+
+### 1. 新增组件
+
+#### SpaceBackground.tsx
+**路径**: `frontend/src/components/SpaceBackground.tsx`
+
+**功能**: 统一的星空背景组件,可复用于多个页面
+
+**特性**:
+- 程序生成的星星背景 (5000颗星)
+- 可选的星云效果
+- 可调节透明度
+- 使用 React Three Fiber 渲染
+
+**Props**:
+```typescript
+interface SpaceBackgroundProps {
+ showNebulae?: boolean; // 是否显示星云(默认 true)
+ starCount?: number; // 星星数量(默认 5000)
+ opacity?: number; // 背景透明度(默认 1)
+ className?: string; // 额外的 CSS 类名
+}
+```
+
+---
+
+#### LoginCard.tsx
+**路径**: `frontend/src/components/LoginCard.tsx`
+
+**功能**: 组件化的登录卡片,使用毛玻璃效果
+
+**设计特点**:
+- 黑色半透明背景 (`bg-black/40`)
+- 毛玻璃模糊效果 (`backdrop-blur-md`)
+- 白色半透明边框 (`border-white/10`)
+- 圆角卡片 (`rounded-2xl`)
+- 深色输入框,与星空主题一致
+
+**Props**:
+```typescript
+interface LoginCardProps {
+ onLoginSuccess: (userData: any) => void; // 登录成功回调
+ className?: string; // 额外的 CSS 类名
+}
+```
+
+---
+
+### 2. 重构页面
+
+#### Login.tsx
+**路径**: `frontend/src/pages/Login.tsx`
+
+**改动前**:
+- 使用渐变背景 (`linear-gradient(135deg, #667eea 0%, #764ba2 100%)`)
+- 使用 Ant Design Card 组件
+- 白色背景卡片,与首页风格不一致
+
+**改动后**:
+- 使用 `SpaceBackground` 组件(星空背景)
+- 使用 `LoginCard` 组件(毛玻璃卡片)
+- 添加"返回首页"链接
+- 完全组件化,易于维护和复用
+
+---
+
+## 视觉效果对比
+
+### 改造前
+```
+┌───────────────────────────────┐
+│ 紫色渐变背景 │
+│ │
+│ ┌─────────────────┐ │
+│ │ 白色卡片 │ │
+│ │ Cosmo 后台管理 │ │
+│ │ [用户名] │ │
+│ │ [密码] │ │
+│ │ [登录按钮] │ │
+│ └─────────────────┘ │
+│ │
+└───────────────────────────────┘
+```
+
+### 改造后
+```
+┌───────────────────────────────┐
+│ ✨ 星空背景(星星 + 星云) │
+│ │
+│ ┌─────────────────┐ │
+│ │ 半透明毛玻璃卡片 │ │
+│ │ Cosmo │ │
+│ │ 宇宙星空可视化 │ │
+│ │ [深色输入框] │ │
+│ │ [深色输入框] │ │
+│ │ [蓝色按钮] │ │
+│ └─────────────────┘ │
+│ │
+│ [返回首页] │
+└───────────────────────────────┘
+```
+
+---
+
+## 技术细节
+
+### 样式统一
+1. **背景**: 黑色星空 + 星云(与首页 Scene 组件一致)
+2. **卡片**: 毛玻璃效果(`backdrop-blur-md`)
+3. **颜色**: 深色主题,白色/灰色文字
+4. **输入框**: 深色背景,半透明边框
+5. **按钮**: 蓝色主题(`bg-blue-600`)
+
+### 组件复用
+- `SpaceBackground` 可用于其他需要星空背景的页面
+- `LoginCard` 可用于弹窗登录、注册等场景
+
+### 响应式设计
+- 使用 `max-w-md` 限制卡片最大宽度
+- 使用 `p-4` 在小屏幕上提供内边距
+- 卡片在各种屏幕尺寸下居中显示
+
+---
+
+## 测试清单
+
+- [x] 登录页面背景显示星空和星云
+- [x] 登录卡片使用毛玻璃效果
+- [x] 输入框和按钮样式正确
+- [x] 登录功能正常工作
+- [x] "返回首页"链接正常工作
+- [x] 响应式布局正常
+
+---
+
+## 后续优化建议
+
+1. **动画效果**: 为登录卡片添加淡入动画
+2. **星空互动**: 鼠标移动时星空微微偏移(视差效果)
+3. **主题切换**: 支持亮色/暗色主题切换
+4. **多语言**: 支持中英文切换
+
+---
+
+## 相关文件
+
+### 新增文件
+- `frontend/src/components/SpaceBackground.tsx`
+- `frontend/src/components/LoginCard.tsx`
+
+### 修改文件
+- `frontend/src/pages/Login.tsx`
+
+### 依赖组件
+- `frontend/src/components/Nebulae.tsx` (已存在)
+- `@react-three/fiber` (已安装)
+- `@react-three/drei` (已安装)
+
+---
+
+**完成状态**: ✅ 所有改造已完成并测试通过
diff --git a/ORBIT_GENERATION_SYSTEM.md b/ORBIT_GENERATION_SYSTEM.md
new file mode 100644
index 0000000..44e5af3
--- /dev/null
+++ b/ORBIT_GENERATION_SYSTEM.md
@@ -0,0 +1,404 @@
+# 天体轨道生成系统文档
+
+**版本**: 1.0
+**最后更新**: 2025-12-10
+
+---
+
+## 概述
+
+Cosmo项目中的行星和矮行星轨道采用**预计算**方式存储在数据库中,而不是实时计算。这样做的好处是:
+
+1. **性能优化** - 前端无需实时计算复杂的椭圆轨道
+2. **NASA数据** - 使用真实的NASA JPL Horizons API数据
+3. **精确性** - 考虑了引力摄动等真实天文因素
+
+---
+
+## 轨道生成逻辑
+
+### 1. 适用天体类型
+
+轨道生成功能仅适用于以下两种天体类型:
+
+- **行星 (planet)** - 八大行星
+- **矮行星 (dwarf_planet)** - 冥王星、谷神星、阋神星等
+
+### 2. 生成流程
+
+#### 步骤1: 确定轨道参数
+
+在 `backend/app/api/celestial_orbit.py` 中定义了硬编码的轨道周期:
+
+```python
+ORBITAL_PERIODS = {
+ # 行星 - 一个完整公转周期
+ "199": 88.0, # 水星 (88天)
+ "299": 224.7, # 金星
+ "399": 365.25, # 地球 (1年)
+ "499": 687.0, # 火星
+ "599": 4333.0, # 木星 (11.86年)
+ "699": 10759.0, # 土星 (29.46年)
+ "799": 30687.0, # 天王星 (84.01年)
+ "899": 60190.0, # 海王星 (164.79年)
+
+ # 矮行星 - 一个完整公转周期
+ "999": 90560.0, # 冥王星 (247.94年)
+ "2000001": 1680.0, # 谷神星 (4.6年)
+ "136199": 203500.0,# 阋神星 (557年)
+ "136108": 104000.0,# 妊神星 (285年)
+ "136472": 112897.0,# 鸟神星 (309年)
+}
+```
+
+轨道颜色也是硬编码的:
+
+```python
+DEFAULT_COLORS = {
+ "199": "#8C7853", # 水星 - 棕色
+ "299": "#FFC649", # 金星 - 黄色
+ "399": "#4A90E2", # 地球 - 蓝色
+ "499": "#CD5C5C", # 火星 - 红色
+ # ... 其他天体
+}
+```
+
+#### 步骤2: 计算采样点数量
+
+采样策略(`orbit_service.py`):
+
+```python
+MIN_POINTS = 100 # 最少100个点,保证椭圆光滑
+MAX_POINTS = 1000 # 最多1000个点,避免数据过大
+
+if period_days < 3650: # < 10年
+ # 行星:约每天1个点,最少100个
+ num_points = max(MIN_POINTS, min(int(period_days), 365))
+else: # >= 10年
+ # 外行星和矮行星:每月采样一次
+ num_points = min(int(period_days / 30), MAX_POINTS)
+```
+
+**示例**:
+- 地球 (365.25天) → 365个采样点
+- 冥王星 (90560天 ≈ 248年) → 1000个采样点(每90天一个)
+
+#### 步骤3: 查询NASA Horizons API
+
+调用NASA JPL Horizons API获取真实轨道数据:
+
+```python
+positions = await horizons_service.get_body_positions(
+ body_id=body_id,
+ start_time=start_time,
+ end_time=end_time,
+ step=f"{step_days}d"
+)
+```
+
+**特殊处理**:
+- 短周期天体(<150年):从当前时间开始
+- 长周期天体(≥150年):从1900年开始(避免超出NASA数据范围)
+
+#### 步骤4: 存储到数据库
+
+将轨道点存储到 `orbits` 表:
+
+```sql
+CREATE TABLE orbits (
+ body_id VARCHAR(50) PRIMARY KEY,
+ points JSONB NOT NULL, -- [{"x": 1.0, "y": 0.5, "z": 0.0}, ...]
+ num_points INTEGER, -- 点的数量
+ period_days DOUBLE PRECISION, -- 轨道周期(天)
+ color VARCHAR(20), -- 轨道线颜色
+ created_at TIMESTAMP,
+ updated_at TIMESTAMP
+);
+```
+
+---
+
+## 当前问题与解决方案
+
+### 问题1: 新增天体后轨道不自动生成
+
+**现状**:管理员在天体管理界面新增矮行星后,需要手动执行以下步骤:
+
+1. 到"NASA数据下载"页面
+2. 点击"生成轨道"按钮
+3. 系统遍历所有行星/矮行星,调用NASA API生成轨道
+
+**问题**:
+- 流程繁琐,容易遗忘
+- 无法针对单个天体生成
+- 新增天体时轨道参数(周期、颜色)是硬编码的
+
+### 问题2: 轨道参数硬编码
+
+**现状**:轨道周期和颜色定义在 `celestial_orbit.py` 中,无法灵活配置。
+
+**问题**:
+- 新增矮行星必须修改代码添加周期和颜色
+- 无法为自定义天体生成轨道
+- 缺乏数据库层面的配置灵活性
+
+---
+
+## 解决方案设计
+
+### 方案A: 自动触发轨道生成(推荐)
+
+**实现思路**:
+
+1. **数据库扩展** - 在 `celestial_bodies` 表添加轨道参数字段:
+ ```sql
+ ALTER TABLE celestial_bodies ADD COLUMN orbit_period_days DOUBLE PRECISION;
+ ALTER TABLE celestial_bodies ADD COLUMN orbit_color VARCHAR(20);
+ ALTER TABLE celestial_bodies ADD COLUMN auto_generate_orbit BOOLEAN DEFAULT FALSE;
+ ```
+
+2. **创建天体时自动生成** - 修改 `POST /celestial` API:
+ ```python
+ @router.post("")
+ async def create_celestial_body(body_data, db):
+ # 1. 创建天体
+ new_body = await celestial_body_service.create_body(body_data.dict(), db)
+
+ # 2. 如果是行星/矮行星且设置了轨道参数,自动生成轨道
+ if new_body.type in ["planet", "dwarf_planet"] and new_body.orbit_period_days:
+ await orbit_service.generate_orbit(
+ body_id=new_body.id,
+ body_name=new_body.name_zh or new_body.name,
+ period_days=new_body.orbit_period_days,
+ color=new_body.orbit_color or "#CCCCCC",
+ session=db,
+ horizons_service=horizons_service
+ )
+ ```
+
+3. **前端界面调整** - 在天体新增表单中添加:
+ - 轨道周期输入框(天)
+ - 轨道颜色选择器
+ - "自动生成轨道"复选框
+
+**优点**:
+- ✅ 无需手动操作,完全自动化
+- ✅ 轨道参数可配置
+- ✅ 新增天体立即可用
+
+**缺点**:
+- ⚠️ 创建天体时可能耗时较长(等待NASA API响应)
+- ⚠️ 需要修改数据库结构
+
+### 方案B: 异步后台生成
+
+**实现思路**:
+
+1. 创建天体时立即返回,在后台异步生成轨道
+2. 使用Celery或FastAPI BackgroundTasks
+3. 前端显示"轨道生成中..."状态
+
+**优点**:
+- ✅ 用户体验好,不会阻塞
+- ✅ 可以批量生成
+
+**缺点**:
+- ⚠️ 需要引入任务队列(增加系统复杂度)
+- ⚠️ 需要轮询检查生成状态
+
+### 方案C: 手动触发但优化流程(最简单)
+
+**实现思路**:
+
+1. 在天体列表页添加"生成轨道"按钮(每行一个)
+2. 点击后调用 `POST /celestial/admin/orbits/generate?body_ids={id}`
+3. 使用天体的 `extra_data` 字段存储轨道参数
+
+**优点**:
+- ✅ 实现简单,无需修改数据库
+- ✅ 灵活可控
+
+**缺点**:
+- ⚠️ 仍需手动操作
+
+---
+
+## 推荐实施步骤
+
+### Phase 1: 快速修复(方案C)
+
+1. **修改 `CelestialBodyCreate` 模型**,允许在 `extra_data` 中传入:
+ ```json
+ {
+ "orbit_period_days": 90560.0,
+ "orbit_color": "#8B7355"
+ }
+ ```
+
+2. **修改轨道生成API**,优先从 `extra_data` 读取参数:
+ ```python
+ # 优先从天体的extra_data读取,其次从硬编码字典读取
+ extra_data = body.extra_data or {}
+ period = extra_data.get("orbit_period_days") or ORBITAL_PERIODS.get(body.id)
+ color = extra_data.get("orbit_color") or DEFAULT_COLORS.get(body.id, "#CCCCCC")
+ ```
+
+3. **前端添加按钮** - 在天体管理列表每行添加"生成轨道"操作按钮
+
+### Phase 2: 自动化(方案A)
+
+1. 添加数据库迁移,新增轨道参数字段
+2. 修改创建天体API,支持自动生成
+3. 前端表单添加轨道参数输入
+
+---
+
+## API接口文档
+
+### 生成轨道
+
+**端点**: `POST /celestial/admin/orbits/generate`
+
+**查询参数**:
+- `body_ids` (可选) - 逗号分隔的天体ID列表,如 "999,2000001"
+- 如果不提供,则为所有行星和矮行星生成轨道
+
+**响应示例**:
+```json
+{
+ "message": "Generated 2 orbits (0 failed)",
+ "results": [
+ {
+ "body_id": "999",
+ "body_name": "冥王星",
+ "status": "success",
+ "num_points": 1000,
+ "period_days": 90560.0
+ }
+ ]
+}
+```
+
+### 获取轨道数据
+
+**端点**: `GET /celestial/orbits`
+
+**查询参数**:
+- `body_type` (可选) - 过滤天体类型,如 "planet" 或 "dwarf_planet"
+
+**响应示例**:
+```json
+{
+ "orbits": [
+ {
+ "body_id": "399",
+ "body_name": "地球",
+ "body_name_zh": "地球",
+ "points": [
+ {"x": 1.0, "y": 0.0, "z": 0.0},
+ {"x": 0.99, "y": 0.01, "z": 0.0},
+ ...
+ ],
+ "num_points": 365,
+ "period_days": 365.25,
+ "color": "#4A90E2",
+ "updated_at": "2025-12-10T10:30:00"
+ }
+ ]
+}
+```
+
+### 删除轨道
+
+**端点**: `DELETE /celestial/admin/orbits/{body_id}`
+
+**响应**:
+```json
+{
+ "message": "Orbit for 999 deleted successfully"
+}
+```
+
+---
+
+## 数据库结构
+
+### orbits 表
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| body_id | VARCHAR(50) | 天体ID(主键,外键到celestial_bodies) |
+| points | JSONB | 轨道点数组 [{"x", "y", "z"}, ...] |
+| num_points | INTEGER | 轨道点数量 |
+| period_days | DOUBLE PRECISION | 轨道周期(天) |
+| color | VARCHAR(20) | 轨道线颜色(HEX) |
+| created_at | TIMESTAMP | 创建时间 |
+| updated_at | TIMESTAMP | 更新时间 |
+
+**索引**:
+- PRIMARY KEY: `body_id`
+- FOREIGN KEY: `body_id` → `celestial_bodies.id` (ON DELETE CASCADE)
+
+---
+
+## 前端使用
+
+### 获取并渲染轨道
+
+```typescript
+// 1. 获取轨道数据
+const response = await request.get('/celestial/orbits?body_type=planet');
+const orbits = response.data.orbits;
+
+// 2. 渲染轨道线
+orbits.forEach(orbit => {
+ const points = orbit.points.map(p => new Vector3(p.x, p.y, p.z));
+ const geometry = new BufferGeometry().setFromPoints(points);
+ const material = new LineBasicMaterial({ color: orbit.color });
+ const line = new Line(geometry, material);
+ scene.add(line);
+});
+```
+
+---
+
+## 常见问题
+
+### Q1: 为什么不实时计算轨道?
+
+**A**: 实时计算需要考虑多体引力、引力摄动等复杂因素,计算量大且不准确。预计算方式使用NASA真实数据,更准确且性能更好。
+
+### Q2: 如何为新增的矮行星生成轨道?
+
+**A**:
+1. **短期方案**:在天体的 `extra_data` 中添加 `orbit_period_days` 和 `orbit_color`
+2. **长期方案**:等待数据库迁移,使用独立字段存储轨道参数
+
+### Q3: NASA Horizons API有哪些限制?
+
+**A**:
+- 时间范围:通常限制在1900-2200年之间
+- 频率限制:建议每次查询间隔1秒
+- 天体覆盖:只包含太阳系天体,不包括系外行星
+
+### Q4: 轨道数据多久更新一次?
+
+**A**: 理论上轨道是稳定的,无需频繁更新。建议:
+- 行星:每年更新一次(考虑引力摄动)
+- 矮行星:每5年更新一次
+
+---
+
+## 未来优化方向
+
+1. **自动化轨道生成** - 创建天体时自动触发轨道生成
+2. **轨道参数配置化** - 从数据库读取而非硬编码
+3. **批量生成优化** - 并发调用NASA API,提升生成速度
+4. **轨道缓存策略** - 避免重复生成相同天体的轨道
+5. **支持更多天体** - 扩展到卫星、小行星等
+
+---
+
+**文档作者**: Claude Code AI
+**版本历史**:
+- v1.0 (2025-12-10) - 初始版本,基于现有代码分析
diff --git a/ORBIT_GENERATION_USER_GUIDE.md b/ORBIT_GENERATION_USER_GUIDE.md
new file mode 100644
index 0000000..9358973
--- /dev/null
+++ b/ORBIT_GENERATION_USER_GUIDE.md
@@ -0,0 +1,316 @@
+# 天体轨道自动生成功能使用说明
+
+**版本**: 1.0
+**最后更新**: 2025-12-10
+
+---
+
+## 功能概述
+
+Cosmo项目现已支持为**行星**和**矮行星**自动生成公转轨道。该功能通过调用NASA JPL Horizons API获取真实的天体位置数据,预先计算并存储完整的轨道路径,供前端高效渲染。
+
+---
+
+## 适用天体类型
+
+| 天体类型 | 是否支持轨道生成 | 说明 |
+|----------|------------------|------|
+| ✅ 行星 (planet) | 是 | 太阳系八大行星 |
+| ✅ 矮行星 (dwarf_planet) | 是 | 冥王星、谷神星、阋神星等 |
+| ❌ 卫星 (satellite) | 否 | 当前版本不支持 |
+| ❌ 恒星 (star) | 否 | 恒星无需轨道 |
+| ❌ 探测器 (probe) | 否 | 当前版本不支持 |
+| ❌ 彗星 (comet) | 否 | 当前版本不支持 |
+
+---
+
+## 使用方法
+
+### 方法一:为新增天体生成轨道(推荐)
+
+#### 步骤1: 新增天体时填写轨道参数
+
+1. 进入 **管理后台 > 天体数据管理**
+2. 选择恒星系统(例如"太阳系")
+3. 点击 **"新增"** 按钮
+4. 填写基本信息:
+ - JPL Horizons ID
+ - 英文名 / 中文名
+ - **类型**: 选择"行星"或"矮行星"
+ - 描述
+
+5. **填写轨道参数**(当类型为行星/矮行星时自动显示):
+
+ ![轨道参数示例]
+
+ - **轨道周期(天)**: 天体完整公转一周所需的天数
+ - 例如:地球 = 365.25天
+ - 例如:冥王星 = 90560天(约248年)
+
+ - **轨道颜色**: 轨道线的显示颜色(HEX格式)
+ - 例如:地球 = #4A90E2(蓝色)
+ - 例如:火星 = #CD5C5C(红色)
+ - 点击颜色选择器可视化选择
+
+6. 保存天体
+
+#### 步骤2: 生成轨道
+
+1. 保存后返回天体列表
+2. 找到刚才新增的天体
+3. 点击该行右侧的 **"生成轨道"** 按钮
+4. 等待生成完成(通常需要5-30秒)
+5. 成功后会显示:"轨道生成成功!共 XXX 个点"
+
+---
+
+### 方法二:为已有天体补充轨道参数并生成
+
+#### 步骤1: 编辑天体添加轨道参数
+
+1. 进入 **管理后台 > 天体数据管理**
+2. 选择对应的恒星系统
+3. 找到目标天体,点击 **"编辑"** 按钮
+4. 在"基础信息"Tab中找到"轨道参数"部分
+5. 填写:
+ - 轨道周期(天)
+ - 轨道颜色
+6. 保存
+
+#### 步骤2: 生成轨道
+
+1. 返回列表,点击 **"生成轨道"** 按钮
+2. 等待生成完成
+
+---
+
+### 方法三:批量生成(适合系统管理员)
+
+如果您已经在代码中定义了轨道参数(通过硬编码的ORBITAL_PERIODS字典),可以使用批量生成:
+
+1. 进入 **管理后台 > NASA数据下载**
+2. 点击 **"生成所有轨道"** 按钮
+3. 系统会自动为所有已定义轨道周期的行星/矮行星生成轨道
+
+---
+
+## 轨道参数参考
+
+### 太阳系行星轨道周期
+
+| 天体 | 轨道周期(天) | 建议颜色 |
+|------|---------------|----------|
+| 水星 | 88.0 | #8C7853 |
+| 金星 | 224.7 | #FFC649 |
+| 地球 | 365.25 | #4A90E2 |
+| 火星 | 687.0 | #CD5C5C |
+| 木星 | 4333.0 | #DAA520 |
+| 土星 | 10759.0 | #F4A460 |
+| 天王星 | 30687.0 | #4FD1C5 |
+| 海王星 | 60190.0 | #4169E1 |
+
+### 太阳系矮行星轨道周期
+
+| 天体 | 轨道周期(天) | 建议颜色 |
+|------|---------------|----------|
+| 谷神星 (Ceres) | 1680.0 | #9E9E9E |
+| 冥王星 (Pluto) | 90560.0 | #8B7355 |
+| 阋神星 (Eris) | 203500.0 | #E0E0E0 |
+| 妊神星 (Haumea) | 104000.0 | #D4A574 |
+| 鸟神星 (Makemake) | 112897.0 | #C49A6C |
+
+---
+
+## 技术细节
+
+### 轨道采样策略
+
+系统会根据轨道周期智能决定采样点数量:
+
+- **短周期天体**(<10年):约每天1个点,最少100个点
+ - 例如:地球(365天)→ 365个采样点
+
+- **长周期天体**(≥10年):约每月1个点,最多1000个点
+ - 例如:冥王星(90560天)→ 1000个采样点
+
+### 数据存储
+
+生成的轨道数据存储在 `orbits` 表中:
+
+```sql
+SELECT * FROM orbits WHERE body_id = '999'; -- 查看冥王星的轨道
+```
+
+返回字段:
+- `body_id`: 天体ID
+- `points`: 轨道点数组(JSONB)
+- `num_points`: 点数量
+- `period_days`: 轨道周期
+- `color`: 轨道颜色
+- `updated_at`: 最后更新时间
+
+### 前端渲染
+
+前端通过API获取轨道数据后,使用Three.js的Line对象渲染:
+
+```typescript
+GET /celestial/orbits?body_type=planet
+```
+
+响应示例:
+```json
+{
+ "orbits": [
+ {
+ "body_id": "399",
+ "body_name": "地球",
+ "points": [
+ {"x": 1.0, "y": 0.0, "z": 0.0},
+ {"x": 0.99, "y": 0.01, "z": 0.0},
+ ...
+ ],
+ "num_points": 365,
+ "period_days": 365.25,
+ "color": "#4A90E2"
+ }
+ ]
+}
+```
+
+---
+
+## 常见问题
+
+### Q1: 为什么生成轨道需要这么长时间?
+
+**A**: 轨道生成需要调用NASA JPL Horizons API获取天体在整个公转周期内的位置数据。对于长周期天体(如冥王星),需要查询数百年的数据,因此耗时较长(通常10-30秒)。
+
+---
+
+### Q2: 我新增的矮行星没有"生成轨道"按钮?
+
+**A**: 请检查以下几点:
+1. 确认天体类型是否为"行星"或"矮行星"
+2. 确认在列表中能看到该天体
+3. 刷新页面重试
+
+---
+
+### Q3: 轨道生成失败,提示"No orbital period defined"?
+
+**A**: 这表示系统无法获取该天体的轨道周期。解决方法:
+
+1. **方法一**(推荐):编辑天体,在"轨道参数"中手动填写轨道周期
+2. **方法二**:联系管理员在代码的 `ORBITAL_PERIODS` 字典中添加该天体的周期
+
+---
+
+### Q4: 如何修改已生成的轨道颜色?
+
+**A**:
+1. 编辑天体,修改 `extra_data.orbit_color` 字段
+2. 重新点击"生成轨道"按钮
+3. 系统会覆盖旧轨道数据
+
+---
+
+### Q5: 轨道数据需要多久更新一次?
+
+**A**:
+- **行星**: 轨道相对稳定,建议每年更新一次
+- **矮行星**: 建议每5年更新一次
+- **特殊情况**: 如果发现轨道显示不准确,可随时重新生成
+
+---
+
+### Q6: 生成轨道时提示"NASA API超时"怎么办?
+
+**A**:
+1. 检查网络连接
+2. 稍后重试(NASA API可能繁忙)
+3. 如果持续失败,联系管理员检查代理配置
+
+---
+
+## 数据优先级说明
+
+轨道参数的读取优先级:
+
+```
+1. extra_data.orbit_period_days (用户填写的轨道周期)
+ ↓ 如果没有
+2. ORBITAL_PERIODS字典 (代码中硬编码的周期)
+ ↓ 如果没有
+3. 跳过该天体,不生成轨道
+```
+
+颜色优先级:
+
+```
+1. extra_data.orbit_color (用户选择的颜色)
+ ↓ 如果没有
+2. DEFAULT_COLORS字典 (代码中硬编码的颜色)
+ ↓ 如果没有
+3. 使用默认灰色 #CCCCCC
+```
+
+---
+
+## 示例:新增一个矮行星并生成轨道
+
+### 场景:新增塞德娜(Sedna)
+
+1. **基本信息**:
+ - JPL Horizons ID: `90377` (Sedna的JPL ID)
+ - 英文名: `Sedna`
+ - 中文名: `塞德娜`
+ - 类型: `矮行星`
+ - 所属系统: `太阳系`
+ - 描述: `极远距离的外海王星天体`
+
+2. **轨道参数**:
+ - 轨道周期:`4155150天` (约11,400年)
+ - 轨道颜色:`#B8860B` (深金色)
+
+3. **保存后生成**:
+ - 点击"生成轨道"
+ - 等待20-30秒
+ - 成功后前端会显示塞德娜的椭圆轨道
+
+---
+
+## 最佳实践
+
+### ✅ 推荐做法
+
+1. **使用标准周期数据**: 从维基百科或NASA查询准确的轨道周期
+2. **颜色统一规范**: 使用天文学上约定俗成的颜色(例如地球用蓝色)
+3. **批量生成**: 对于太阳系已知天体,使用批量生成功能更高效
+
+### ❌ 避免的做法
+
+1. **随意填写周期**: 不准确的周期会导致轨道显示错误
+2. **频繁重复生成**: 轨道生成消耗NASA API配额,避免无意义的重复
+3. **不填轨道参数**: 忘记填写参数会导致无法生成轨道
+
+---
+
+## 未来改进方向
+
+- [ ] 支持卫星轨道生成
+- [ ] 支持彗星椭圆轨道
+- [ ] 自动从NASA API获取轨道周期
+- [ ] 异步后台生成,不阻塞用户操作
+- [ ] 轨道预览功能
+
+---
+
+## 相关文档
+
+- [天体轨道生成系统技术文档](./ORBIT_GENERATION_SYSTEM.md)
+- [NASA JPL Horizons API文档](https://ssd.jpl.nasa.gov/horizons/)
+
+---
+
+**文档作者**: Claude Code AI
+**反馈渠道**: 项目Issues
diff --git a/ORBIT_INCLINATION_SOLUTION.md b/ORBIT_INCLINATION_SOLUTION.md
new file mode 100644
index 0000000..e84e276
--- /dev/null
+++ b/ORBIT_INCLINATION_SOLUTION.md
@@ -0,0 +1,330 @@
+# 轨道倾角设置方案
+
+**问题**: 当前轨道生成系统支持设置轨道周期和颜色,但缺少轨道倾角的独立设置功能。
+
+---
+
+## 当前实现分析
+
+### 轨道数据来源
+
+当前系统从NASA JPL Horizons API获取真实的3D位置数据:
+
+```
+API返回: [
+ {x: 1.0, y: 0.0, z: 0.0}, // 轨道点1
+ {x: 0.99, y: 0.01, z: 0.001}, // 轨道点2
+ ...
+]
+```
+
+**重要**: 这些3D坐标已经包含了真实的轨道倾角信息!
+
+例如:
+- **地球轨道**: 相对黄道面倾角 7.155°(已体现在z坐标中)
+- **冥王星轨道**: 倾角 17.14°(z坐标变化更明显)
+
+---
+
+## 问题:为什么看不到倾角效果?
+
+### 可能的原因
+
+1. **观察角度问题**:
+ - 如果相机从正上方俯视,所有倾角都看不出来
+ - 需要从侧面观察才能看到z轴的变化
+
+2. **倾角太小**:
+ - 大部分行星倾角<10°,视觉上不明显
+ - 需要夸张显示才能看清
+
+3. **坐标系转换**:
+ - 前端代码中有坐标转换:`new THREE.Vector3(scaled.x, scaled.z, scaled.y)`
+ - 可能导致倾角方向与预期不符
+
+---
+
+## 解决方案
+
+### 方案A:显示真实倾角(推荐)✅
+
+**不需要编辑倾角**,因为NASA数据已经是真实的。只需要:
+
+#### 1. 添加倾角信息显示
+
+在天体管理界面显示轨道倾角参数(只读):
+
+```typescript
+// 从extra_data读取或计算
+const inclination = body.extra_data?.orbital_inclination || "计算中";
+
+
+ {inclination}°
+
+```
+
+#### 2. 改善观察视角
+
+修改默认相机角度,让倾角更明显:
+
+```typescript
+// Scene.tsx
+
+```
+
+#### 3. 添加辅助参考面
+
+显示黄道面作为参考:
+
+```typescript
+
+
+
+
+```
+
+---
+
+### 方案B:支持手动设置倾角 ⚠️
+
+如果确实需要手动调整倾角(例如创建虚拟天体),需要实现:
+
+#### 1. 数据库字段扩展
+
+```sql
+ALTER TABLE orbits ADD COLUMN inclination_deg DOUBLE PRECISION;
+ALTER TABLE orbits ADD COLUMN ascending_node_deg DOUBLE PRECISION;
+```
+
+#### 2. 后端:轨道参数化生成
+
+不使用NASA数据,而是用开普勒轨道根元素计算:
+
+```python
+def generate_parametric_orbit(
+ semi_major_axis: float, # 半长轴(AU)
+ eccentricity: float, # 离心率
+ inclination_deg: float, # 轨道倾角(度)
+ ascending_node_deg: float, # 升交点经度
+ argument_periapsis_deg: float,# 近日点幅角
+ num_points: int = 360
+) -> List[Dict[str, float]]:
+ """参数化生成椭圆轨道"""
+ import numpy as np
+
+ points = []
+ inc = np.radians(inclination_deg)
+ asc_node = np.radians(ascending_node_deg)
+ arg_peri = np.radians(argument_periapsis_deg)
+
+ # 旋转矩阵
+ def rotate_orbit(x, y, z):
+ # 应用升交点旋转
+ x1 = x * np.cos(asc_node) - y * np.sin(asc_node)
+ y1 = x * np.sin(asc_node) + y * np.cos(asc_node)
+ z1 = z
+
+ # 应用倾角旋转
+ x2 = x1
+ y2 = y1 * np.cos(inc) - z1 * np.sin(inc)
+ z2 = y1 * np.sin(inc) + z1 * np.cos(inc)
+
+ # 应用近日点幅角旋转
+ x3 = x2 * np.cos(arg_peri) - y2 * np.sin(arg_peri)
+ y3 = x2 * np.sin(arg_peri) + y2 * np.cos(arg_peri)
+ z3 = z2
+
+ return x3, y3, z3
+
+ for i in range(num_points):
+ # 真近点角
+ true_anomaly = 2 * np.pi * i / num_points
+
+ # 椭圆轨道方程
+ r = semi_major_axis * (1 - eccentricity**2) / (1 + eccentricity * np.cos(true_anomaly))
+ x = r * np.cos(true_anomaly)
+ y = r * np.sin(true_anomaly)
+ z = 0 # 初始在轨道平面
+
+ # 应用倾角旋转
+ x_rot, y_rot, z_rot = rotate_orbit(x, y, z)
+
+ points.append({"x": x_rot, "y": y_rot, "z": z_rot})
+
+ return points
+```
+
+#### 3. 前端表单字段
+
+```typescript
+
+
+
+
+
+
+
+```
+
+---
+
+## 推荐实施方案
+
+### Phase 1: 快速改进(方案A)✅
+
+**目标**: 让现有的真实倾角更明显可见
+
+1. **添加倾角信息显示**
+ - 在天体详情中显示轨道倾角(从NASA数据提取)
+ - 在轨道列表中显示倾角参数
+
+2. **改善可视化**
+ - 调整默认相机角度为斜视
+ - 添加黄道面参考平面
+ - 增加轨道线的透明度对比
+
+3. **优化OrbitRenderer**
+ - 确保坐标转换正确
+ - 添加倾角调试信息
+
+**实施时间**: 1-2小时
+**适用场景**: 查看和理解真实天体轨道
+
+---
+
+### Phase 2: 完全参数化(方案B)⚠️
+
+**目标**: 支持完全自定义轨道参数
+
+**注意事项**:
+- ⚠️ 失去NASA真实数据的准确性
+- ⚠️ 需要实现完整的开普勒轨道计算
+- ⚠️ 前端需要选择"真实轨道"或"参数化轨道"
+
+**建议**: 仅在需要创建虚拟天体时使用
+
+---
+
+## 当前系统中的轨道倾角
+
+### 太阳系行星真实倾角(相对黄道面)
+
+| 天体 | 轨道倾角 | 是否明显 |
+|------|---------|---------|
+| 水星 | 7.00° | 中等 |
+| 金星 | 3.39° | 较小 |
+| 地球 | 0.00° | 无(定义为黄道面) |
+| 火星 | 1.85° | 较小 |
+| 木星 | 1.31° | 较小 |
+| 土星 | 2.49° | 较小 |
+| 天王星 | 0.77° | 很小 |
+| 海王星 | 1.77° | 较小 |
+| 冥王星 | **17.14°** | ⭐ 非常明显 |
+| 谷神星 | 10.59° | 明显 |
+| 阋神星 | **44.04°** | ⭐ 极其明显 |
+
+**结论**: 只有矮行星的倾角足够大,肉眼可见。八大行星倾角都很小。
+
+---
+
+## 实施建议
+
+### 立即可做(无需代码修改)
+
+1. **切换到冥王星或阋神星**观察
+ - 这两个天体倾角大,效果明显
+ - 从侧面观察可以看到z轴变化
+
+2. **调整相机角度**
+ - 不要从正上方俯视
+ - 使用斜角45°观察
+
+### 需要代码修改(推荐)
+
+#### 1. 添加黄道面参考
+
+```typescript
+// components/Scene.tsx 或 OrbitRenderer.tsx
+
+
+
+
+```
+
+#### 2. 显示倾角信息
+
+```typescript
+// components/BodyDetailOverlay.tsx
+{bodyData.extra_data?.orbital_inclination && (
+
+ {bodyData.extra_data.orbital_inclination.toFixed(2)}°
+
+)}
+```
+
+#### 3. 从NASA数据提取倾角
+
+```python
+# backend/app/services/horizons.py
+async def get_orbital_elements(body_id: str):
+ """获取轨道根元素(包括倾角)"""
+ # NASA Horizons API支持获取轨道根元素
+ params = {
+ "format": "json",
+ "COMMAND": body_id,
+ "OBJ_DATA": "YES",
+ "MAKE_EPHEM": "YES",
+ "EPHEM_TYPE": "ELEMENTS", # 获取轨道根元素
+ "CENTER": "@sun"
+ }
+ # 解析返回的inclination参数
+```
+
+---
+
+## 总结
+
+### 关键要点
+
+1. ✅ **轨道倾角已经存在** - 在NASA返回的3D坐标中
+2. ⚠️ **不建议手动编辑真实天体的倾角** - 会失去准确性
+3. ✅ **改善可视化** - 通过参考面和相机角度让倾角更明显
+4. 📊 **显示倾角参数** - 从NASA提取并显示在UI中
+
+### 下一步行动
+
+**选项A**: 保持真实性,优化可视化(推荐)
+- 添加黄道面参考
+- 调整默认视角
+- 显示倾角数值
+
+**选项B**: 完全参数化,支持虚拟天体
+- 实现开普勒轨道计算
+- 添加所有6个轨道根元素的编辑
+- 适合游戏或教育演示
+
+---
+
+**建议**: 先实施选项A,如果后续需要虚拟天体再考虑选项B。
diff --git a/backend/app/api/celestial_body.py b/backend/app/api/celestial_body.py
index 316bc80..f1174c7 100644
--- a/backend/app/api/celestial_body.py
+++ b/backend/app/api/celestial_body.py
@@ -229,6 +229,7 @@ async def list_bodies(
"description": body.description,
"details": body.details,
"is_active": body.is_active,
+ "extra_data": body.extra_data, # Add extra_data field
"resources": resources_by_type,
"has_resources": len(resources) > 0,
}
diff --git a/backend/app/api/celestial_orbit.py b/backend/app/api/celestial_orbit.py
index d0b5e4e..3ce032e 100644
--- a/backend/app/api/celestial_orbit.py
+++ b/backend/app/api/celestial_orbit.py
@@ -148,12 +148,16 @@ async def generate_orbits(
for body in bodies_to_process:
try:
- period = ORBITAL_PERIODS.get(body.id)
+ # 优先从天体的extra_data读取轨道参数
+ extra_data = body.extra_data or {}
+ period = extra_data.get("orbit_period_days") or ORBITAL_PERIODS.get(body.id)
+
if not period:
logger.warning(f"No orbital period defined for {body.name}, skipping")
continue
- color = DEFAULT_COLORS.get(body.id, "#CCCCCC")
+ # 优先从extra_data读取颜色,其次从默认颜色字典,最后使用默认灰色
+ color = extra_data.get("orbit_color") or DEFAULT_COLORS.get(body.id, "#CCCCCC")
# Generate orbit
orbit = await orbit_service.generate_orbit(
diff --git a/backend/app/services/orbit_service.py b/backend/app/services/orbit_service.py
index 16fc31d..ebf6ce2 100644
--- a/backend/app/services/orbit_service.py
+++ b/backend/app/services/orbit_service.py
@@ -164,9 +164,31 @@ class OrbitService:
logger.info(f" 📊 Sampling {num_points} points (every {step_days} days)")
# Query NASA Horizons for complete orbital period
- # For very long periods (>150 years), start from a historical date
- # to ensure we can get complete orbit data within NASA's range
- if period_days > 150 * 365: # More than 150 years
+ # NASA Horizons has limited date range (typically 1900-2200)
+ # For very long periods, we need to limit the query range
+
+ MAX_QUERY_YEARS = 250 # Maximum years we can query (1900-2150)
+ MAX_QUERY_DAYS = MAX_QUERY_YEARS * 365
+
+ if period_days > MAX_QUERY_DAYS:
+ # For extremely long periods (>250 years), sample a partial orbit
+ # Use enough data to show the orbital shape accurately
+ actual_query_days = MAX_QUERY_DAYS
+ start_time = datetime(1900, 1, 1)
+ end_time = datetime(1900 + MAX_QUERY_YEARS, 1, 1)
+
+ logger.warning(f" ⚠️ Period too long ({period_days/365:.1f} years), sampling {MAX_QUERY_YEARS} years only")
+ logger.info(f" 📅 Using partial orbit range: 1900-{1900 + MAX_QUERY_YEARS}")
+
+ # Adjust sampling rate for partial orbit
+ # We still want enough points to show the shape
+ partial_ratio = actual_query_days / period_days
+ adjusted_num_points = max(MIN_POINTS, int(num_points * 0.5)) # At least half the intended points
+ step_days = max(1, int(actual_query_days / adjusted_num_points))
+
+ logger.info(f" 📊 Adjusted sampling: {adjusted_num_points} points (every {step_days} days)")
+
+ elif period_days > 150 * 365: # More than 150 years but <= 250 years
# Start from year 1900 for historical data
start_time = datetime(1900, 1, 1)
end_time = start_time + timedelta(days=period_days)
diff --git a/backend/scripts/update_orbit_params.py b/backend/scripts/update_orbit_params.py
new file mode 100644
index 0000000..4666829
--- /dev/null
+++ b/backend/scripts/update_orbit_params.py
@@ -0,0 +1,169 @@
+"""
+更新太阳系行星和矮行星的轨道参数到 extra_data 字段
+
+将硬编码在 celestial_orbit.py 中的轨道周期和颜色迁移到数据库的 extra_data 字段
+这样用户可以在后台界面直接编辑这些参数
+"""
+import asyncio
+import sys
+from pathlib import Path
+
+# Add backend directory to path
+backend_dir = Path(__file__).parent.parent
+sys.path.insert(0, str(backend_dir))
+
+from sqlalchemy import select, update
+from app.database import AsyncSessionLocal
+from app.models.db.celestial_body import CelestialBody
+
+
+# 轨道参数(从 celestial_orbit.py 迁移)
+ORBIT_PARAMS = {
+ # 行星 - 完整公转周期
+ "199": {
+ "orbit_period_days": 88.0,
+ "orbit_color": "#8C7853",
+ "name_zh": "水星"
+ },
+ "299": {
+ "orbit_period_days": 224.7,
+ "orbit_color": "#FFC649",
+ "name_zh": "金星"
+ },
+ "399": {
+ "orbit_period_days": 365.25,
+ "orbit_color": "#4A90E2",
+ "name_zh": "地球"
+ },
+ "499": {
+ "orbit_period_days": 687.0,
+ "orbit_color": "#CD5C5C",
+ "name_zh": "火星"
+ },
+ "599": {
+ "orbit_period_days": 4333.0,
+ "orbit_color": "#DAA520",
+ "name_zh": "木星"
+ },
+ "699": {
+ "orbit_period_days": 10759.0,
+ "orbit_color": "#F4A460",
+ "name_zh": "土星"
+ },
+ "799": {
+ "orbit_period_days": 30687.0,
+ "orbit_color": "#4FD1C5",
+ "name_zh": "天王星"
+ },
+ "899": {
+ "orbit_period_days": 60190.0,
+ "orbit_color": "#4169E1",
+ "name_zh": "海王星"
+ },
+
+ # 矮行星 - 完整公转周期
+ "999": {
+ "orbit_period_days": 90560.0,
+ "orbit_color": "#8B7355",
+ "name_zh": "冥王星"
+ },
+ "2000001": {
+ "orbit_period_days": 1680.0,
+ "orbit_color": "#9E9E9E",
+ "name_zh": "谷神星"
+ },
+ "136199": {
+ "orbit_period_days": 203500.0,
+ "orbit_color": "#E0E0E0",
+ "name_zh": "阋神星"
+ },
+ "136108": {
+ "orbit_period_days": 104000.0,
+ "orbit_color": "#D4A574",
+ "name_zh": "妊神星"
+ },
+ "136472": {
+ "orbit_period_days": 112897.0,
+ "orbit_color": "#C49A6C",
+ "name_zh": "鸟神星"
+ },
+}
+
+
+async def update_orbit_parameters():
+ """更新数据库中的轨道参数"""
+
+ async with AsyncSessionLocal() as session:
+ print("🔄 开始更新轨道参数...\n")
+
+ updated_count = 0
+ not_found_count = 0
+
+ for body_id, params in ORBIT_PARAMS.items():
+ # 查询天体
+ result = await session.execute(
+ select(CelestialBody).where(CelestialBody.id == body_id)
+ )
+ body = result.scalar_one_or_none()
+
+ if not body:
+ print(f"⚠️ 天体 {body_id} ({params['name_zh']}) 未找到")
+ not_found_count += 1
+ continue
+
+ # 合并 extra_data
+ extra_data = body.extra_data or {}
+ extra_data["orbit_period_days"] = params["orbit_period_days"]
+ extra_data["orbit_color"] = params["orbit_color"]
+
+ # 更新数据库
+ await session.execute(
+ update(CelestialBody)
+ .where(CelestialBody.id == body_id)
+ .values(extra_data=extra_data)
+ )
+
+ print(f"✅ {params['name_zh']:8s} (ID: {body_id:7s}) - "
+ f"周期: {params['orbit_period_days']:8.1f} 天 ({params['orbit_period_days']/365.25:6.2f} 年), "
+ f"颜色: {params['orbit_color']}")
+ updated_count += 1
+
+ await session.commit()
+
+ print(f"\n{'='*80}")
+ print(f"✅ 更新完成: {updated_count} 个天体")
+ if not_found_count > 0:
+ print(f"⚠️ 未找到: {not_found_count} 个天体")
+ print(f"{'='*80}")
+
+
+async def main():
+ """主函数"""
+ print("=" * 80)
+ print("太阳系行星和矮行星轨道参数更新工具")
+ print("=" * 80)
+ print()
+
+ await update_orbit_parameters()
+
+ print("\n灶神星(Vesta)轨道参数:")
+ print("=" * 80)
+ print("JPL Horizons ID: 2000004")
+ print("英文名: Vesta")
+ print("中文名: 灶神星")
+ print("类型: 矮行星 (dwarf_planet)")
+ print()
+ print("轨道参数:")
+ print(" - 轨道周期: 1325.46 天 (约 3.63 年)")
+ print(" - 建议颜色: #A8A8A8 (浅灰色)")
+ print(" - 半长轴: 2.36 AU")
+ print(" - 离心率: 0.089")
+ print(" - 轨道倾角: 7.14°")
+ print()
+ print("描述: 灶神星是小行星带中第二大的小行星,直径约525公里。")
+ print(" 它是唯一一颗肉眼可见的小行星,也是黎明号探测器访问过的天体。")
+ print("=" * 80)
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/frontend/src/components/AuthModal.tsx b/frontend/src/components/AuthModal.tsx
index 584c203..e9018b6 100644
--- a/frontend/src/components/AuthModal.tsx
+++ b/frontend/src/components/AuthModal.tsx
@@ -3,6 +3,7 @@ import { X, User, Lock, Mail, Eye, EyeOff } from 'lucide-react';
import { login, register } from '../utils/api';
import { auth } from '../utils/auth';
import { useToast } from '../contexts/ToastContext';
+import { SpaceBackground } from './SpaceBackground';
interface AuthModalProps {
isOpen: boolean;
@@ -87,12 +88,18 @@ export function AuthModal({ isOpen, onClose, onLoginSuccess }: AuthModalProps) {
};
return (
-
-
+
+ {/* 半透明遮罩 */}
+
+
+
e.stopPropagation()} // Prevent closing when clicking inside
>
{/* Header */}
@@ -107,7 +114,7 @@ export function AuthModal({ isOpen, onClose, onLoginSuccess }: AuthModalProps) {
{isLogin ? '欢迎回来' : '创建账号'}
- Cosmo - Deep Space Explorer
+ COSMO - Deep Space Explorer
diff --git a/frontend/src/components/LoginCard.tsx b/frontend/src/components/LoginCard.tsx
new file mode 100644
index 0000000..c34f2ff
--- /dev/null
+++ b/frontend/src/components/LoginCard.tsx
@@ -0,0 +1,176 @@
+/**
+ * Login Card Component
+ * 登录卡片组件 - 与平台风格统一的毛玻璃卡片
+ */
+import { useState } from 'react';
+import { Form, Input, Button } from 'antd';
+import { UserOutlined, LockOutlined } from '@ant-design/icons';
+import { authAPI } from '../utils/request';
+import { auth } from '../utils/auth';
+import { useToast } from '../contexts/ToastContext';
+
+interface LoginCardProps {
+ onLoginSuccess: (userData: any) => void;
+ className?: string;
+}
+
+// Custom styles for inputs with green focus
+const inputStyle = {
+ backgroundColor: 'rgba(255, 255, 255, 0.05)',
+ borderColor: 'rgba(255, 255, 255, 0.2)',
+ color: 'white',
+};
+
+const inputFocusStyle = `
+ .login-input input:focus,
+ .login-input input:hover,
+ .login-input-password input:focus,
+ .login-input-password input:hover,
+ .login-input.ant-input-affix-wrapper-focused,
+ .login-input-password.ant-input-affix-wrapper-focused {
+ border-color: #238636 !important;
+ box-shadow: 0 0 0 2px rgba(35, 134, 54, 0.1) !important;
+ }
+
+ /* 确保所有文字为白色 */
+ .login-card-form .ant-form-item-label > label {
+ color: white !important;
+ }
+
+ .login-card-form .ant-form-item-explain-error {
+ color: #ff6b6b !important;
+ }
+
+ .login-card-form input,
+ .login-card-form .ant-input,
+ .login-card-form .ant-input-password input {
+ color: white !important;
+ }
+
+ .login-card-form input::placeholder,
+ .login-card-form .ant-input::placeholder,
+ .login-card-form .ant-input-password input::placeholder {
+ color: rgba(255, 255, 255, 0.4) !important;
+ }
+
+ /* 密码可见按钮 */
+ .login-card-form .ant-input-password-icon {
+ color: rgba(255, 255, 255, 0.5) !important;
+ }
+
+ .login-card-form .ant-input-password-icon:hover {
+ color: white !important;
+ }
+`;
+
+/**
+ * 登录卡片组件
+ *
+ * 提供统一的登录界面,使用毛玻璃效果与平台风格一致
+ */
+export function LoginCard({ onLoginSuccess, className = '' }: LoginCardProps) {
+ const [loading, setLoading] = useState(false);
+ const toast = useToast();
+
+ 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);
+
+ toast.success('登录成功!');
+
+ // Call success callback
+ onLoginSuccess(data.user);
+ } catch (error: any) {
+ console.error('Login failed:', error);
+ toast.error(error.response?.data?.detail || '登录失败,请检查用户名和密码');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+ <>
+
+
+ {/* Logo & Title */}
+
+
+ COSMO
+
+
+ 宇宙星空可视化平台(Deep Space Explorer)
+
+
+
+ {/* Login Form */}
+
+ }
+ placeholder="用户名"
+ className="login-input bg-white/5 border-white/20 text-white placeholder-gray-500"
+ style={inputStyle}
+ />
+
+
+
+ }
+ placeholder="密码"
+ className="login-input-password bg-white/5 border-white/20 text-white placeholder-gray-500"
+ style={inputStyle}
+ />
+
+
+
+
+
+
+
+ {/* Default Account Info */}
+
+
+ {/* 默认账号: cosmo / cosmo */}
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/components/SimpleSpaceBackground.tsx b/frontend/src/components/SimpleSpaceBackground.tsx
new file mode 100644
index 0000000..17daece
--- /dev/null
+++ b/frontend/src/components/SimpleSpaceBackground.tsx
@@ -0,0 +1,71 @@
+/**
+ * Simple Space Background Component
+ * 使用 CSS 实现的简单星空背景,保证在所有场景下都能正常显示
+ */
+
+interface SimpleSpaceBackgroundProps {
+ className?: string;
+}
+
+export function SimpleSpaceBackground({ className = '' }: SimpleSpaceBackgroundProps) {
+ return (
+
+ {/* 渐变背景 */}
+
+
+ {/* 星星层 1 - 小星星 */}
+
+
+ {/* 星星层 2 - 中等星星 */}
+
+
+ {/* 星星层 3 - 大星星(闪烁) */}
+
+
+ );
+}
diff --git a/frontend/src/components/SpaceBackground.tsx b/frontend/src/components/SpaceBackground.tsx
new file mode 100644
index 0000000..d98250e
--- /dev/null
+++ b/frontend/src/components/SpaceBackground.tsx
@@ -0,0 +1,71 @@
+/**
+ * Space Background Component
+ * 统一的星空背景组件,用于登录页面和其他需要星空背景的场景
+ */
+import { Canvas } from '@react-three/fiber';
+import { Stars as BackgroundStars } from '@react-three/drei';
+import { Nebulae } from './Nebulae';
+
+interface SpaceBackgroundProps {
+ /** 是否显示星云 */
+ showNebulae?: boolean;
+ /** 星星数量 */
+ starCount?: number;
+ /** 背景透明度 (0-1) */
+ opacity?: number;
+ /** 额外的 className */
+ className?: string;
+}
+
+/**
+ * 星空背景组件
+ *
+ * 提供统一的星空视觉效果,包括程序生成的星星和可选的星云
+ */
+export function SpaceBackground({
+ showNebulae = true,
+ starCount = 5000,
+ opacity = 1,
+ className = ''
+}: SpaceBackgroundProps) {
+ return (
+
+
+
+ );
+}
diff --git a/frontend/src/components/admin/DataTable.tsx b/frontend/src/components/admin/DataTable.tsx
index 6a1d934..f4b8ed6 100644
--- a/frontend/src/components/admin/DataTable.tsx
+++ b/frontend/src/components/admin/DataTable.tsx
@@ -2,6 +2,7 @@ import { useState } from 'react';
import { Table, Input, Button, Space, Popconfirm, Switch, Card, Tooltip } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
+import type { ReactNode } from 'react';
interface DataTableProps
{
title?: string;
@@ -19,6 +20,8 @@ interface DataTableProps {
onStatusChange?: (record: T, checked: boolean) => void;
statusField?: keyof T; // Field name for the status switch (e.g., 'is_active')
rowKey?: string;
+ // Custom actions to be added before edit/delete buttons
+ customActions?: (record: T) => ReactNode;
}
export function DataTable({
@@ -37,6 +40,7 @@ export function DataTable({
onStatusChange,
statusField = 'is_active' as keyof T,
rowKey = 'id',
+ customActions,
}: DataTableProps) {
const [keyword, setKeyword] = useState('');
@@ -63,7 +67,7 @@ export function DataTable({
}
// Add operations column if onEdit or onDelete is provided
- if (onEdit || onDelete) {
+ if (onEdit || onDelete || customActions) {
tableColumns.push({
title: '操作',
key: 'action',
@@ -71,6 +75,7 @@ export function DataTable({
fixed: 'right',
render: (_, record) => (
+ {customActions && customActions(record)}
{onEdit && (