优化了布局
parent
efd2b9051b
commit
72c9b3a243
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export function ControlPanel({
|
||||||
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'
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ export function FocusInfo({ body, onClose, toast }: FocusInfoProps) {
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}} />;
|
}} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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' }}
|
||||||
|
|
@ -257,7 +257,7 @@ function BodyItem({ body, distance, isSelected, onClick }: {
|
||||||
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'
|
||||||
|
|
@ -265,7 +265,7 @@ function BodyItem({ body, distance, isSelected, onClick }: {
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -185,7 +185,7 @@ export function TimelineController({ onTimeChange, minDate, maxDate }: TimelineC
|
||||||
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 ? "暂停" : "播放"}
|
||||||
|
|
|
||||||
|
|
@ -8,35 +8,29 @@ 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) {
|
||||||
|
|
@ -46,23 +40,24 @@ api.interceptors.request.use(
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue