Spaces:
Sleeping
Sleeping
Trae Assistant commited on
Commit ·
c21fa1a
0
Parent(s):
Initial commit: Enhanced Orbit Ops Agent with file upload and visualization
Browse files- Dockerfile +12 -0
- README.md +48 -0
- app.py +209 -0
- instance/orbit_ops.db +0 -0
- requirements.txt +4 -0
- templates/index.html +487 -0
Dockerfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.9-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
COPY . .
|
| 9 |
+
|
| 10 |
+
RUN mkdir -p instance && chmod 777 instance
|
| 11 |
+
|
| 12 |
+
CMD ["gunicorn", "-b", "0.0.0.0:7866", "app:app"]
|
README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Orbit Ops Agent
|
| 3 |
+
emoji: 🛰️
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7866
|
| 8 |
+
short_description: 商业航天卫星星座运维与任务规划智能体
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# 轨道运维智能体 (Orbit Ops Agent)
|
| 12 |
+
|
| 13 |
+
## 项目简介
|
| 14 |
+
Orbit Ops Agent 是一个专为商业航天设计的卫星运维与任务规划 SaaS 平台。它集成了实时遥测监控、卫星资产管理、任务规划时间轴以及基于 LLM 的智能任务助手。
|
| 15 |
+
|
| 16 |
+
## 核心功能
|
| 17 |
+
1. **态势感知仪表盘**:实时监控卫星信号强度、星座能力雷达图及关键指标。
|
| 18 |
+
2. **卫星资产管理**:全生命周期的卫星数据库(发射、在轨、退役)。
|
| 19 |
+
3. **任务规划**:可视化的任务时间轴,支持发射、变轨、维护等任务管理。
|
| 20 |
+
4. **AI 任务助手**:集成 SiliconFlow (Qwen) 大模型,提供专业的轨道力学咨询、异常排查建议和任务规划辅助。
|
| 21 |
+
|
| 22 |
+
## 技术栈
|
| 23 |
+
- **后端**:Python Flask, SQLite
|
| 24 |
+
- **前端**:Vue 3, Tailwind CSS, ECharts
|
| 25 |
+
- **AI**:SiliconFlow API (Qwen-2.5-7B)
|
| 26 |
+
- **部署**:Docker
|
| 27 |
+
|
| 28 |
+
## 快速开始
|
| 29 |
+
|
| 30 |
+
### 本地运行
|
| 31 |
+
```bash
|
| 32 |
+
# 构建镜像
|
| 33 |
+
docker build -t orbit-ops .
|
| 34 |
+
|
| 35 |
+
# 运行容器
|
| 36 |
+
docker run -p 7865:7865 orbit-ops
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
或者直接使用 Python 运行:
|
| 40 |
+
```bash
|
| 41 |
+
pip install -r requirements.txt
|
| 42 |
+
python app.py
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
访问 `http://localhost:7866` 即可使用。
|
| 46 |
+
|
| 47 |
+
## 许可证
|
| 48 |
+
MIT
|
app.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sqlite3
|
| 3 |
+
import json
|
| 4 |
+
import requests
|
| 5 |
+
import random
|
| 6 |
+
import time
|
| 7 |
+
from flask import Flask, render_template, request, jsonify, g
|
| 8 |
+
from flask_cors import CORS
|
| 9 |
+
from werkzeug.utils import secure_filename
|
| 10 |
+
|
| 11 |
+
app = Flask(__name__)
|
| 12 |
+
CORS(app)
|
| 13 |
+
|
| 14 |
+
# Configuration
|
| 15 |
+
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB
|
| 16 |
+
app.config['UPLOAD_FOLDER'] = os.path.join(app.instance_path, 'uploads')
|
| 17 |
+
SILICONFLOW_API_KEY = "sk-vimuseiptfbomzegyuvmebjzooncsqbyjtlddrfodzcdskgi"
|
| 18 |
+
SILICONFLOW_API_URL = "https://api.siliconflow.cn/v1/chat/completions"
|
| 19 |
+
|
| 20 |
+
# Ensure upload directory exists
|
| 21 |
+
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
| 22 |
+
|
| 23 |
+
# Error Handlers
|
| 24 |
+
@app.errorhandler(413)
|
| 25 |
+
def request_entity_too_large(error):
|
| 26 |
+
return jsonify({'error': 'File too large. Maximum size is 16MB.'}), 413
|
| 27 |
+
|
| 28 |
+
@app.errorhandler(404)
|
| 29 |
+
def not_found_error(error):
|
| 30 |
+
return render_template('index.html'), 200 # SPA fallback or just ignore
|
| 31 |
+
|
| 32 |
+
@app.errorhandler(500)
|
| 33 |
+
def internal_error(error):
|
| 34 |
+
return jsonify({'error': 'Internal Server Error'}), 500
|
| 35 |
+
|
| 36 |
+
# Database Setup
|
| 37 |
+
def get_db():
|
| 38 |
+
db = getattr(g, '_database', None)
|
| 39 |
+
if db is None:
|
| 40 |
+
db_path = os.path.join(app.instance_path, 'orbit_ops.db')
|
| 41 |
+
if not os.path.exists(app.instance_path):
|
| 42 |
+
os.makedirs(app.instance_path)
|
| 43 |
+
db = g._database = sqlite3.connect(db_path)
|
| 44 |
+
db.row_factory = sqlite3.Row
|
| 45 |
+
return db
|
| 46 |
+
|
| 47 |
+
@app.teardown_appcontext
|
| 48 |
+
def close_connection(exception):
|
| 49 |
+
db = getattr(g, '_database', None)
|
| 50 |
+
if db is not None:
|
| 51 |
+
db.close()
|
| 52 |
+
|
| 53 |
+
def init_db():
|
| 54 |
+
with app.app_context():
|
| 55 |
+
db = get_db()
|
| 56 |
+
# Satellites Table
|
| 57 |
+
db.execute('''
|
| 58 |
+
CREATE TABLE IF NOT EXISTS satellites (
|
| 59 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 60 |
+
name TEXT NOT NULL,
|
| 61 |
+
type TEXT NOT NULL,
|
| 62 |
+
status TEXT NOT NULL,
|
| 63 |
+
orbit_altitude INTEGER,
|
| 64 |
+
inclination REAL,
|
| 65 |
+
launch_date TEXT
|
| 66 |
+
)
|
| 67 |
+
''')
|
| 68 |
+
# Missions Table
|
| 69 |
+
db.execute('''
|
| 70 |
+
CREATE TABLE IF NOT EXISTS missions (
|
| 71 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 72 |
+
name TEXT NOT NULL,
|
| 73 |
+
description TEXT,
|
| 74 |
+
status TEXT NOT NULL,
|
| 75 |
+
target_date TEXT
|
| 76 |
+
)
|
| 77 |
+
''')
|
| 78 |
+
|
| 79 |
+
# Seed Data if empty
|
| 80 |
+
cur = db.execute('SELECT count(*) FROM satellites')
|
| 81 |
+
if cur.fetchone()[0] == 0:
|
| 82 |
+
satellites = [
|
| 83 |
+
('StarLink-X1', 'Comm', 'Active', 550, 53.0, '2024-01-15'),
|
| 84 |
+
('Sentinel-Prime', 'EarthObs', 'Active', 700, 98.2, '2023-11-20'),
|
| 85 |
+
('Quantum-Relay', 'Comm', 'Testing', 1200, 45.0, '2025-02-01'),
|
| 86 |
+
('Debris-Hunter', 'Cleanup', 'Planned', 800, 85.0, '2026-06-10')
|
| 87 |
+
]
|
| 88 |
+
db.executemany('INSERT INTO satellites (name, type, status, orbit_altitude, inclination, launch_date) VALUES (?, ?, ?, ?, ?, ?)', satellites)
|
| 89 |
+
|
| 90 |
+
missions = [
|
| 91 |
+
('Polar Orbit Insertion', 'Deploy Sentinel-Prime to polar orbit for ice monitoring.', 'Completed', '2023-11-20'),
|
| 92 |
+
('Constellation Expansion', 'Launch batch of 60 StarLink-X satellites.', 'Pending', '2026-03-15'),
|
| 93 |
+
('Debris Removal Demo', 'Test capture mechanism on defunct satellite.', 'Planning', '2026-08-01')
|
| 94 |
+
]
|
| 95 |
+
db.executemany('INSERT INTO missions (name, description, status, target_date) VALUES (?, ?, ?, ?)', missions)
|
| 96 |
+
|
| 97 |
+
db.commit()
|
| 98 |
+
|
| 99 |
+
# Routes
|
| 100 |
+
@app.route('/')
|
| 101 |
+
def index():
|
| 102 |
+
return render_template('index.html')
|
| 103 |
+
|
| 104 |
+
@app.route('/api/satellites', methods=['GET', 'POST'])
|
| 105 |
+
def handle_satellites():
|
| 106 |
+
db = get_db()
|
| 107 |
+
if request.method == 'POST':
|
| 108 |
+
data = request.json
|
| 109 |
+
db.execute('INSERT INTO satellites (name, type, status, orbit_altitude, inclination, launch_date) VALUES (?, ?, ?, ?, ?, ?)',
|
| 110 |
+
(data['name'], data['type'], data['status'], data['orbit_altitude'], data['inclination'], data['launch_date']))
|
| 111 |
+
db.commit()
|
| 112 |
+
return jsonify({'status': 'success'})
|
| 113 |
+
else:
|
| 114 |
+
cur = db.execute('SELECT * FROM satellites')
|
| 115 |
+
return jsonify([dict(row) for row in cur.fetchall()])
|
| 116 |
+
|
| 117 |
+
@app.route('/api/missions', methods=['GET', 'POST'])
|
| 118 |
+
def handle_missions():
|
| 119 |
+
db = get_db()
|
| 120 |
+
if request.method == 'POST':
|
| 121 |
+
data = request.json
|
| 122 |
+
db.execute('INSERT INTO missions (name, description, status, target_date) VALUES (?, ?, ?, ?)',
|
| 123 |
+
(data['name'], data['description'], data['status'], data['target_date']))
|
| 124 |
+
db.commit()
|
| 125 |
+
return jsonify({'status': 'success'})
|
| 126 |
+
else:
|
| 127 |
+
cur = db.execute('SELECT * FROM missions')
|
| 128 |
+
return jsonify([dict(row) for row in cur.fetchall()])
|
| 129 |
+
|
| 130 |
+
@app.route('/api/upload', methods=['POST'])
|
| 131 |
+
def upload_file():
|
| 132 |
+
if 'file' not in request.files:
|
| 133 |
+
return jsonify({'error': 'No file part'}), 400
|
| 134 |
+
file = request.files['file']
|
| 135 |
+
if file.filename == '':
|
| 136 |
+
return jsonify({'error': 'No selected file'}), 400
|
| 137 |
+
if file:
|
| 138 |
+
filename = secure_filename(file.filename)
|
| 139 |
+
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
| 140 |
+
file.save(filepath)
|
| 141 |
+
|
| 142 |
+
# If it's a JSON file, try to import satellites or missions
|
| 143 |
+
if filename.endswith('.json'):
|
| 144 |
+
try:
|
| 145 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 146 |
+
data = json.load(f)
|
| 147 |
+
db = get_db()
|
| 148 |
+
|
| 149 |
+
if 'satellites' in data:
|
| 150 |
+
for sat in data['satellites']:
|
| 151 |
+
db.execute('INSERT INTO satellites (name, type, status, orbit_altitude, inclination, launch_date) VALUES (?, ?, ?, ?, ?, ?)',
|
| 152 |
+
(sat.get('name'), sat.get('type'), sat.get('status'), sat.get('orbit_altitude'), sat.get('inclination'), sat.get('launch_date')))
|
| 153 |
+
|
| 154 |
+
if 'missions' in data:
|
| 155 |
+
for mission in data['missions']:
|
| 156 |
+
db.execute('INSERT INTO missions (name, description, status, target_date) VALUES (?, ?, ?, ?)',
|
| 157 |
+
(mission.get('name'), mission.get('description'), mission.get('status'), mission.get('target_date')))
|
| 158 |
+
|
| 159 |
+
db.commit()
|
| 160 |
+
return jsonify({'status': 'success', 'message': 'File uploaded and data imported successfully'})
|
| 161 |
+
except Exception as e:
|
| 162 |
+
return jsonify({'status': 'warning', 'message': f'File uploaded but import failed: {str(e)}'})
|
| 163 |
+
|
| 164 |
+
return jsonify({'status': 'success', 'message': 'File uploaded successfully'})
|
| 165 |
+
|
| 166 |
+
@app.route('/api/telemetry')
|
| 167 |
+
def get_telemetry():
|
| 168 |
+
# Mock real-time telemetry
|
| 169 |
+
return jsonify({
|
| 170 |
+
'timestamp': time.time(),
|
| 171 |
+
'signal_strength': random.uniform(80, 100),
|
| 172 |
+
'battery_level': random.uniform(90, 100),
|
| 173 |
+
'cpu_load': random.uniform(10, 40),
|
| 174 |
+
'temperature': random.uniform(20, 35)
|
| 175 |
+
})
|
| 176 |
+
|
| 177 |
+
@app.route('/api/chat', methods=['POST'])
|
| 178 |
+
def chat():
|
| 179 |
+
data = request.json
|
| 180 |
+
user_message = data.get('message', '')
|
| 181 |
+
|
| 182 |
+
headers = {
|
| 183 |
+
"Authorization": f"Bearer {SILICONFLOW_API_KEY}",
|
| 184 |
+
"Content-Type": "application/json"
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
payload = {
|
| 188 |
+
"model": "Qwen/Qwen2.5-7B-Instruct",
|
| 189 |
+
"messages": [
|
| 190 |
+
{"role": "system", "content": "你是 Orbit Ops 的 AI 任务规划助手。你是一个专业的航天工程师,负责协助用户进行卫星星座管理、轨道计算、发射任务规划和故障排查。请用中文回答,回答要专业、严谨,并在适当时提供 Markdown 格式的表格或列表。如果涉及数据分析,可以建议用户查看仪表盘。"},
|
| 191 |
+
{"role": "user", "content": user_message}
|
| 192 |
+
],
|
| 193 |
+
"stream": False
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
try:
|
| 197 |
+
response = requests.post(SILICONFLOW_API_URL, json=payload, headers=headers, timeout=30)
|
| 198 |
+
response.raise_for_status()
|
| 199 |
+
result = response.json()
|
| 200 |
+
content = result['choices'][0]['message']['content']
|
| 201 |
+
return jsonify({'response': content})
|
| 202 |
+
except Exception as e:
|
| 203 |
+
print(f"API Error: {e}")
|
| 204 |
+
# Mock Fallback
|
| 205 |
+
return jsonify({'response': f"AI 服务连接失败 (Mock Mode): 我收到了你的请求 '{user_message}'。作为一个模拟的 AI 助手,我建议你检查卫星 #003 的遥测数据,似乎有异常波动。请在仪表盘中查看详细信息。"})
|
| 206 |
+
|
| 207 |
+
if __name__ == '__main__':
|
| 208 |
+
init_db()
|
| 209 |
+
app.run(host='0.0.0.0', port=7866, debug=True)
|
instance/orbit_ops.db
ADDED
|
Binary file (16.4 kB). View file
|
|
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Flask==3.0.0
|
| 2 |
+
flask-cors==4.0.0
|
| 3 |
+
requests==2.31.0
|
| 4 |
+
gunicorn==21.2.0
|
templates/index.html
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Orbit Ops Agent - 商业航天运维平台</title>
|
| 7 |
+
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
| 8 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 9 |
+
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
| 10 |
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
| 11 |
+
<style>
|
| 12 |
+
[v-cloak] { display: none; }
|
| 13 |
+
.markdown-body { font-size: 0.95rem; line-height: 1.6; }
|
| 14 |
+
.markdown-body pre { background: #f3f4f6; padding: 1rem; border-radius: 0.5rem; overflow-x: auto; }
|
| 15 |
+
.markdown-body code { background: #f3f4f6; padding: 0.2rem 0.4rem; border-radius: 0.25rem; font-family: monospace; }
|
| 16 |
+
.markdown-body table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
|
| 17 |
+
.markdown-body th, .markdown-body td { border: 1px solid #e5e7eb; padding: 0.5rem; }
|
| 18 |
+
.markdown-body th { background: #f9fafb; }
|
| 19 |
+
</style>
|
| 20 |
+
</head>
|
| 21 |
+
<body class="bg-slate-50 text-slate-900 h-screen flex overflow-hidden">
|
| 22 |
+
<div id="app" v-cloak class="flex w-full h-full">
|
| 23 |
+
<!-- Sidebar -->
|
| 24 |
+
<div class="w-64 bg-slate-900 text-white flex flex-col shrink-0 transition-all duration-300" :class="{'w-20': collapsed}">
|
| 25 |
+
<div class="p-4 flex items-center justify-between border-b border-slate-700">
|
| 26 |
+
<div class="flex items-center gap-3" v-if="!collapsed">
|
| 27 |
+
<div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center font-bold">O</div>
|
| 28 |
+
<span class="font-bold text-lg tracking-wide">Orbit Ops</span>
|
| 29 |
+
</div>
|
| 30 |
+
<div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center font-bold" v-else>O</div>
|
| 31 |
+
<button @click="collapsed = !collapsed" class="text-slate-400 hover:text-white">
|
| 32 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
| 33 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
| 34 |
+
</svg>
|
| 35 |
+
</button>
|
| 36 |
+
</div>
|
| 37 |
+
|
| 38 |
+
<nav class="flex-1 p-4 space-y-2">
|
| 39 |
+
<button @click="currentView = 'dashboard'" :class="{'bg-blue-600 text-white': currentView === 'dashboard', 'text-slate-400 hover:bg-slate-800': currentView !== 'dashboard'}" class="w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors">
|
| 40 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M8.159 13.39s.009-.089.009-.089a.75.75 0 00-.717.052l-2.613 1.488a.75.75 0 00-.007 1.298l2.613 1.487a.75.75 0 00.724.048s-.008-.09-.008-.09a2.25 2.25 0 01-2.072-3.093zM15 13.5h2.25a2.25 2.25 0 012.25 2.25v2.25a2.25 2.25 0 01-2.25 2.25H15a2.25 2.25 0 01-2.25-2.25V15.75A2.25 2.25 0 0115 13.5zM15 3v2.25a2.25 2.25 0 01-2.25 2.25H10.5a2.25 2.25 0 01-2.25-2.25V3" /></svg>
|
| 41 |
+
<span v-if="!collapsed">态势感知</span>
|
| 42 |
+
</button>
|
| 43 |
+
<button @click="currentView = 'satellites'" :class="{'bg-blue-600 text-white': currentView === 'satellites', 'text-slate-400 hover:bg-slate-800': currentView !== 'satellites'}" class="w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors">
|
| 44 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 00.75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 00-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0112 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 01-.673-.38m0 0A2.18 2.18 0 013 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 013.413-.387m7.5 0V5.25A2.25 2.25 0 0013.5 3h-3a2.25 2.25 0 00-2.25 2.25v.894m7.5 0a48.667 48.667 0 00-7.5 0M12 12.75h.008v.008H12v-.008z" /></svg>
|
| 45 |
+
<span v-if="!collapsed">卫星资产</span>
|
| 46 |
+
</button>
|
| 47 |
+
<button @click="currentView = 'missions'" :class="{'bg-blue-600 text-white': currentView === 'missions', 'text-slate-400 hover:bg-slate-800': currentView !== 'missions'}" class="w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors">
|
| 48 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0h18M5 10.5h.008v.008H5V10.5zm0 3h.008v.008H5V13.5zm0 3h.008v.008H5V16.5zm3-6h.008v.008H8V10.5zm0 3h.008v.008H8V13.5zm0 3h.008v.008H8V16.5zm3-6h.008v.008H11V10.5zm0 3h.008v.008H11V13.5zm0 3h.008v.008H11V16.5zm3-6h.008v.008H14V10.5zm0 3h.008v.008H14V13.5zm0 3h.008v.008H14V16.5zm3-6h.008v.008H17V10.5zm0 3h.008v.008H17V13.5zm0 3h.008v.008H17V16.5z" /></svg>
|
| 49 |
+
<span v-if="!collapsed">任务规划</span>
|
| 50 |
+
</button>
|
| 51 |
+
<button @click="currentView = 'chat'" :class="{'bg-blue-600 text-white': currentView === 'chat', 'text-slate-400 hover:bg-slate-800': currentView !== 'chat'}" class="w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors">
|
| 52 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a5.969 5.969 0 01-.474-.065 4.48 4.48 0 00.978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z" /></svg>
|
| 53 |
+
<span v-if="!collapsed">AI 助手</span>
|
| 54 |
+
</button>
|
| 55 |
+
</nav>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
<!-- Main Content -->
|
| 59 |
+
<main class="flex-1 overflow-auto bg-slate-50 relative">
|
| 60 |
+
<!-- Dashboard View -->
|
| 61 |
+
<div v-if="currentView === 'dashboard'" class="p-6 space-y-6">
|
| 62 |
+
<header class="flex justify-between items-center mb-6">
|
| 63 |
+
<h2 class="text-2xl font-bold text-slate-800">态势感知仪表盘</h2>
|
| 64 |
+
<div class="flex items-center gap-2 text-sm text-slate-500">
|
| 65 |
+
<span class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></span>
|
| 66 |
+
实时数据连接正常
|
| 67 |
+
</div>
|
| 68 |
+
</header>
|
| 69 |
+
|
| 70 |
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
| 71 |
+
<div class="bg-white p-4 rounded-xl shadow-sm border border-slate-200">
|
| 72 |
+
<div class="text-slate-500 text-sm mb-1">在轨卫星</div>
|
| 73 |
+
<div class="text-2xl font-bold text-slate-800">${ satellites.length }</div>
|
| 74 |
+
</div>
|
| 75 |
+
<div class="bg-white p-4 rounded-xl shadow-sm border border-slate-200">
|
| 76 |
+
<div class="text-slate-500 text-sm mb-1">活跃任务</div>
|
| 77 |
+
<div class="text-2xl font-bold text-blue-600">${ activeMissionsCount }</div>
|
| 78 |
+
</div>
|
| 79 |
+
<div class="bg-white p-4 rounded-xl shadow-sm border border-slate-200">
|
| 80 |
+
<div class="text-slate-500 text-sm mb-1">平均信号强度</div>
|
| 81 |
+
<div class="text-2xl font-bold text-green-600">${ telemetry.signal_strength.toFixed(1) }%</div>
|
| 82 |
+
</div>
|
| 83 |
+
<div class="bg-white p-4 rounded-xl shadow-sm border border-slate-200">
|
| 84 |
+
<div class="text-slate-500 text-sm mb-1">碰撞风险</div>
|
| 85 |
+
<div class="text-2xl font-bold text-red-500">低</div>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
|
| 89 |
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 h-80">
|
| 90 |
+
<div class="bg-white p-4 rounded-xl shadow-sm border border-slate-200 flex flex-col">
|
| 91 |
+
<h3 class="font-semibold text-slate-700 mb-4">遥测信号趋势</h3>
|
| 92 |
+
<div id="telemetryChart" class="flex-1 w-full h-full"></div>
|
| 93 |
+
</div>
|
| 94 |
+
<div class="bg-white p-4 rounded-xl shadow-sm border border-slate-200 flex flex-col">
|
| 95 |
+
<h3 class="font-semibold text-slate-700 mb-4">星座能力雷达</h3>
|
| 96 |
+
<div id="radarChart" class="flex-1 w-full h-full"></div>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
|
| 101 |
+
<!-- Satellites View -->
|
| 102 |
+
<div v-if="currentView === 'satellites'" class="p-6 space-y-6">
|
| 103 |
+
<header class="flex justify-between items-center mb-6">
|
| 104 |
+
<h2 class="text-2xl font-bold text-slate-800">卫星资产管理</h2>
|
| 105 |
+
<div class="flex gap-2">
|
| 106 |
+
<button @click="triggerUpload" class="bg-white border border-slate-300 hover:bg-slate-50 text-slate-700 px-4 py-2 rounded-lg flex items-center gap-2 transition-colors">
|
| 107 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" /></svg>
|
| 108 |
+
���入数据
|
| 109 |
+
</button>
|
| 110 |
+
<input type="file" ref="fileInput" @change="handleFileUpload" class="hidden" accept=".json">
|
| 111 |
+
<button @click="showAddSatellite = true" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors">
|
| 112 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
|
| 113 |
+
添加卫星
|
| 114 |
+
</button>
|
| 115 |
+
</div>
|
| 116 |
+
</header>
|
| 117 |
+
|
| 118 |
+
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
| 119 |
+
<table class="w-full text-left text-sm text-slate-600">
|
| 120 |
+
<thead class="bg-slate-50 text-slate-800 border-b border-slate-200">
|
| 121 |
+
<tr>
|
| 122 |
+
<th class="px-6 py-4 font-semibold">名称</th>
|
| 123 |
+
<th class="px-6 py-4 font-semibold">类型</th>
|
| 124 |
+
<th class="px-6 py-4 font-semibold">状态</th>
|
| 125 |
+
<th class="px-6 py-4 font-semibold">轨道高度 (km)</th>
|
| 126 |
+
<th class="px-6 py-4 font-semibold">倾角 (deg)</th>
|
| 127 |
+
<th class="px-6 py-4 font-semibold">发射日期</th>
|
| 128 |
+
</tr>
|
| 129 |
+
</thead>
|
| 130 |
+
<tbody class="divide-y divide-slate-100">
|
| 131 |
+
<tr v-for="sat in satellites" :key="sat.id" class="hover:bg-slate-50 transition-colors">
|
| 132 |
+
<td class="px-6 py-4 font-medium text-slate-900">${ sat.name }</td>
|
| 133 |
+
<td class="px-6 py-4">
|
| 134 |
+
<span class="px-2 py-1 bg-slate-100 rounded text-xs">${ sat.type }</span>
|
| 135 |
+
</td>
|
| 136 |
+
<td class="px-6 py-4">
|
| 137 |
+
<span :class="{'bg-green-100 text-green-700': sat.status === 'Active', 'bg-yellow-100 text-yellow-700': sat.status === 'Testing', 'bg-slate-100 text-slate-700': sat.status === 'Planned'}" class="px-2 py-1 rounded-full text-xs font-medium">
|
| 138 |
+
${ sat.status }
|
| 139 |
+
</span>
|
| 140 |
+
</td>
|
| 141 |
+
<td class="px-6 py-4">${ sat.orbit_altitude }</td>
|
| 142 |
+
<td class="px-6 py-4">${ sat.inclination }°</td>
|
| 143 |
+
<td class="px-6 py-4 text-slate-500">${ sat.launch_date }</td>
|
| 144 |
+
</tr>
|
| 145 |
+
</tbody>
|
| 146 |
+
</table>
|
| 147 |
+
</div>
|
| 148 |
+
</div>
|
| 149 |
+
|
| 150 |
+
<!-- Missions View -->
|
| 151 |
+
<div v-if="currentView === 'missions'" class="p-6 space-y-6">
|
| 152 |
+
<header class="flex justify-between items-center mb-6">
|
| 153 |
+
<h2 class="text-2xl font-bold text-slate-800">任务规划时间轴</h2>
|
| 154 |
+
<button @click="showAddMission = true" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2">
|
| 155 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
|
| 156 |
+
新建任务
|
| 157 |
+
</button>
|
| 158 |
+
</header>
|
| 159 |
+
|
| 160 |
+
<div class="space-y-4">
|
| 161 |
+
<div v-for="mission in missions" :key="mission.id" class="bg-white p-6 rounded-xl shadow-sm border border-slate-200 flex gap-6 items-start">
|
| 162 |
+
<div class="flex flex-col items-center gap-1 min-w-[100px]">
|
| 163 |
+
<span class="text-sm font-bold text-slate-400">${ mission.target_date }</span>
|
| 164 |
+
<div class="w-px h-full bg-slate-200 my-2"></div>
|
| 165 |
+
</div>
|
| 166 |
+
<div class="flex-1">
|
| 167 |
+
<div class="flex justify-between items-start mb-2">
|
| 168 |
+
<h3 class="text-lg font-bold text-slate-800">${ mission.name }</h3>
|
| 169 |
+
<span :class="{'bg-blue-100 text-blue-700': mission.status === 'Pending', 'bg-green-100 text-green-700': mission.status === 'Completed', 'bg-purple-100 text-purple-700': mission.status === 'Planning'}" class="px-2 py-1 rounded text-xs font-medium">
|
| 170 |
+
${ mission.status }
|
| 171 |
+
</span>
|
| 172 |
+
</div>
|
| 173 |
+
<p class="text-slate-600 text-sm">${ mission.description }</p>
|
| 174 |
+
</div>
|
| 175 |
+
</div>
|
| 176 |
+
</div>
|
| 177 |
+
</div>
|
| 178 |
+
|
| 179 |
+
<!-- Chat View -->
|
| 180 |
+
<div v-if="currentView === 'chat'" class="flex flex-col h-full">
|
| 181 |
+
<div class="flex-1 overflow-y-auto p-6 space-y-4" ref="chatContainer">
|
| 182 |
+
<div v-for="(msg, index) in chatHistory" :key="index" :class="{'flex justify-end': msg.role === 'user', 'flex justify-start': msg.role === 'ai'}">
|
| 183 |
+
<div :class="{'bg-blue-600 text-white': msg.role === 'user', 'bg-white border border-slate-200 text-slate-800': msg.role === 'ai'}" class="max-w-[80%] rounded-2xl px-4 py-3 shadow-sm">
|
| 184 |
+
<div v-if="msg.role === 'ai'" class="markdown-body" v-html="renderMarkdown(msg.content)"></div>
|
| 185 |
+
<div v-else>${ msg.content }</div>
|
| 186 |
+
</div>
|
| 187 |
+
</div>
|
| 188 |
+
<div v-if="isThinking" class="flex justify-start">
|
| 189 |
+
<div class="bg-white border border-slate-200 text-slate-500 rounded-2xl px-4 py-3 shadow-sm flex items-center gap-2">
|
| 190 |
+
<div class="w-2 h-2 bg-slate-400 rounded-full animate-bounce"></div>
|
| 191 |
+
<div class="w-2 h-2 bg-slate-400 rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
|
| 192 |
+
<div class="w-2 h-2 bg-slate-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
|
| 193 |
+
</div>
|
| 194 |
+
</div>
|
| 195 |
+
</div>
|
| 196 |
+
<div class="p-4 bg-white border-t border-slate-200">
|
| 197 |
+
<div class="flex gap-2 max-w-4xl mx-auto">
|
| 198 |
+
<input v-model="userQuery" @keyup.enter="sendMessage" type="text" placeholder="输入任务指令,例如:'帮我规划 StarLink-X1 的变轨策略'..." class="flex-1 px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
| 199 |
+
<button @click="sendMessage" :disabled="isThinking || !userQuery.trim()" class="bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white px-6 py-2 rounded-lg font-medium transition-colors">
|
| 200 |
+
发送
|
| 201 |
+
</button>
|
| 202 |
+
</div>
|
| 203 |
+
</div>
|
| 204 |
+
</div>
|
| 205 |
+
|
| 206 |
+
<!-- Modals -->
|
| 207 |
+
<div v-if="showAddSatellite" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
| 208 |
+
<div class="bg-white rounded-xl shadow-xl w-96 p-6">
|
| 209 |
+
<h3 class="text-xl font-bold mb-4">添加新卫星</h3>
|
| 210 |
+
<div class="space-y-3">
|
| 211 |
+
<input v-model="newSat.name" placeholder="卫星名称" class="w-full border rounded p-2">
|
| 212 |
+
<select v-model="newSat.type" class="w-full border rounded p-2">
|
| 213 |
+
<option value="Comm">通信 (Comm)</option>
|
| 214 |
+
<option value="EarthObs">遥感 (EarthObs)</option>
|
| 215 |
+
<option value="Nav">导航 (Nav)</option>
|
| 216 |
+
</select>
|
| 217 |
+
<select v-model="newSat.status" class="w-full border rounded p-2">
|
| 218 |
+
<option value="Active">Active</option>
|
| 219 |
+
<option value="Testing">Testing</option>
|
| 220 |
+
<option value="Planned">Planned</option>
|
| 221 |
+
</select>
|
| 222 |
+
<input v-model.number="newSat.orbit_altitude" placeholder="轨道高度 (km)" type="number" class="w-full border rounded p-2">
|
| 223 |
+
<input v-model.number="newSat.inclination" placeholder="倾角 (deg)" type="number" class="w-full border rounded p-2">
|
| 224 |
+
<input v-model="newSat.launch_date" type="date" class="w-full border rounded p-2">
|
| 225 |
+
</div>
|
| 226 |
+
<div class="flex justify-end gap-2 mt-6">
|
| 227 |
+
<button @click="showAddSatellite = false" class="text-slate-500 hover:text-slate-700">取消</button>
|
| 228 |
+
<button @click="addSatellite" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">确认添加</button>
|
| 229 |
+
</div>
|
| 230 |
+
</div>
|
| 231 |
+
</div>
|
| 232 |
+
|
| 233 |
+
<div v-if="showAddMission" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
| 234 |
+
<div class="bg-white rounded-xl shadow-xl w-96 p-6">
|
| 235 |
+
<h3 class="text-xl font-bold mb-4">添加新任务</h3>
|
| 236 |
+
<div class="space-y-3">
|
| 237 |
+
<input v-model="newMission.name" placeholder="任务名称" class="w-full border rounded p-2">
|
| 238 |
+
<textarea v-model="newMission.description" placeholder="任务描述" class="w-full border rounded p-2 h-24"></textarea>
|
| 239 |
+
<select v-model="newMission.status" class="w-full border rounded p-2">
|
| 240 |
+
<option value="Planning">Planning</option>
|
| 241 |
+
<option value="Pending">Pending</option>
|
| 242 |
+
<option value="Completed">Completed</option>
|
| 243 |
+
</select>
|
| 244 |
+
<input v-model="newMission.target_date" type="date" class="w-full border rounded p-2">
|
| 245 |
+
</div>
|
| 246 |
+
<div class="flex justify-end gap-2 mt-6">
|
| 247 |
+
<button @click="showAddMission = false" class="text-slate-500 hover:text-slate-700">取消</button>
|
| 248 |
+
<button @click="addMission" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">确认添加</button>
|
| 249 |
+
</div>
|
| 250 |
+
</div>
|
| 251 |
+
</div>
|
| 252 |
+
|
| 253 |
+
</main>
|
| 254 |
+
</div>
|
| 255 |
+
|
| 256 |
+
<script>
|
| 257 |
+
const { createApp, ref, onMounted, computed, nextTick, watch } = Vue;
|
| 258 |
+
|
| 259 |
+
createApp({
|
| 260 |
+
delimiters: ['${', '}'],
|
| 261 |
+
setup() {
|
| 262 |
+
const collapsed = ref(false);
|
| 263 |
+
const currentView = ref('dashboard');
|
| 264 |
+
const satellites = ref([]);
|
| 265 |
+
const missions = ref([]);
|
| 266 |
+
const telemetry = ref({ signal_strength: 0, battery_level: 0, cpu_load: 0, temperature: 0 });
|
| 267 |
+
const showAddSatellite = ref(false);
|
| 268 |
+
const showAddMission = ref(false);
|
| 269 |
+
|
| 270 |
+
const newSat = ref({ name: '', type: 'Comm', status: 'Planned', orbit_altitude: 500, inclination: 0, launch_date: '' });
|
| 271 |
+
const newMission = ref({ name: '', description: '', status: 'Planning', target_date: '' });
|
| 272 |
+
const fileInput = ref(null);
|
| 273 |
+
|
| 274 |
+
const chatHistory = ref([
|
| 275 |
+
{ role: 'ai', content: '你好!我是 Orbit Ops 任务规划助手。我可以帮你计算轨道参数、规划发射窗口或分析遥测数据。' }
|
| 276 |
+
]);
|
| 277 |
+
const userQuery = ref('');
|
| 278 |
+
const isThinking = ref(false);
|
| 279 |
+
const chatContainer = ref(null);
|
| 280 |
+
|
| 281 |
+
const activeMissionsCount = computed(() => missions.value.filter(m => m.status === 'Pending' || m.status === 'Planning').length);
|
| 282 |
+
|
| 283 |
+
// Fetch Data
|
| 284 |
+
const fetchSatellites = async () => {
|
| 285 |
+
try {
|
| 286 |
+
const res = await fetch('/api/satellites');
|
| 287 |
+
satellites.value = await res.json();
|
| 288 |
+
} catch (e) { console.error(e); }
|
| 289 |
+
};
|
| 290 |
+
|
| 291 |
+
const fetchMissions = async () => {
|
| 292 |
+
try {
|
| 293 |
+
const res = await fetch('/api/missions');
|
| 294 |
+
missions.value = await res.json();
|
| 295 |
+
} catch (e) { console.error(e); }
|
| 296 |
+
};
|
| 297 |
+
|
| 298 |
+
const fetchTelemetry = async () => {
|
| 299 |
+
try {
|
| 300 |
+
const res = await fetch('/api/telemetry');
|
| 301 |
+
const data = await res.json();
|
| 302 |
+
telemetry.value = data;
|
| 303 |
+
updateCharts(data);
|
| 304 |
+
} catch (e) { console.error(e); }
|
| 305 |
+
};
|
| 306 |
+
|
| 307 |
+
// Charts
|
| 308 |
+
let telemetryChart = null;
|
| 309 |
+
let radarChart = null;
|
| 310 |
+
const telemetryHistory = [];
|
| 311 |
+
|
| 312 |
+
const initCharts = () => {
|
| 313 |
+
const tChartDom = document.getElementById('telemetryChart');
|
| 314 |
+
const rChartDom = document.getElementById('radarChart');
|
| 315 |
+
|
| 316 |
+
if (tChartDom) {
|
| 317 |
+
if (telemetryChart) telemetryChart.dispose();
|
| 318 |
+
telemetryChart = echarts.init(tChartDom);
|
| 319 |
+
telemetryChart.setOption({
|
| 320 |
+
tooltip: { trigger: 'axis' },
|
| 321 |
+
grid: { top: 20, right: 20, bottom: 20, left: 40, containLabel: true },
|
| 322 |
+
xAxis: { type: 'category', data: [] },
|
| 323 |
+
yAxis: { type: 'value', min: 60, max: 100 },
|
| 324 |
+
series: [{ data: [], type: 'line', smooth: true, areaStyle: { opacity: 0.1 }, itemStyle: { color: '#3b82f6' } }]
|
| 325 |
+
});
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
if (rChartDom) {
|
| 329 |
+
if (radarChart) radarChart.dispose();
|
| 330 |
+
radarChart = echarts.init(rChartDom);
|
| 331 |
+
radarChart.setOption({
|
| 332 |
+
radar: {
|
| 333 |
+
indicator: [
|
| 334 |
+
{ name: '通信覆盖', max: 100 },
|
| 335 |
+
{ name: '观测精度', max: 100 },
|
| 336 |
+
{ name: '响应速度', max: 100 },
|
| 337 |
+
{ name: '能源效率', max: 100 },
|
| 338 |
+
{ name: '安全性', max: 100 }
|
| 339 |
+
]
|
| 340 |
+
},
|
| 341 |
+
series: [{
|
| 342 |
+
type: 'radar',
|
| 343 |
+
data: [
|
| 344 |
+
{ value: [85, 90, 75, 95, 80], name: '当���状态', areaStyle: { opacity: 0.2 }, itemStyle: { color: '#10b981' } }
|
| 345 |
+
]
|
| 346 |
+
}]
|
| 347 |
+
});
|
| 348 |
+
}
|
| 349 |
+
};
|
| 350 |
+
|
| 351 |
+
const updateCharts = (data) => {
|
| 352 |
+
if (!telemetryChart) return;
|
| 353 |
+
|
| 354 |
+
const now = new Date().toLocaleTimeString();
|
| 355 |
+
telemetryHistory.push({ time: now, value: data.signal_strength });
|
| 356 |
+
if (telemetryHistory.length > 20) telemetryHistory.shift();
|
| 357 |
+
|
| 358 |
+
telemetryChart.setOption({
|
| 359 |
+
xAxis: { data: telemetryHistory.map(i => i.time) },
|
| 360 |
+
series: [{ data: telemetryHistory.map(i => i.value) }]
|
| 361 |
+
});
|
| 362 |
+
};
|
| 363 |
+
|
| 364 |
+
// Actions
|
| 365 |
+
const addSatellite = async () => {
|
| 366 |
+
await fetch('/api/satellites', {
|
| 367 |
+
method: 'POST',
|
| 368 |
+
headers: { 'Content-Type': 'application/json' },
|
| 369 |
+
body: JSON.stringify(newSat.value)
|
| 370 |
+
});
|
| 371 |
+
showAddSatellite.value = false;
|
| 372 |
+
fetchSatellites();
|
| 373 |
+
newSat.value = { name: '', type: 'Comm', status: 'Planned', orbit_altitude: 500, inclination: 0, launch_date: '' };
|
| 374 |
+
};
|
| 375 |
+
|
| 376 |
+
const addMission = async () => {
|
| 377 |
+
await fetch('/api/missions', {
|
| 378 |
+
method: 'POST',
|
| 379 |
+
headers: { 'Content-Type': 'application/json' },
|
| 380 |
+
body: JSON.stringify(newMission.value)
|
| 381 |
+
});
|
| 382 |
+
showAddMission.value = false;
|
| 383 |
+
fetchMissions();
|
| 384 |
+
newMission.value = { name: '', description: '', status: 'Planning', target_date: '' };
|
| 385 |
+
};
|
| 386 |
+
|
| 387 |
+
const sendMessage = async () => {
|
| 388 |
+
if (!userQuery.value.trim()) return;
|
| 389 |
+
const msg = userQuery.value;
|
| 390 |
+
userQuery.value = '';
|
| 391 |
+
chatHistory.value.push({ role: 'user', content: msg });
|
| 392 |
+
isThinking.value = true;
|
| 393 |
+
scrollToBottom();
|
| 394 |
+
|
| 395 |
+
try {
|
| 396 |
+
const res = await fetch('/api/chat', {
|
| 397 |
+
method: 'POST',
|
| 398 |
+
headers: { 'Content-Type': 'application/json' },
|
| 399 |
+
body: JSON.stringify({ message: msg })
|
| 400 |
+
});
|
| 401 |
+
const data = await res.json();
|
| 402 |
+
chatHistory.value.push({ role: 'ai', content: data.response });
|
| 403 |
+
} catch (e) {
|
| 404 |
+
chatHistory.value.push({ role: 'ai', content: '连接中断,请稍后再试。' });
|
| 405 |
+
} finally {
|
| 406 |
+
isThinking.value = false;
|
| 407 |
+
scrollToBottom();
|
| 408 |
+
}
|
| 409 |
+
};
|
| 410 |
+
|
| 411 |
+
const triggerUpload = () => {
|
| 412 |
+
if (fileInput.value) fileInput.value.click();
|
| 413 |
+
};
|
| 414 |
+
|
| 415 |
+
const handleFileUpload = async (event) => {
|
| 416 |
+
const file = event.target.files[0];
|
| 417 |
+
if (!file) return;
|
| 418 |
+
|
| 419 |
+
const formData = new FormData();
|
| 420 |
+
formData.append('file', file);
|
| 421 |
+
|
| 422 |
+
try {
|
| 423 |
+
const res = await fetch('/api/upload', {
|
| 424 |
+
method: 'POST',
|
| 425 |
+
body: formData
|
| 426 |
+
});
|
| 427 |
+
const result = await res.json();
|
| 428 |
+
if (result.status === 'success') {
|
| 429 |
+
alert('上传成功!' + (result.message || ''));
|
| 430 |
+
fetchSatellites();
|
| 431 |
+
fetchMissions();
|
| 432 |
+
} else {
|
| 433 |
+
alert('上传失败:' + result.message);
|
| 434 |
+
}
|
| 435 |
+
} catch (e) {
|
| 436 |
+
alert('上传出错:' + e.message);
|
| 437 |
+
}
|
| 438 |
+
// Reset input
|
| 439 |
+
event.target.value = '';
|
| 440 |
+
};
|
| 441 |
+
|
| 442 |
+
const scrollToBottom = () => {
|
| 443 |
+
nextTick(() => {
|
| 444 |
+
if (chatContainer.value) chatContainer.value.scrollTop = chatContainer.value.scrollHeight;
|
| 445 |
+
});
|
| 446 |
+
};
|
| 447 |
+
|
| 448 |
+
const renderMarkdown = (text) => {
|
| 449 |
+
return marked.parse(text);
|
| 450 |
+
};
|
| 451 |
+
|
| 452 |
+
watch(currentView, (newVal) => {
|
| 453 |
+
if (newVal === 'dashboard') {
|
| 454 |
+
nextTick(() => {
|
| 455 |
+
initCharts();
|
| 456 |
+
});
|
| 457 |
+
}
|
| 458 |
+
});
|
| 459 |
+
|
| 460 |
+
onMounted(() => {
|
| 461 |
+
fetchSatellites();
|
| 462 |
+
fetchMissions();
|
| 463 |
+
setInterval(fetchTelemetry, 2000);
|
| 464 |
+
|
| 465 |
+
if (currentView.value === 'dashboard') {
|
| 466 |
+
setTimeout(initCharts, 500);
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
window.addEventListener('resize', () => {
|
| 470 |
+
if (telemetryChart) telemetryChart.resize();
|
| 471 |
+
if (radarChart) radarChart.resize();
|
| 472 |
+
});
|
| 473 |
+
});
|
| 474 |
+
|
| 475 |
+
return {
|
| 476 |
+
collapsed, currentView, satellites, missions, telemetry,
|
| 477 |
+
showAddSatellite, showAddMission, newSat, newMission,
|
| 478 |
+
chatHistory, userQuery, isThinking, chatContainer,
|
| 479 |
+
activeMissionsCount, fileInput,
|
| 480 |
+
addSatellite, addMission, sendMessage, renderMarkdown,
|
| 481 |
+
triggerUpload, handleFileUpload
|
| 482 |
+
};
|
| 483 |
+
}
|
| 484 |
+
}).mount('#app');
|
| 485 |
+
</script>
|
| 486 |
+
</body>
|
| 487 |
+
</html>
|