优化了布局

main
mula.liu 2025-12-03 13:32:57 +08:00
parent efd2b9051b
commit 72c9b3a243
9 changed files with 144 additions and 60 deletions

View File

@ -124,7 +124,7 @@ export function AuthModal({ isOpen, onClose, onLoginSuccess }: AuthModalProps) {
value={formData.username} value={formData.username}
onChange={handleChange} onChange={handleChange}
placeholder="Username" placeholder="Username"
className="w-full bg-black/50 border border-gray-700 rounded-lg pl-10 pr-4 py-2.5 text-white focus:outline-none focus:border-blue-500 transition-colors" className="w-full bg-black/50 border border-gray-700 rounded-lg pl-10 pr-4 py-2.5 text-white focus:outline-none focus:border-[#238636] transition-colors"
/> />
</div> </div>
</div> </div>
@ -141,7 +141,7 @@ export function AuthModal({ isOpen, onClose, onLoginSuccess }: AuthModalProps) {
value={formData.email} value={formData.email}
onChange={handleChange} onChange={handleChange}
placeholder="Email" placeholder="Email"
className="w-full bg-black/50 border border-gray-700 rounded-lg pl-10 pr-4 py-2.5 text-white focus:outline-none focus:border-blue-500 transition-colors" className="w-full bg-black/50 border border-gray-700 rounded-lg pl-10 pr-4 py-2.5 text-white focus:outline-none focus:border-[#238636] transition-colors"
/> />
</div> </div>
</div> </div>
@ -155,7 +155,7 @@ export function AuthModal({ isOpen, onClose, onLoginSuccess }: AuthModalProps) {
value={formData.fullName} value={formData.fullName}
onChange={handleChange} onChange={handleChange}
placeholder="Full Name" placeholder="Full Name"
className="w-full bg-black/50 border border-gray-700 rounded-lg pl-10 pr-4 py-2.5 text-white focus:outline-none focus:border-blue-500 transition-colors" className="w-full bg-black/50 border border-gray-700 rounded-lg pl-10 pr-4 py-2.5 text-white focus:outline-none focus:border-[#238636] transition-colors"
/> />
</div> </div>
</div> </div>
@ -173,7 +173,7 @@ export function AuthModal({ isOpen, onClose, onLoginSuccess }: AuthModalProps) {
value={formData.password} value={formData.password}
onChange={handleChange} onChange={handleChange}
placeholder="Password" placeholder="Password"
className="w-full bg-black/50 border border-gray-700 rounded-lg pl-10 pr-10 py-2.5 text-white focus:outline-none focus:border-blue-500 transition-colors" className="w-full bg-black/50 border border-gray-700 rounded-lg pl-10 pr-10 py-2.5 text-white focus:outline-none focus:border-[#238636] transition-colors"
/> />
<button <button
type="button" type="button"
@ -188,7 +188,7 @@ export function AuthModal({ isOpen, onClose, onLoginSuccess }: AuthModalProps) {
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="w-full py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-all shadow-lg shadow-blue-600/20 disabled:opacity-50 disabled:cursor-not-allowed mt-2" className="w-full py-3 bg-[#238636] hover:bg-[#2ea043] text-white rounded-lg font-medium transition-all shadow-lg shadow-[#238636]/20 disabled:opacity-50 disabled:cursor-not-allowed mt-2"
> >
{loading ? '处理中...' : (isLogin ? '登录' : '注册并登录')} {loading ? '处理中...' : (isLogin ? '登录' : '注册并登录')}
</button> </button>
@ -200,7 +200,7 @@ export function AuthModal({ isOpen, onClose, onLoginSuccess }: AuthModalProps) {
{isLogin ? '还没有账号?' : '已有账号?'} {isLogin ? '还没有账号?' : '已有账号?'}
<button <button
onClick={toggleMode} onClick={toggleMode}
className="ml-2 text-blue-400 hover:text-blue-300 font-medium transition-colors" className="ml-2 text-[#4ade80] hover:text-[#2ea043] font-medium transition-colors"
> >
{isLogin ? '立即注册' : '去登录'} {isLogin ? '立即注册' : '去登录'}
</button> </button>

View File

@ -112,7 +112,9 @@ function Planet({ body, size, emissive, emissiveIntensity, allBodies, isSelected
} }
}) })
.catch((err) => { .catch((err) => {
console.error(`Failed to load texture for ${body.name}:`, err); if (import.meta.env.DEV) {
console.error(`Failed to load texture for ${body.name}:`, err);
}
setTexturePath(null); setTexturePath(null);
}); });
}, [body.id, body.name]); }, [body.id, body.name]);

View File

@ -34,8 +34,8 @@ export function ControlPanel({
}: ControlPanelProps) { }: ControlPanelProps) {
const buttonClass = (isActive: boolean) => ` const buttonClass = (isActive: boolean) => `
p-2 rounded-lg transition-all duration-200 relative group p-2 rounded-lg transition-all duration-200 relative group
${isActive ${isActive
? 'bg-blue-600 text-white shadow-lg shadow-blue-500/50' ? 'bg-[#238636] text-white shadow-lg shadow-[#238636]/50'
: 'bg-white/10 text-gray-300 hover:bg-white/20 border border-white/5' : 'bg-white/10 text-gray-300 hover:bg-white/20 border border-white/5'
} }
`; `;

View File

@ -77,9 +77,9 @@ export function FocusInfo({ body, onClose, toast }: FocusInfoProps) {
{body.name_zh || body.name} {body.name_zh || body.name}
</h2> </h2>
<span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider border ${ <span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider border ${
isProbe isProbe
? 'bg-purple-500/20 border-purple-500/40 text-purple-300' ? 'bg-purple-500/20 border-purple-500/40 text-purple-300'
: 'bg-blue-500/20 border-blue-500/40 text-blue-300' : 'bg-[#238636]/20 border-[#238636]/40 text-[#4ade80]'
}`}> }`}>
{isProbe ? '探测器' : '天体'} {isProbe ? '探测器' : '天体'}
</span> </span>
@ -94,7 +94,7 @@ export function FocusInfo({ body, onClose, toast }: FocusInfoProps) {
<div className="grid grid-cols-2 gap-2 mb-2"> <div className="grid grid-cols-2 gap-2 mb-2">
{/* Column 1: Heliocentric Distance Card */} {/* Column 1: Heliocentric Distance Card */}
<div className="bg-white/5 rounded-lg p-2.5 flex items-center gap-2.5 border border-white/5 h-[52px]"> <div className="bg-white/5 rounded-lg p-2.5 flex items-center gap-2.5 border border-white/5 h-[52px]">
<div className="p-1.5 rounded-full bg-blue-500/20 text-blue-400"> <div className="p-1.5 rounded-full bg-[#238636]/20 text-[#4ade80]">
<Ruler size={14} /> <Ruler size={14} />
</div> </div>
<div> <div>

View File

@ -1,15 +1,111 @@
/** /**
* Loading component * Loading component with green theme
*/ */
export function Loading() { import { useEffect, useState } from 'react';
interface LoadingProps {
message?: string;
showProgress?: boolean;
}
export function Loading({ message = "正在加载深空探索器...", showProgress = true }: LoadingProps) {
const [dots, setDots] = useState('');
const [progress, setProgress] = useState(0);
// Animate dots
useEffect(() => {
const interval = setInterval(() => {
setDots(prev => prev.length >= 3 ? '' : prev + '.');
}, 500);
return () => clearInterval(interval);
}, []);
// Simulate progress
useEffect(() => {
if (!showProgress) return;
const interval = setInterval(() => {
setProgress(prev => {
if (prev >= 90) return prev; // Stop at 90% until real data loads
return prev + Math.random() * 10;
});
}, 300);
return () => clearInterval(interval);
}, [showProgress]);
return ( return (
<div className="w-full h-full flex items-center justify-center bg-black text-white"> <div className="fixed inset-0 w-full h-full flex items-center justify-center bg-black z-50">
<div className="text-center"> {/* Starfield background effect */}
<div className="mb-4"> <div className="absolute inset-0 overflow-hidden">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div> {[...Array(50)].map((_, i) => (
</div> <div
<p className="text-lg">Loading celestial data from NASA JPL Horizons...</p> key={i}
className="absolute w-1 h-1 bg-white rounded-full opacity-50"
style={{
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
animation: `twinkle ${2 + Math.random() * 3}s infinite`,
animationDelay: `${Math.random() * 2}s`
}}
/>
))}
</div> </div>
{/* Loading content */}
<div className="relative z-10 text-center">
{/* Spinning loader with green theme */}
<div className="mb-6 flex justify-center">
<div className="relative">
{/* Outer ring */}
<div className="w-24 h-24 rounded-full border-4 border-[#238636]/20"></div>
{/* Spinning ring */}
<div className="absolute inset-0 w-24 h-24 rounded-full border-4 border-transparent border-t-[#238636] border-r-[#238636] animate-spin"></div>
{/* Inner glow */}
<div className="absolute inset-0 w-24 h-24 rounded-full shadow-[0_0_30px_rgba(35,134,54,0.3)]"></div>
{/* Center icon */}
<div className="absolute inset-0 flex items-center justify-center">
<svg className="w-10 h-10 text-[#238636]" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z" />
</svg>
</div>
</div>
</div>
{/* Loading text with green theme */}
<div className="text-[#238636] text-xl font-medium mb-2">
{message}{dots}
</div>
{/* Subtitle */}
<div className="text-gray-400 text-sm mb-6">
</div>
{/* Progress bar */}
{showProgress && (
<div className="w-64 mx-auto">
<div className="h-1 bg-white/10 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-[#238636] to-[#2ea043] rounded-full transition-all duration-300 shadow-[0_0_10px_rgba(35,134,54,0.5)]"
style={{ width: `${progress}%` }}
/>
</div>
<div className="text-[#238636] text-xs mt-2">
{Math.round(progress)}%
</div>
</div>
)}
</div>
{/* Add twinkle animation */}
<style>{`
@keyframes twinkle {
0%, 100% { opacity: 0.2; }
50% { opacity: 0.8; }
}
`}</style>
</div> </div>
); );
} }

View File

@ -227,12 +227,10 @@ export function Probe({ body, allBodies, isSelected = false }: ProbeProps) {
// Fetch model from backend API // Fetch model from backend API
useEffect(() => { useEffect(() => {
console.log(`[Probe ${body.name}] Fetching resources...`);
setLoadError(false); // Reset error state setLoadError(false); // Reset error state
fetchBodyResources(body.id, 'model') fetchBodyResources(body.id, 'model')
.then((response) => { .then((response) => {
console.log(`[Probe ${body.name}] Resources response:`, response);
if (response.resources.length > 0) { if (response.resources.length > 0) {
// Get the first model resource // Get the first model resource
const modelResource = response.resources[0]; const modelResource = response.resources[0];
@ -240,47 +238,40 @@ export function Probe({ body, allBodies, isSelected = false }: ProbeProps) {
// Get scale from extra_data // Get scale from extra_data
const scale = modelResource.extra_data?.scale || 1.0; const scale = modelResource.extra_data?.scale || 1.0;
setResourceScale(scale); setResourceScale(scale);
console.log(`[Probe ${body.name}] Scale from resource:`, scale);
// Construct path for Nginx proxy // Construct path for Nginx proxy
// Database stores relative path like "texture/2k_mercury.jpg" or "model/webb.glb" // Database stores relative path like "texture/2k_mercury.jpg" or "model/webb.glb"
// We need to add /upload/ prefix for Nginx to proxy to backend // We need to add /upload/ prefix for Nginx to proxy to backend
const fullPath = `/upload/${modelResource.file_path}`; const fullPath = `/upload/${modelResource.file_path}`;
console.log(`[Probe ${body.name}] Model path:`, fullPath);
// Preload the model before setting the path // Preload the model before setting the path
useGLTF.preload(fullPath); useGLTF.preload(fullPath);
console.log(`[Probe ${body.name}] Model preloaded`);
setModelPath(fullPath); setModelPath(fullPath);
} else { } else {
console.log(`[Probe ${body.name}] No resources found, using fallback`);
setModelPath(null); setModelPath(null);
} }
}) })
.catch((err) => { .catch((err) => {
console.error(`[Probe ${body.name}] Failed to load model:`, err); if (import.meta.env.DEV) {
console.error(`[Probe ${body.name}] Failed to load model:`, err);
}
setLoadError(true); setLoadError(true);
setModelPath(null); setModelPath(null);
}); });
}, [body.id, body.name]); }, [body.id, body.name]);
if (!position) { if (!position) {
console.log(`[Probe ${body.name}] No position data`);
return null; return null;
} }
if (modelPath === undefined) { if (modelPath === undefined) {
console.log(`[Probe ${body.name}] Waiting for model path...`);
return null; // Wait for model to load return null; // Wait for model to load
} }
console.log(`[Probe ${body.name}] Rendering with modelPath:`, modelPath, 'loadError:', loadError);
// Use model if available and no load error, otherwise use fallback // Use model if available and no load error, otherwise use fallback
if (modelPath && !loadError) { if (modelPath && !loadError) {
return <ProbeModel body={body} modelPath={modelPath} allBodies={allBodies} isSelected={isSelected} resourceScale={resourceScale} onError={() => { return <ProbeModel body={body} modelPath={modelPath} allBodies={allBodies} isSelected={isSelected} resourceScale={resourceScale} onError={() => {
console.error(`[Probe ${body.name}] ProbeModel rendering failed, switching to fallback`);
setLoadError(true); setLoadError(true);
}} />; }} />;
} }

View File

@ -85,7 +85,7 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
placeholder="搜索天体..." placeholder="搜索天体..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} 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" 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-[#238636]/50 transition-colors"
/> />
</div> </div>
</div> </div>
@ -178,7 +178,7 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
absolute top-0 ${isCollapsed ? 'left-0' : '-right-4'} absolute top-0 ${isCollapsed ? 'left-0' : '-right-4'}
w-8 h-8 flex items-center justify-center w-8 h-8 flex items-center justify-center
bg-black/80 backdrop-blur-md border border-white/10 rounded-full bg-black/80 backdrop-blur-md border border-white/10 rounded-full
text-white hover:bg-blue-600 transition-all shadow-lg z-50 text-white hover:bg-[#238636] transition-all shadow-lg z-50
${!isCollapsed && 'translate-x-1/2'} ${!isCollapsed && 'translate-x-1/2'}
`} `}
style={{ top: '20px' }} style={{ top: '20px' }}
@ -256,16 +256,16 @@ function BodyItem({ body, distance, isSelected, onClick }: {
disabled={isInactive} disabled={isInactive}
className={` className={`
w-full flex items-center justify-between p-2 rounded-lg text-left transition-all duration-200 group w-full flex items-center justify-between p-2 rounded-lg text-left transition-all duration-200 group
${isSelected ${isSelected
? 'bg-blue-600/20 border border-blue-500/50 shadow-[0_0_15px_rgba(37,99,235,0.2)]' ? 'bg-[#238636]/20 border border-[#238636]/50 shadow-[0_0_15px_rgba(35,134,54,0.2)]'
: isInactive : isInactive
? 'opacity-40 cursor-not-allowed' ? 'opacity-40 cursor-not-allowed'
: 'hover:bg-white/10 border border-transparent' : 'hover:bg-white/10 border border-transparent'
} }
`} `}
> >
<div> <div>
<div className={`text-xs font-medium ${isSelected ? 'text-blue-200' : 'text-gray-200 group-hover:text-white'}`}> {/* text-sm -> text-xs */} <div className={`text-xs font-medium ${isSelected ? 'text-[#4ade80]' : 'text-gray-200 group-hover:text-white'}`}> {/* text-sm -> text-xs */}
{body.name_zh || body.name} {body.name_zh || body.name}
</div> </div>
<div className="text-[9px] text-gray-500 font-mono"> {/* text-[10px] -> text-[9px] */} <div className="text-[9px] text-gray-500 font-mono"> {/* text-[10px] -> text-[9px] */}
@ -274,7 +274,7 @@ function BodyItem({ body, distance, isSelected, onClick }: {
</div> </div>
{isSelected && ( {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" /> <div className="w-1.5 h-1.5 rounded-full bg-[#4ade80] shadow-[0_0_8px_rgba(74,222,128,0.8)] animate-pulse" />
)} )}
</button> </button>
); );

View File

@ -183,9 +183,9 @@ export function TimelineController({ onTimeChange, minDate, maxDate }: TimelineC
onClick={handlePlayPause} onClick={handlePlayPause}
className={` className={`
p-3 rounded-full text-white shadow-lg transition-all transform hover:scale-105 active:scale-95 p-3 rounded-full text-white shadow-lg transition-all transform hover:scale-105 active:scale-95
${isPlaying ${isPlaying
? 'bg-amber-500 hover:bg-amber-600 shadow-amber-500/30' ? 'bg-amber-500 hover:bg-amber-600 shadow-amber-500/30'
: 'bg-blue-600 hover:bg-blue-700 shadow-blue-600/30' : 'bg-[#238636] hover:bg-[#2ea043] shadow-[#238636]/30'
} }
`} `}
title={isPlaying ? "暂停" : "播放"} title={isPlaying ? "暂停" : "播放"}

View File

@ -8,61 +8,56 @@ import { auth } from './auth';
// Dynamically determine the API base URL // Dynamically determine the API base URL
const getApiBaseUrl = () => { const getApiBaseUrl = () => {
if (import.meta.env.VITE_API_BASE_URL) { if (import.meta.env.VITE_API_BASE_URL) {
console.log('[API] Using VITE_API_BASE_URL:', import.meta.env.VITE_API_BASE_URL);
return import.meta.env.VITE_API_BASE_URL; return import.meta.env.VITE_API_BASE_URL;
} }
// In production, use relative path /api (proxied by Nginx) // In production, use relative path /api (proxied by Nginx)
// This works for both internal IP and external domain access // This works for both internal IP and external domain access
if (import.meta.env.PROD) { if (import.meta.env.PROD) {
console.log('[API] Using production relative path: /api');
return '/api'; return '/api';
} }
// In development, proxy is configured in vite.config.ts // In development, proxy is configured in vite.config.ts
console.log('[API] Using development relative path: /api');
return '/api'; return '/api';
}; };
const API_BASE_URL = getApiBaseUrl(); const API_BASE_URL = getApiBaseUrl();
console.log('[API] Final API_BASE_URL:', API_BASE_URL);
export const api = axios.create({ export const api = axios.create({
baseURL: API_BASE_URL, baseURL: API_BASE_URL,
timeout: 120000, // Increase timeout to 120 seconds for historical data queries timeout: 120000, // Increase timeout to 120 seconds for historical data queries
}); });
// Add request interceptor for debugging and auth // Add request interceptor for auth
api.interceptors.request.use( api.interceptors.request.use(
(config) => { (config) => {
console.log('[API Request]', config.method?.toUpperCase(), config.url, config.params);
// Add token if available // Add token if available
const token = auth.getToken(); const token = auth.getToken();
if (token) { if (token) {
config.headers.Authorization = `Bearer ${token}`; config.headers.Authorization = `Bearer ${token}`;
} }
return config; return config;
}, },
(error) => { (error) => {
console.error('[API Request Error]', error);
return Promise.reject(error); return Promise.reject(error);
} }
); );
// Add response interceptor for debugging // Add response interceptor for error handling
api.interceptors.response.use( api.interceptors.response.use(
(response) => { (response) => {
console.log('[API Response]', response.config.url, response.status, 'Data:', response.data);
return response; return response;
}, },
(error) => { (error) => {
console.error('[API Error]', error.config?.url, error.message); // Only log errors in development
if (error.response) { if (import.meta.env.DEV) {
console.error('[API Error Response]', error.response.status, error.response.data); console.error('[API Error]', error.config?.url, error.message);
} else if (error.request) { if (error.response) {
console.error('[API Error Request]', error.request); console.error('[API Error Response]', error.response.status, error.response.data);
} else if (error.request) {
console.error('[API Error Request]', error.request);
}
} }
return Promise.reject(error); return Promise.reject(error);
} }