cosmo/frontend/src/components/AuthModal.tsx

213 lines
7.4 KiB
TypeScript

import { useState, useEffect } from 'react';
import { X, User, Lock, Mail, Eye, EyeOff } from 'lucide-react';
import { login, register } from '../utils/api';
import { auth } from '../utils/auth';
import { useToast } from '../contexts/ToastContext';
interface AuthModalProps {
isOpen: boolean;
onClose: () => void;
onLoginSuccess: (user: any) => void;
}
export function AuthModal({ isOpen, onClose, onLoginSuccess }: AuthModalProps) {
const [isLogin, setIsLogin] = useState(true);
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const toast = useToast();
const [formData, setFormData] = useState({
username: '',
password: '',
email: '',
fullName: ''
});
// Reset form when modal opens
useEffect(() => {
if (isOpen) {
setFormData({
username: '',
password: '',
email: '',
fullName: ''
});
setIsLogin(true);
setShowPassword(false);
}
}, [isOpen]);
if (!isOpen) return null;
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
let data;
if (isLogin) {
data = await login(formData.username, formData.password);
} else {
data = await register(
formData.username,
formData.password,
formData.email || undefined,
formData.fullName || undefined
);
}
// Store token and user info
auth.setToken(data.access_token);
auth.setUser(data.user);
toast.success(isLogin ? '登录成功!' : '注册成功!正在为您登录...');
onLoginSuccess(data.user);
onClose();
} catch (err: any) {
console.error('Auth error:', err);
const msg = err.response?.data?.detail || '操作失败,请重试';
const errorMsg = typeof msg === 'string' ? msg : JSON.stringify(msg);
toast.error(errorMsg);
} finally {
setLoading(false);
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const toggleMode = () => {
setIsLogin(!isLogin);
};
return (
<div
className="fixed inset-0 z-[60] flex items-center justify-center bg-black/90 backdrop-blur-xl p-4"
onClick={onClose} // Close modal when clicking outside
>
<div
className="bg-gray-900/90 border border-white/10 rounded-2xl w-full max-w-md shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-200"
onClick={(e) => e.stopPropagation()} // Prevent closing when clicking inside
>
{/* Header */}
<div className="relative p-6 pb-4 border-b border-white/10">
<button
onClick={onClose}
className="absolute top-4 right-4 text-gray-400 hover:text-white transition-colors"
>
<X size={20} />
</button>
<h2 className="text-2xl font-bold text-white text-center">
{isLogin ? '欢迎回来' : '创建账号'}
</h2>
<p className="text-gray-400 text-sm text-center mt-1">
Cosmo - Deep Space Explorer
</p>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="p-6 space-y-4">
<div className="space-y-1">
<label className="text-xs text-gray-400 ml-1"></label>
<div className="relative">
<User size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" />
<input
name="username"
type="text"
required
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"
/>
</div>
</div>
{!isLogin && (
<>
<div className="space-y-1">
<label className="text-xs text-gray-400 ml-1"> ()</label>
<div className="relative">
<Mail size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" />
<input
name="email"
type="email"
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"
/>
</div>
</div>
<div className="space-y-1">
<label className="text-xs text-gray-400 ml-1"> ()</label>
<div className="relative">
<User size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" />
<input
name="fullName"
type="text"
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"
/>
</div>
</div>
</>
)}
<div className="space-y-1">
<label className="text-xs text-gray-400 ml-1"></label>
<div className="relative">
<Lock size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" />
<input
name="password"
type={showPassword ? "text" : "password"}
required
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"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-white transition-colors"
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</div>
<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"
>
{loading ? '处理中...' : (isLogin ? '登录' : '注册并登录')}
</button>
</form>
{/* Footer */}
<div className="p-4 bg-white/5 border-t border-white/5 text-center">
<p className="text-sm text-gray-400">
{isLogin ? '还没有账号?' : '已有账号?'}
<button
onClick={toggleMode}
className="ml-2 text-blue-400 hover:text-blue-300 font-medium transition-colors"
>
{isLogin ? '立即注册' : '去登录'}
</button>
</p>
</div>
</div>
</div>
);
}