优化了布局

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}
onChange={handleChange}
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>
@ -141,7 +141,7 @@ export function AuthModal({ isOpen, onClose, onLoginSuccess }: AuthModalProps) {
value={formData.email}
onChange={handleChange}
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>
@ -155,7 +155,7 @@ export function AuthModal({ isOpen, onClose, onLoginSuccess }: AuthModalProps) {
value={formData.fullName}
onChange={handleChange}
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>
@ -173,7 +173,7 @@ export function AuthModal({ isOpen, onClose, onLoginSuccess }: AuthModalProps) {
value={formData.password}
onChange={handleChange}
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
type="button"
@ -188,7 +188,7 @@ export function AuthModal({ isOpen, onClose, onLoginSuccess }: AuthModalProps) {
<button
type="submit"
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 ? '登录' : '注册并登录')}
</button>
@ -200,7 +200,7 @@ export function AuthModal({ isOpen, onClose, onLoginSuccess }: AuthModalProps) {
{isLogin ? '还没有账号?' : '已有账号?'}
<button
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 ? '立即注册' : '去登录'}
</button>

View File

@ -112,7 +112,9 @@ function Planet({ body, size, emissive, emissiveIntensity, allBodies, isSelected
}
})
.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);
});
}, [body.id, body.name]);

View File

@ -34,8 +34,8 @@ export function ControlPanel({
}: ControlPanelProps) {
const buttonClass = (isActive: boolean) => `
p-2 rounded-lg transition-all duration-200 relative group
${isActive
? 'bg-blue-600 text-white shadow-lg shadow-blue-500/50'
${isActive
? 'bg-[#238636] text-white shadow-lg shadow-[#238636]/50'
: '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}
</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
? 'bg-purple-500/20 border-purple-500/40 text-purple-300'
: 'bg-[#238636]/20 border-[#238636]/40 text-[#4ade80]'
}`}>
{isProbe ? '探测器' : '天体'}
</span>
@ -94,7 +94,7 @@ export function FocusInfo({ body, onClose, toast }: FocusInfoProps) {
<div className="grid grid-cols-2 gap-2 mb-2">
{/* 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="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} />
</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 (
<div className="w-full h-full flex items-center justify-center bg-black text-white">
<div className="text-center">
<div className="mb-4">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
</div>
<p className="text-lg">Loading celestial data from NASA JPL Horizons...</p>
<div className="fixed inset-0 w-full h-full flex items-center justify-center bg-black z-50">
{/* Starfield background effect */}
<div className="absolute inset-0 overflow-hidden">
{[...Array(50)].map((_, i) => (
<div
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>
{/* 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>
);
}

View File

@ -227,12 +227,10 @@ export function Probe({ body, allBodies, isSelected = false }: ProbeProps) {
// Fetch model from backend API
useEffect(() => {
console.log(`[Probe ${body.name}] Fetching resources...`);
setLoadError(false); // Reset error state
fetchBodyResources(body.id, 'model')
.then((response) => {
console.log(`[Probe ${body.name}] Resources response:`, response);
if (response.resources.length > 0) {
// Get the first model resource
const modelResource = response.resources[0];
@ -240,47 +238,40 @@ export function Probe({ body, allBodies, isSelected = false }: ProbeProps) {
// Get scale from extra_data
const scale = modelResource.extra_data?.scale || 1.0;
setResourceScale(scale);
console.log(`[Probe ${body.name}] Scale from resource:`, scale);
// Construct path for Nginx proxy
// 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
const fullPath = `/upload/${modelResource.file_path}`;
console.log(`[Probe ${body.name}] Model path:`, fullPath);
// Preload the model before setting the path
useGLTF.preload(fullPath);
console.log(`[Probe ${body.name}] Model preloaded`);
setModelPath(fullPath);
} else {
console.log(`[Probe ${body.name}] No resources found, using fallback`);
setModelPath(null);
}
})
.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);
setModelPath(null);
});
}, [body.id, body.name]);
if (!position) {
console.log(`[Probe ${body.name}] No position data`);
return null;
}
if (modelPath === undefined) {
console.log(`[Probe ${body.name}] Waiting for model path...`);
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
if (modelPath && !loadError) {
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);
}} />;
}

View File

@ -85,7 +85,7 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
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"
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>
@ -178,7 +178,7 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
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
text-white hover:bg-[#238636] transition-all shadow-lg z-50
${!isCollapsed && 'translate-x-1/2'}
`}
style={{ top: '20px' }}
@ -256,16 +256,16 @@ function BodyItem({ body, distance, isSelected, 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'
${isSelected
? 'bg-[#238636]/20 border border-[#238636]/50 shadow-[0_0_15px_rgba(35,134,54,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 */}
<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}
</div>
<div className="text-[9px] text-gray-500 font-mono"> {/* text-[10px] -> text-[9px] */}
@ -274,7 +274,7 @@ function BodyItem({ body, distance, isSelected, onClick }: {
</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" />
<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>
);

View File

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

View File

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