优化了布局
parent
efd2b9051b
commit
72c9b3a243
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 ? "暂停" : "播放"}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue