cosmo/frontend/src/components/ProbeList.tsx

176 lines
6.2 KiB
TypeScript
Raw Normal View History

import { useState } from 'react';
2025-11-29 16:58:58 +00:00
import { ChevronLeft, ChevronRight, Search, Globe, Rocket } from 'lucide-react';
import type { CelestialBody } from '../types';
interface ProbeListProps {
probes: CelestialBody[];
planets: CelestialBody[];
onBodySelect: (body: CelestialBody | null) => void;
selectedBody: CelestialBody | null;
2025-11-30 02:43:47 +00:00
onResetCamera: () => void;
}
2025-11-30 02:43:47 +00:00
export function ProbeList({ probes, planets, onBodySelect, selectedBody, onResetCamera }: ProbeListProps) {
2025-11-29 16:58:58 +00:00
const [isCollapsed, setIsCollapsed] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
2025-11-29 16:58:58 +00:00
// Calculate distance for sorting
const calculateDistance = (body: CelestialBody) => {
const pos = body.positions[0];
return Math.sqrt(pos.x ** 2 + pos.y ** 2 + pos.z ** 2);
};
2025-11-29 16:58:58 +00:00
const processBodies = (list: CelestialBody[]) => {
return list
.filter(b =>
(b.name_zh || b.name).toLowerCase().includes(searchTerm.toLowerCase()) &&
b.type !== 'star' // Exclude Sun from list
)
.map(body => ({
body,
distance: calculateDistance(body)
}))
.sort((a, b) => a.distance - b.distance);
};
2025-11-29 16:58:58 +00:00
const planetList = processBodies(planets);
const probeList = processBodies(probes);
return (
2025-11-29 16:58:58 +00:00
<div
className={`
absolute top-24 left-4 bottom-8 z-40
transition-all duration-300 ease-in-out flex
${isCollapsed ? 'w-12' : 'w-64'} // Adjusted width
`}
>
{/* Main Content Panel */}
<div className={`
flex-1 bg-black/80 backdrop-blur-md border border-white/10 rounded-2xl overflow-hidden flex flex-col
transition-opacity duration-300
${isCollapsed ? 'opacity-0 pointer-events-none' : 'opacity-100'}
`}>
{/* Header & Search */}
<div className="p-4 border-b border-white/10 space-y-3">
<div className="flex items-center justify-between text-white">
2025-11-30 02:43:47 +00:00
<h2 className="font-bold text-base tracking-wide"></h2>
2025-11-29 16:58:58 +00:00
<button
2025-11-30 02:43:47 +00:00
onClick={() => {
onBodySelect(null);
onResetCamera();
}}
2025-11-29 16:58:58 +00:00
className="text-xs bg-white/10 hover:bg-white/20 px-2 py-1 rounded transition-colors text-gray-300"
>
</button>
</div>
2025-11-29 16:58:58 +00:00
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" size={14} />
<input
type="text"
placeholder="搜索天体..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full bg-white/5 border border-white/10 rounded-lg pl-9 pr-3 py-2 text-xs text-white placeholder-gray-500 focus:outline-none focus:border-blue-500/50 transition-colors"
/>
</div>
</div>
2025-11-29 16:58:58 +00:00
{/* List Content */}
<div className="flex-1 overflow-y-auto custom-scrollbar p-2 space-y-4">
{/* Planets Group */}
<div>
<div className="px-2 mb-2 flex items-center gap-2 text-[10px] font-bold text-gray-400 uppercase tracking-wider"> {/* text-xs -> text-[10px] */}
<Globe size={12} />
({planetList.length})
</div>
<div className="space-y-1">
{planetList.map(({ body, distance }) => (
<BodyItem
key={body.id}
body={body}
distance={distance}
isSelected={selectedBody?.id === body.id}
onClick={() => onBodySelect(body)}
/>
))}
</div>
</div>
2025-11-29 15:10:00 +00:00
2025-11-29 16:58:58 +00:00
{/* Probes Group */}
<div>
<div className="px-2 mb-2 flex items-center gap-2 text-[10px] font-bold text-gray-400 uppercase tracking-wider"> {/* text-xs -> text-[10px] */}
<Rocket size={12} />
({probeList.length})
</div>
<div className="space-y-1">
{probeList.map(({ body, distance }) => (
<BodyItem
key={body.id}
body={body}
distance={distance}
isSelected={selectedBody?.id === body.id}
onClick={() => onBodySelect(body)}
/>
))}
</div>
</div>
2025-11-29 16:58:58 +00:00
</div>
</div>
2025-11-29 16:58:58 +00:00
{/* Toggle Button (Attached to the side) */}
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className={`
absolute top-0 ${isCollapsed ? 'left-0' : '-right-4'}
w-8 h-8 flex items-center justify-center
bg-black/80 backdrop-blur-md border border-white/10 rounded-full
text-white hover:bg-blue-600 transition-all shadow-lg z-50
${!isCollapsed && 'translate-x-1/2'}
`}
style={{ top: '20px' }}
>
{isCollapsed ? <ChevronRight size={16} /> : <ChevronLeft size={16} />}
</button>
</div>
);
}
2025-11-29 16:58:58 +00:00
function BodyItem({ body, distance, isSelected, onClick }: {
body: CelestialBody,
distance: number,
isSelected: boolean,
onClick: () => void
}) {
const isInactive = body.is_active === false;
return (
<button
onClick={isInactive ? undefined : onClick}
disabled={isInactive}
className={`
w-full flex items-center justify-between p-2 rounded-lg text-left transition-all duration-200 group
${isSelected
? 'bg-blue-600/20 border border-blue-500/50 shadow-[0_0_15px_rgba(37,99,235,0.2)]'
: isInactive
? 'opacity-40 cursor-not-allowed'
: 'hover:bg-white/10 border border-transparent'
}
`}
>
<div>
<div className={`text-xs font-medium ${isSelected ? 'text-blue-200' : 'text-gray-200 group-hover:text-white'}`}> {/* text-sm -> text-xs */}
{body.name_zh || body.name}
</div>
<div className="text-[9px] text-gray-500 font-mono"> {/* text-[10px] -> text-[9px] */}
{distance.toFixed(2)} AU
</div>
</div>
{isSelected && (
<div className="w-1.5 h-1.5 rounded-full bg-blue-400 shadow-[0_0_8px_rgba(96,165,250,0.8)] animate-pulse" />
)}
</button>
);
}