修复了轨道生成问题
parent
1c1f118c91
commit
d10dea21a8
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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` (已安装)
|
||||
|
||||
---
|
||||
|
||||
**完成状态**: ✅ 所有改造已完成并测试通过
|
||||
|
|
@ -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) - 初始版本,基于现有代码分析
|
||||
|
|
@ -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
|
||||
|
|
@ -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 || "计算中";
|
||||
|
||||
<Descriptions.Item label="轨道倾角">
|
||||
{inclination}°
|
||||
</Descriptions.Item>
|
||||
```
|
||||
|
||||
#### 2. 改善观察视角
|
||||
|
||||
修改默认相机角度,让倾角更明显:
|
||||
|
||||
```typescript
|
||||
// Scene.tsx
|
||||
<PerspectiveCamera
|
||||
makeDefault
|
||||
position={[300, 150, 300]} // 从斜上方观察
|
||||
fov={60}
|
||||
/>
|
||||
```
|
||||
|
||||
#### 3. 添加辅助参考面
|
||||
|
||||
显示黄道面作为参考:
|
||||
|
||||
```typescript
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]}>
|
||||
<planeGeometry args={[1000, 1000]} />
|
||||
<meshBasicMaterial
|
||||
color="#333333"
|
||||
transparent
|
||||
opacity={0.1}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</mesh>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 方案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
|
||||
<Form.Item
|
||||
name={['extra_data', 'orbital_inclination']}
|
||||
label="轨道倾角(度)"
|
||||
tooltip="轨道平面相对黄道面的倾斜角度"
|
||||
>
|
||||
<InputNumber min={0} max={180} step={0.1} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={['extra_data', 'orbital_eccentricity']}
|
||||
label="轨道离心率"
|
||||
tooltip="0=圆形,0-1=椭圆,1=抛物线"
|
||||
>
|
||||
<InputNumber min={0} max={0.99} step={0.01} />
|
||||
</Form.Item>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 推荐实施方案
|
||||
|
||||
### 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
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]}>
|
||||
<circleGeometry args={[500, 64]} />
|
||||
<meshBasicMaterial
|
||||
color="#1a1a1a"
|
||||
transparent
|
||||
opacity={0.2}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</mesh>
|
||||
```
|
||||
|
||||
#### 2. 显示倾角信息
|
||||
|
||||
```typescript
|
||||
// components/BodyDetailOverlay.tsx
|
||||
{bodyData.extra_data?.orbital_inclination && (
|
||||
<Descriptions.Item label="轨道倾角">
|
||||
{bodyData.extra_data.orbital_inclination.toFixed(2)}°
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
```
|
||||
|
||||
#### 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。
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
@ -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 (
|
||||
<div
|
||||
className="fixed inset-0 z-[60] flex items-center justify-center bg-black/90 backdrop-blur-xl p-4"
|
||||
<div
|
||||
className="fixed inset-0 z-[60] flex items-center justify-center p-4"
|
||||
onClick={onClose} // Close modal when clicking outside
|
||||
>
|
||||
<div
|
||||
className="bg-gray-900/90 border border-white/10 rounded-2xl w-full max-w-md shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-200"
|
||||
{/* 星空背景 */}
|
||||
<SpaceBackground showNebulae={true} opacity={0.6} />
|
||||
|
||||
{/* 半透明遮罩 */}
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
||||
|
||||
<div
|
||||
className="relative bg-black/40 backdrop-blur-md border border-[#238636] rounded-2xl w-full max-w-md shadow-2xl shadow-[#238636]/20 overflow-hidden animate-in fade-in zoom-in-95 duration-200"
|
||||
onClick={(e) => e.stopPropagation()} // Prevent closing when clicking inside
|
||||
>
|
||||
{/* Header */}
|
||||
|
|
@ -107,7 +114,7 @@ export function AuthModal({ isOpen, onClose, onLoginSuccess }: AuthModalProps) {
|
|||
{isLogin ? '欢迎回来' : '创建账号'}
|
||||
</h2>
|
||||
<p className="text-gray-400 text-sm text-center mt-1">
|
||||
Cosmo - Deep Space Explorer
|
||||
COSMO - Deep Space Explorer
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
<style>{inputFocusStyle}</style>
|
||||
<div
|
||||
className={`
|
||||
w-full max-w-md
|
||||
bg-black/40 backdrop-blur-md
|
||||
border border-[#238636]
|
||||
rounded-2xl
|
||||
p-8
|
||||
shadow-2xl shadow-[#238636]/20
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
{/* Logo & Title */}
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-white mb-2">
|
||||
COSMO
|
||||
</h1>
|
||||
<p className="text-white/70 text-sm">
|
||||
宇宙星空可视化平台(Deep Space Explorer)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Login Form */}
|
||||
<Form
|
||||
name="login"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
size="large"
|
||||
layout="vertical"
|
||||
className="login-card-form"
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[{ required: true, message: '请输入用户名' }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined className="text-gray-400" />}
|
||||
placeholder="用户名"
|
||||
className="login-input bg-white/5 border-white/20 text-white placeholder-gray-500"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{ required: true, message: '请输入密码' }]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined className="text-gray-400" />}
|
||||
placeholder="密码"
|
||||
className="login-input-password bg-white/5 border-white/20 text-white placeholder-gray-500"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item className="mb-0">
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
block
|
||||
size="large"
|
||||
className="h-12 text-base font-medium bg-[#238636] hover:bg-[#2ea043] border-none"
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{/* Default Account Info */}
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-gray-500 text-xs">
|
||||
{/* 默认账号: <span className="text-gray-400">cosmo / cosmo</span> */}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* Simple Space Background Component
|
||||
* 使用 CSS 实现的简单星空背景,保证在所有场景下都能正常显示
|
||||
*/
|
||||
|
||||
interface SimpleSpaceBackgroundProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SimpleSpaceBackground({ className = '' }: SimpleSpaceBackgroundProps) {
|
||||
return (
|
||||
<div className={`absolute inset-0 ${className}`} style={{ zIndex: 0 }}>
|
||||
{/* 渐变背景 */}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
background: 'radial-gradient(ellipse at bottom, #1B2735 0%, #090A0F 100%)',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 星星层 1 - 小星星 */}
|
||||
<div
|
||||
className="absolute inset-0 animate-twinkle"
|
||||
style={{
|
||||
background: `
|
||||
radial-gradient(1px 1px at 20% 30%, white, transparent),
|
||||
radial-gradient(1px 1px at 60% 70%, white, transparent),
|
||||
radial-gradient(1px 1px at 50% 50%, white, transparent),
|
||||
radial-gradient(1px 1px at 80% 10%, white, transparent),
|
||||
radial-gradient(1px 1px at 90% 60%, white, transparent),
|
||||
radial-gradient(1px 1px at 33% 80%, white, transparent),
|
||||
radial-gradient(1px 1px at 15% 55%, white, transparent)
|
||||
`,
|
||||
backgroundSize: '200px 200px, 300px 300px, 250px 250px, 400px 400px, 350px 350px, 300px 300px, 450px 450px',
|
||||
opacity: 0.8,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 星星层 2 - 中等星星 */}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
background: `
|
||||
radial-gradient(2px 2px at 40% 20%, rgba(255, 255, 255, 0.9), transparent),
|
||||
radial-gradient(2px 2px at 70% 50%, rgba(255, 255, 255, 0.9), transparent),
|
||||
radial-gradient(2px 2px at 25% 65%, rgba(255, 255, 255, 0.9), transparent),
|
||||
radial-gradient(2px 2px at 85% 40%, rgba(255, 255, 255, 0.9), transparent)
|
||||
`,
|
||||
backgroundSize: '300px 300px, 400px 400px, 350px 350px, 450px 450px',
|
||||
opacity: 0.7,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 星星层 3 - 大星星(闪烁) */}
|
||||
<div
|
||||
className="absolute inset-0 animate-pulse"
|
||||
style={{
|
||||
background: `
|
||||
radial-gradient(3px 3px at 55% 35%, rgba(255, 255, 255, 1), transparent),
|
||||
radial-gradient(3px 3px at 10% 45%, rgba(255, 255, 255, 1), transparent),
|
||||
radial-gradient(3px 3px at 75% 75%, rgba(255, 255, 255, 1), transparent),
|
||||
radial-gradient(3px 3px at 95% 25%, rgba(255, 255, 255, 1), transparent)
|
||||
`,
|
||||
backgroundSize: '500px 500px, 450px 450px, 400px 400px, 550px 550px',
|
||||
opacity: 0.6,
|
||||
animationDuration: '3s',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div
|
||||
className={`absolute inset-0 ${className}`}
|
||||
style={{ opacity, zIndex: 0 }}
|
||||
>
|
||||
<Canvas
|
||||
camera={{
|
||||
position: [0, 0, 5],
|
||||
fov: 75,
|
||||
far: 1000,
|
||||
}}
|
||||
gl={{
|
||||
alpha: false,
|
||||
antialias: true,
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: '#000000'
|
||||
}}
|
||||
>
|
||||
{/* 微弱的环境光 */}
|
||||
<ambientLight intensity={0.2} />
|
||||
|
||||
{/* 程序生成的星星背景 */}
|
||||
<BackgroundStars
|
||||
radius={100}
|
||||
depth={50}
|
||||
count={starCount}
|
||||
factor={4}
|
||||
saturation={0}
|
||||
fade={true}
|
||||
speed={0.5}
|
||||
/>
|
||||
|
||||
{/* 星云(可选) */}
|
||||
{showNebulae && <Nebulae />}
|
||||
</Canvas>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<T> {
|
||||
title?: string;
|
||||
|
|
@ -19,6 +20,8 @@ interface DataTableProps<T> {
|
|||
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<T extends object>({
|
||||
|
|
@ -37,6 +40,7 @@ export function DataTable<T extends object>({
|
|||
onStatusChange,
|
||||
statusField = 'is_active' as keyof T,
|
||||
rowKey = 'id',
|
||||
customActions,
|
||||
}: DataTableProps<T>) {
|
||||
const [keyword, setKeyword] = useState('');
|
||||
|
||||
|
|
@ -63,7 +67,7 @@ export function DataTable<T extends object>({
|
|||
}
|
||||
|
||||
// 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<T extends object>({
|
|||
fixed: 'right',
|
||||
render: (_, record) => (
|
||||
<Space size="middle">
|
||||
{customActions && customActions(record)}
|
||||
{onEdit && (
|
||||
<Tooltip title="编辑">
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -1,101 +1,38 @@
|
|||
/**
|
||||
* Login Page
|
||||
* 使用统一的星空背景和组件化的登录卡片
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Form, Input, Button, Card } from 'antd';
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons';
|
||||
import { authAPI } from '../utils/request';
|
||||
import { auth } from '../utils/auth';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { SimpleSpaceBackground } from '../components/SimpleSpaceBackground';
|
||||
import { LoginCard } from '../components/LoginCard';
|
||||
|
||||
export function Login() {
|
||||
const navigate = useNavigate();
|
||||
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('登录成功!');
|
||||
|
||||
// Navigate to admin dashboard
|
||||
navigate('/admin');
|
||||
} catch (error: any) {
|
||||
console.error('Login failed:', error);
|
||||
toast.error(error.response?.data?.detail || '登录失败,请检查用户名和密码');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
const handleLoginSuccess = () => {
|
||||
// Navigate to admin dashboard
|
||||
navigate('/admin');
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
}}>
|
||||
<Card
|
||||
style={{
|
||||
width: 400,
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: 'center', marginBottom: 24 }}>
|
||||
<h1 style={{ fontSize: 28, marginBottom: 8 }}>Cosmo 后台管理</h1>
|
||||
<p style={{ color: '#666' }}>宇宙星空可视化平台</p>
|
||||
</div>
|
||||
<div className="relative min-h-screen w-full overflow-hidden bg-black">
|
||||
{/* 星空背景 */}
|
||||
<SimpleSpaceBackground />
|
||||
|
||||
<Form
|
||||
name="login"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
size="large"
|
||||
{/* 登录卡片 - 居中显示 */}
|
||||
<div className="relative z-10 flex items-center justify-center min-h-screen p-4">
|
||||
<LoginCard onLoginSuccess={handleLoginSuccess} />
|
||||
</div>
|
||||
|
||||
{/* 底部返回首页链接 */}
|
||||
<div className="absolute bottom-8 left-0 right-0 z-10 text-center">
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="text-gray-400 hover:text-white transition-colors text-sm underline"
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[{ required: true, message: '请输入用户名' }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined />}
|
||||
placeholder="用户名"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{ required: true, message: '请输入密码' }]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder="密码"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
block
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<div style={{ textAlign: 'center', color: '#999', fontSize: 12 }}>
|
||||
<p>默认账号: cosmo / cosmo</p>
|
||||
</div>
|
||||
</Card>
|
||||
返回首页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { Modal, Form, Input, Select, Switch, InputNumber, Tag, Badge, Descriptions, Button, Space, Alert, Upload, Popconfirm, Row, Col, Tabs, Card } from 'antd';
|
||||
import { Modal, Form, Input, Select, Switch, InputNumber, Tag, Badge, Descriptions, Button, Space, Alert, Upload, Popconfirm, Row, Col, Tabs, Card, Tooltip } from 'antd';
|
||||
import { CheckCircleOutlined, CloseCircleOutlined, SearchOutlined, UploadOutlined, DeleteOutlined, StarOutlined } from '@ant-design/icons';
|
||||
import type { UploadFile } from 'antd/es/upload/interface';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
|
|
@ -22,6 +22,11 @@ interface CelestialBody {
|
|||
description: string;
|
||||
details?: string; // Added details field
|
||||
is_active: boolean;
|
||||
extra_data?: {
|
||||
orbit_period_days?: number;
|
||||
orbit_color?: string;
|
||||
[key: string]: any; // Allow other extra data
|
||||
};
|
||||
resources?: {
|
||||
[key: string]: Array<{
|
||||
id: number;
|
||||
|
|
@ -205,7 +210,26 @@ export function CelestialBodies() {
|
|||
// Edit handler
|
||||
const handleEdit = (record: CelestialBody) => {
|
||||
setEditingRecord(record);
|
||||
form.setFieldsValue(record);
|
||||
|
||||
// Parse extra_data if it's a string (from backend JSON field)
|
||||
let extraData = record.extra_data;
|
||||
if (typeof extraData === 'string') {
|
||||
try {
|
||||
extraData = JSON.parse(extraData);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse extra_data:', e);
|
||||
extraData = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Properly set form values including nested extra_data
|
||||
const formValues = {
|
||||
...record,
|
||||
extra_data: extraData || {}, // Ensure extra_data is an object
|
||||
};
|
||||
|
||||
form.setFieldsValue(formValues);
|
||||
|
||||
setActiveTabKey('basic'); // Reset to basic tab
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
|
@ -297,7 +321,8 @@ export function CelestialBodies() {
|
|||
);
|
||||
|
||||
toast.success(`${response.data.message} (上传到 ${response.data.upload_directory} 目录)`);
|
||||
setRefreshResources(prev => prev + 1); // Trigger reload
|
||||
// Trigger a refresh of resources
|
||||
setRefreshResources(prev => prev + 1);
|
||||
return false; // Prevent default upload behavior
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.detail || '上传失败');
|
||||
|
|
@ -307,6 +332,34 @@ export function CelestialBodies() {
|
|||
}
|
||||
};
|
||||
|
||||
// Generate orbit for a celestial body
|
||||
const handleGenerateOrbit = async (record: CelestialBody) => {
|
||||
if (!['planet', 'dwarf_planet'].includes(record.type)) {
|
||||
toast.warning('只有行星和矮行星可以生成轨道');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await request.post(
|
||||
`/celestial/admin/orbits/generate?body_ids=${record.id}`
|
||||
);
|
||||
|
||||
if (response.data.results && response.data.results.length > 0) {
|
||||
const result = response.data.results[0];
|
||||
if (result.status === 'success') {
|
||||
toast.success(`轨道生成成功!共 ${result.num_points} 个点`);
|
||||
} else {
|
||||
toast.error(`轨道生成失败:${result.error}`);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.detail || '轨道生成失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle resource delete
|
||||
const handleResourceDelete = async (resourceId: number) => {
|
||||
try {
|
||||
|
|
@ -452,6 +505,33 @@ export function CelestialBodies() {
|
|||
statusField="is_active"
|
||||
rowKey="id"
|
||||
pageSize={10}
|
||||
customActions={(record) => {
|
||||
// Show "Generate Orbit" button for all types, but disable for non-planets
|
||||
const canGenerateOrbit = ['planet', 'dwarf_planet'].includes(record.type);
|
||||
|
||||
return (
|
||||
<Popconfirm
|
||||
title="确认生成轨道"
|
||||
description={`确定要为 ${record.name_zh || record.name} 生成轨道吗?此操作可能需要一些时间。`}
|
||||
onConfirm={() => handleGenerateOrbit(record)}
|
||||
okText="确认"
|
||||
cancelText="取消"
|
||||
disabled={!canGenerateOrbit}
|
||||
>
|
||||
<Tooltip title={canGenerateOrbit ? "生成轨道" : "仅行星和矮行星可生成轨道"}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
className={canGenerateOrbit ? "text-green-600 hover:text-green-500" : ""}
|
||||
loading={loading}
|
||||
disabled={!canGenerateOrbit}
|
||||
>
|
||||
生成轨道
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
|
|
@ -529,6 +609,54 @@ 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}>
|
||||
{({ getFieldValue }) => {
|
||||
const bodyType = getFieldValue('type');
|
||||
if (!['planet', 'dwarf_planet'].includes(bodyType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert
|
||||
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>
|
||||
}
|
||||
type="info"
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
{editingRecord && (
|
||||
<ResourceManager
|
||||
bodyId={editingRecord.id}
|
||||
|
|
|
|||
Loading…
Reference in New Issue