314 lines
9.5 KiB
TypeScript
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;
|