feat: 创建登录/注册页面,支持用户认证和注册功能。
This commit is contained in:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user