217 lines
8.5 KiB
TypeScript
217 lines
8.5 KiB
TypeScript
import { X, Ruler, Activity, Radar } from 'lucide-react';
|
|
import { useState } from 'react';
|
|
import { Modal, message, Spin } from 'antd';
|
|
import { request } from '../utils/request';
|
|
import type { CelestialBody } from '../types';
|
|
|
|
interface FocusInfoProps {
|
|
body: CelestialBody | null;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function FocusInfo({ body, onClose }: FocusInfoProps) {
|
|
const [showTerminal, setShowTerminal] = useState(false);
|
|
const [terminalData, setTerminalData] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
if (!body) return null;
|
|
|
|
// Calculate distance if position is available
|
|
const pos = body.positions[0];
|
|
const distance = pos ? Math.sqrt(pos.x ** 2 + pos.y ** 2 + pos.z ** 2).toFixed(2) : '---';
|
|
|
|
const isProbe = body.type === 'probe';
|
|
const isActive = body.is_active !== false;
|
|
|
|
const fetchNasaData = async () => {
|
|
setShowTerminal(true);
|
|
setLoading(true);
|
|
try {
|
|
const { data } = await request.get(`/celestial/${body.id}/nasa-data`);
|
|
setTerminalData(data.raw_data);
|
|
} catch (err) {
|
|
console.error(err);
|
|
message.error('连接 NASA Horizons 失败');
|
|
// If failed, maybe show error in terminal
|
|
setTerminalData("CONNECTION FAILED.\n\nError establishing link with JPL Horizons System.\nCheck connection frequencies.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const terminalStyles = `
|
|
.terminal-modal .ant-modal-content {
|
|
background-color: #0d1117 !important;
|
|
border: 1px solid #238636 !important;
|
|
box-shadow: 0 0 30px rgba(35, 134, 54, 0.15) !important;
|
|
color: #2ea043 !important;
|
|
padding: 0 !important;
|
|
overflow: hidden !important;
|
|
}
|
|
.terminal-modal .ant-modal-body {
|
|
background-color: #0d1117 !important;
|
|
}
|
|
.terminal-modal .ant-modal-header {
|
|
background-color: #161b22 !important;
|
|
border-bottom: 1px solid #238636 !important;
|
|
margin-bottom: 0 !important;
|
|
}
|
|
.terminal-modal .ant-modal-title {
|
|
color: #2ea043 !important;
|
|
}
|
|
.terminal-modal .ant-modal-close {
|
|
color: #2ea043 !important;
|
|
}
|
|
.terminal-modal .ant-modal-close:hover {
|
|
background-color: rgba(35, 134, 54, 0.2) !important;
|
|
}
|
|
.terminal-modal .ant-modal-body .animate-in {
|
|
color: #2ea043 !important; /* Ensure content text is green */
|
|
}
|
|
@keyframes spin-slow {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
.animate-spin-slow {
|
|
animation: spin-slow 3s linear infinite;
|
|
}
|
|
`;
|
|
|
|
return (
|
|
// Remove fixed positioning, now handled by parent container (Html component in 3D)
|
|
<div className="flex flex-col items-center -translate-y-24 pointer-events-none">
|
|
<style>{terminalStyles}</style>
|
|
{/* Main Info Card */}
|
|
<div className="bg-black/80 backdrop-blur-xl border border-white/10 rounded-2xl p-5 min-w-[340px] max-w-md shadow-2xl pointer-events-auto relative group mb-2">
|
|
|
|
{/* Close Button */}
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onClose();
|
|
}}
|
|
className="absolute top-3 right-3 text-gray-400 hover:text-white transition-colors p-1 rounded-full hover:bg-white/10"
|
|
>
|
|
<X size={16} />
|
|
</button>
|
|
|
|
{/* Header */}
|
|
<div className="flex items-start justify-between mb-3 pr-6">
|
|
<div>
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<h2 className="text-xl font-bold text-white tracking-tight">
|
|
{body.name_zh || body.name}
|
|
</h2>
|
|
<span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider border ${
|
|
isProbe
|
|
? 'bg-purple-500/20 border-purple-500/40 text-purple-300'
|
|
: 'bg-blue-500/20 border-blue-500/40 text-blue-300'
|
|
}`}>
|
|
{isProbe ? '探测器' : '天体'}
|
|
</span>
|
|
</div>
|
|
<p className="text-xs text-gray-400 line-clamp-2 leading-relaxed">
|
|
{body.description || '暂无描述'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats and Actions Grid */}
|
|
<div className="grid grid-cols-2 gap-2 mb-2">
|
|
{/* Column 1: Heliocentric Distance Card */}
|
|
<div className="bg-white/5 rounded-lg p-2 flex items-center gap-2.5 border border-white/5">
|
|
<div className="p-1.5 rounded-full bg-blue-500/20 text-blue-400">
|
|
<Ruler size={14} />
|
|
</div>
|
|
<div>
|
|
<div className="text-[9px] text-gray-500 uppercase">日心距离</div>
|
|
<div className="text-xs font-mono text-gray-200">{distance} AU</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Column 2: JPL Horizons Button */}
|
|
<div className="flex items-center justify-end">
|
|
<button
|
|
onClick={fetchNasaData}
|
|
className="px-3 py-1.5 rounded-lg bg-cyan-950/30 text-cyan-400 border border-cyan-500/20 hover:bg-cyan-500/10 hover:border-cyan-500/50 transition-all flex items-center gap-2 text-[10px] font-mono uppercase tracking-widest group/btn w-full justify-end"
|
|
title="连接 JPL Horizons System"
|
|
>
|
|
<Radar size={12} className="group-hover/btn:animate-spin-slow" />
|
|
<span>JPL Horizons</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Conditional Probe Status Card (if isProbe is true, this goes in a new row) */}
|
|
{isProbe && (
|
|
<div className="grid grid-cols-1 mb-2">
|
|
<div className="bg-white/5 rounded-lg p-2 flex items-center gap-2.5 border border-white/5">
|
|
<div className={`p-1.5 rounded-full ${isActive ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'}`}>
|
|
<Activity size={14} />
|
|
</div>
|
|
<div>
|
|
<div className="text-[9px] text-gray-500 uppercase">状态</div>
|
|
<div className={`text-xs font-medium ${isActive ? 'text-green-300' : 'text-red-300'}`}>
|
|
{isActive ? '运行中' : '已失效'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Connecting Line/Triangle pointing down to the body */}
|
|
<div className="w-0 h-0 border-l-[8px] border-l-transparent border-r-[8px] border-r-transparent border-t-[8px] border-t-black/80 backdrop-blur-xl mt-[-1px]"></div>
|
|
|
|
{/* Terminal Modal */}
|
|
<Modal
|
|
open={showTerminal}
|
|
onCancel={() => setShowTerminal(false)}
|
|
footer={null}
|
|
width={800}
|
|
centered
|
|
className="terminal-modal"
|
|
styles={{
|
|
header: {
|
|
backgroundColor: '#161b22',
|
|
borderBottom: '1px solid #238636',
|
|
padding: '12px 20px',
|
|
marginBottom: 0,
|
|
display: 'flex',
|
|
alignItems: 'center'
|
|
},
|
|
mask: {
|
|
backgroundColor: 'rgba(0, 0, 0, 0.85)',
|
|
backdropFilter: 'blur(4px)'
|
|
},
|
|
body: {
|
|
padding: '20px',
|
|
backgroundColor: '#0d1117'
|
|
}
|
|
}}
|
|
title={
|
|
<div className="flex items-center gap-2 text-[#2ea043] font-mono tracking-wider text-xs">
|
|
<div className="w-2 h-2 rounded-full bg-[#2ea043] animate-pulse"></div>
|
|
JPL/HORIZONS SYSTEM INTERFACE // {body.name.toUpperCase()}
|
|
</div>
|
|
}
|
|
closeIcon={<X size={18} style={{ color: '#2ea043' }} />}
|
|
>
|
|
<div className="h-[60vh] overflow-auto font-mono text-xs whitespace-pre-wrap scrollbar-none">
|
|
{loading ? (
|
|
<div className="flex items-center justify-center h-full flex-col gap-4 text-[#2ea043]">
|
|
<Spin indicator={<Radar className="animate-spin text-[#2ea043]" size={48} />} />
|
|
<div className="animate-pulse tracking-widest">ESTABLISHING SECURE UPLINK...</div>
|
|
<div className="text-[10px] opacity-50">Connecting to ssd.jpl.nasa.gov...</div>
|
|
</div>
|
|
) : (
|
|
<div className="animate-in fade-in duration-500">
|
|
{terminalData}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
}
|