feat: 创建登录/注册页面,支持用户认证和注册功能。

This commit is contained in:
2026-01-31 12:57:15 +08:00
parent 3e464efcb2
commit 1d698e3c51

View File

@@ -0,0 +1,255 @@
import { useState, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { Icon } from '@iconify/react';
import { motion } from 'framer-motion';
import authService from '../../services/authService';
import './Login.css';
interface LoginProps {
mode?: 'login' | 'register';
}
export default function Login({ mode = 'login' }: LoginProps) {
const navigate = useNavigate();
const location = useLocation();
const [isLogin, setIsLogin] = useState(mode === 'login');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
// Shared form data or separate? Let's use shared for email/password continuity
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
});
// Redirect if authenticated
useEffect(() => {
if (authService.isAuthenticated()) {
const from = (location.state as { from?: { pathname: string } })?.from?.pathname || '/home';
navigate(from, { replace: true });
}
}, [navigate, location]);
// Sync prop mode
useEffect(() => {
setIsLogin(mode === 'login');
}, [mode]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
setError('');
};
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
await authService.register({
email: formData.email,
password: formData.password,
username: formData.username,
});
navigate('/home');
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : '注册失败,请重试';
setError(errorMessage);
} finally {
setLoading(false);
}
};
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
await authService.login({
email: formData.email,
password: formData.password,
});
navigate('/home');
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : '登录失败,请重试';
setError(errorMessage);
} finally {
setLoading(false);
}
};
const Particles = () => {
return (
<div className="particles-container" style={{ position: 'absolute', inset: 0, overflow: 'hidden', pointerEvents: 'none', zIndex: 0 }}>
{[...Array(15)].map((_, i) => (
<motion.div
key={i}
initial={{
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
opacity: Math.random() * 0.5 + 0.1,
}}
animate={{
y: [null, Math.random() * window.innerHeight],
x: [null, Math.random() * window.innerWidth],
}}
transition={{
duration: Math.random() * 20 + 20,
repeat: Infinity,
repeatType: "reverse",
ease: "linear",
}}
style={{
position: 'absolute',
width: Math.random() * 4 + 2 + 'px',
height: Math.random() * 4 + 2 + 'px',
borderRadius: '50%',
backgroundColor: i % 2 === 0 ? '#6366f1' : '#ec4899',
filter: 'blur(1px)',
}}
/>
))}
</div>
);
};
return (
<div className="login-page">
<Particles />
<div className={`container ${!isLogin ? 'right-panel-active' : ''}`} id="container">
{/* Sign Up Form */}
<div className="form-container sign-up-container">
<form onSubmit={handleRegister}>
<h1></h1>
<div className="social-container">
<a href={authService.getGitHubLoginUrl()} title="Github Login">
<Icon icon="mdi:github" width="20" />
</a>
<a href={authService.getGiteeLoginUrl()} title="Gitee Login">
<Icon icon="simple-icons:gitee" width="20" />
</a>
</div>
<span>使</span>
<div className="login-field">
<input
type="text"
name="username"
className="login-input"
placeholder="用户名"
value={formData.username}
onChange={handleChange}
required={!isLogin}
/>
</div>
<div className="login-field">
<input
type="email"
name="email"
className="login-input"
placeholder="邮箱"
value={formData.email}
onChange={handleChange}
required
/>
</div>
<div className="login-field">
<input
type="password"
name="password"
className="login-input"
placeholder="密码"
value={formData.password}
onChange={handleChange}
required
minLength={8}
/>
</div>
{error && !isLogin && <p style={{ color: 'red', margin: '5px 0' }}>{error}</p>}
<button className="login-button" disabled={loading}>
{loading ? <Icon icon="line-md:loading-twotone-loop" /> : '注册'}
</button>
<div className="mobile-toggle">
<span onClick={() => setIsLogin(true)}></span>
</div>
</form>
</div>
{/* Sign In Form */}
<div className="form-container sign-in-container">
<form onSubmit={handleLogin}>
<h1></h1>
<div className="social-container">
<a href={authService.getGitHubLoginUrl()} title="Github Login">
<Icon icon="mdi:github" width="20" />
</a>
<a href={authService.getGiteeLoginUrl()} title="Gitee Login">
<Icon icon="simple-icons:gitee" width="20" />
</a>
</div>
<span>使</span>
<div className="login-field">
<input
type="email"
name="email"
className="login-input"
placeholder="邮箱"
value={formData.email}
onChange={handleChange}
required
/>
</div>
<div className="login-field">
<input
type="password"
name="password"
className="login-input"
placeholder="密码"
value={formData.password}
onChange={handleChange}
required
/>
</div>
<p style={{ margin: '10px 0', fontSize: '12px', cursor: 'pointer' }}>?</p>
{error && isLogin && <p style={{ color: 'red', margin: '5px 0' }}>{error}</p>}
<button className="login-button" disabled={loading}>
{loading ? <Icon icon="line-md:loading-twotone-loop" /> : '登录'}
</button>
<div className="mobile-toggle">
<span onClick={() => setIsLogin(false)}></span>
</div>
</form>
</div>
{/* Overlay Container */}
<div className="overlay-container">
<div className="overlay">
<div className="overlay-panel overlay-left">
<h1>!</h1>
<p></p>
<button className="ghost" onClick={() => setIsLogin(true)}>
</button>
</div>
<div className="overlay-panel overlay-right">
<h1>!</h1>
<p></p>
<button className="ghost" onClick={() => setIsLogin(false)}>
</button>
</div>
</div>
</div>
</div>
</div>
);
}