FE/src/components/auth/LoginPage.tsx

314 lines
9.5 KiB
TypeScript

import React, { useState } from 'react';
import usersData from '../../user.json';
import { Eye, EyeOff, ShieldCheck, LogIn } from 'lucide-react';
const users = usersData as Record<string, string>;
const DEFAULT_PASSWORD = 'admin123';
export interface LoginUser {
name: string;
openId: string;
}
interface LoginPageProps {
onLogin: (user: LoginUser) => void;
}
const LoginPage: React.FC<LoginPageProps> = ({ onLogin }) => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (!username.trim()) {
setError('请输入用户名');
return;
}
if (!users[username]) {
setError('用户名不存在');
return;
}
if (password !== DEFAULT_PASSWORD) {
setError('密码错误');
return;
}
setLoading(true);
setTimeout(() => {
onLogin({ name: username, openId: users[username] });
setLoading(false);
}, 600);
};
return (
<div className="login-root">
<div className="login-bg">
<div className="login-orb orb1" />
<div className="login-orb orb2" />
<div className="login-orb orb3" />
</div>
<div className="login-card glass">
<div className="login-logo">
<div className="logo-icon-lg">Q</div>
<h1 className="login-title">QuantumTest</h1>
<p className="login-subtitle"></p>
</div>
<form className="login-form" onSubmit={handleSubmit}>
<div className="form-group">
<label className="form-label"></label>
<input
type="text"
className={`login-input ${error && !password ? 'input-error' : ''}`}
placeholder=""
value={username}
onChange={e => { setUsername(e.target.value); setError(''); }}
autoFocus
/>
</div>
<div className="form-group">
<label className="form-label"></label>
<div className="password-wrapper">
<input
type={showPassword ? 'text' : 'password'}
className={`login-input ${error && password ? 'input-error' : ''}`}
placeholder="请输入密码"
value={password}
onChange={e => { setPassword(e.target.value); setError(''); }}
/>
<button
type="button"
className="pw-toggle"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div>
{error && (
<div className="login-error">
<ShieldCheck size={14} />
{error}
</div>
)}
<button
type="submit"
className="login-btn"
disabled={loading}
>
{loading ? (
<span className="btn-loading">
<span className="spinner" />
...
</span>
) : (
<>
<LogIn size={16} />
</>
)}
</button>
</form>
<p className="login-hint">默认密码: admin123</p>
</div>
<style>{`
.login-root {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #0A0F1E;
position: relative;
overflow: hidden;
font-family: 'Inter', -apple-system, sans-serif;
}
.login-bg {
position: absolute;
inset: 0;
pointer-events: none;
}
.login-orb {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.35;
}
.orb1 {
width: 500px; height: 500px;
background: radial-gradient(circle, #165DFF, transparent);
top: -150px; left: -100px;
animation: orbFloat 8s ease-in-out infinite;
}
.orb2 {
width: 400px; height: 400px;
background: radial-gradient(circle, #36ABFF, transparent);
bottom: -100px; right: -80px;
animation: orbFloat 10s ease-in-out infinite reverse;
}
.orb3 {
width: 300px; height: 300px;
background: radial-gradient(circle, #722ED1, transparent);
top: 40%; left: 55%;
animation: orbFloat 6s ease-in-out infinite;
}
@keyframes orbFloat {
0%, 100% { transform: translateY(0px) scale(1); }
50% { transform: translateY(-30px) scale(1.05); }
}
.login-card {
position: relative;
width: 420px;
padding: 48px 40px;
border-radius: 20px;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.12);
backdrop-filter: blur(20px);
box-shadow: 0 32px 64px rgba(0,0,0,0.4);
animation: cardIn 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes cardIn {
from { opacity: 0; transform: translateY(30px) scale(0.96); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.login-logo {
text-align: center;
margin-bottom: 36px;
}
.logo-icon-lg {
width: 56px; height: 56px;
background: linear-gradient(135deg, #165DFF, #36ABFF);
color: white;
font-size: 24px;
font-weight: 700;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 14px;
box-shadow: 0 8px 24px rgba(22,93,255,0.4);
}
.login-title {
font-size: 22px;
font-weight: 700;
color: white;
margin: 0 0 4px;
}
.login-subtitle {
font-size: 13px;
color: rgba(255,255,255,0.5);
margin: 0;
}
.login-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-label {
display: block;
font-size: 13px;
font-weight: 500;
color: rgba(255,255,255,0.7);
margin-bottom: 8px;
}
.login-input {
width: 100%;
padding: 12px 16px;
background: rgba(255,255,255,0.07);
border: 1px solid rgba(255,255,255,0.12);
border-radius: 10px;
color: white;
font-size: 14px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
}
.login-input::placeholder { color: rgba(255,255,255,0.3); }
.login-input:focus {
border-color: #165DFF;
box-shadow: 0 0 0 3px rgba(22,93,255,0.25);
}
.input-error { border-color: #F53F3F !important; }
.password-wrapper { position: relative; }
.pw-toggle {
position: absolute; right: 12px; top: 50%;
transform: translateY(-50%);
background: none; border: none; cursor: pointer;
color: rgba(255,255,255,0.4);
display: flex; align-items: center;
}
.pw-toggle:hover { color: rgba(255,255,255,0.8); }
.login-error {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 14px;
background: rgba(245,63,63,0.12);
border: 1px solid rgba(245,63,63,0.3);
border-radius: 8px;
color: #FF7D7D;
font-size: 13px;
animation: shake 0.3s ease;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-6px); }
75% { transform: translateX(6px); }
}
.login-btn {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #165DFF, #36ABFF);
color: white;
border: none;
border-radius: 10px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: opacity 0.2s, transform 0.15s, box-shadow 0.2s;
box-shadow: 0 4px 16px rgba(22,93,255,0.35);
margin-top: 4px;
}
.login-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 8px 24px rgba(22,93,255,0.45);
}
.login-btn:active:not(:disabled) { transform: translateY(0); }
.login-btn:disabled { opacity: 0.7; cursor: not-allowed; }
.btn-loading {
display: flex; align-items: center; gap: 8px;
}
.spinner {
width: 16px; height: 16px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.login-hint {
text-align: center;
font-size: 12px;
color: rgba(255,255,255,0.25);
margin: 20px 0 0;
}
`}</style>
</div>
);
};
export default LoginPage;