P4: 登录/注册/个人中心迁移(验证码 proof/邮箱验证/头像/改密)
This commit is contained in:
@@ -1,6 +1,132 @@
|
||||
import React from "react";
|
||||
import Placeholder from "./Placeholder.jsx";
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import * as authApi from '../api/auth.js';
|
||||
import { required as captchaRequired } from '../api/captcha.js';
|
||||
import { showCaptcha } from '../components/CaptchaModal.jsx';
|
||||
import { getToken, setToken, notifyAuthChange } from '../api/client.js';
|
||||
|
||||
/**
|
||||
* 登录页(迁移自 login.html):
|
||||
* captcha.required('login') 判断 → 需要则显示"点击进行人机验证"按钮 → showCaptcha('login') 拿 proof;
|
||||
* 成功后 setToken + 通知登录态变更 + 跳转来源页或首页。
|
||||
*/
|
||||
export default function Login() {
|
||||
return <Placeholder name="登录" />;
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPw, setShowPw] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [capRequired, setCapRequired] = useState(false);
|
||||
const [capDone, setCapDone] = useState(false);
|
||||
const [capProof, setCapProof] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (getToken()) { navigate('/', { replace: true }); return; }
|
||||
captchaRequired('login')
|
||||
.then((r) => { if (r && r.required) setCapRequired(true); })
|
||||
.catch(() => {});
|
||||
}, [navigate]);
|
||||
|
||||
const doCaptcha = async () => {
|
||||
const proof = await showCaptcha('login');
|
||||
if (proof) { setCapProof(proof); setCapDone(true); }
|
||||
};
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!username.trim() || !password) { setError('请输入用户名和密码'); return; }
|
||||
if (capRequired && !capDone) { setError('请先点击验证按钮完成验证'); return; }
|
||||
setError('');
|
||||
setBusy(true);
|
||||
try {
|
||||
const data = await authApi.login(username.trim(), password, capProof || undefined);
|
||||
setToken(data.token);
|
||||
notifyAuthChange();
|
||||
const from = location.state && location.state.from;
|
||||
navigate(from || '/');
|
||||
} catch (e) {
|
||||
setError(e.message || '登录失败');
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login-page" style={{ minHeight: '70vh' }}>
|
||||
<div className="card login-card">
|
||||
<h1>登录</h1>
|
||||
<p className="subtitle">欢迎回到 RainWeb</p>
|
||||
|
||||
<div className="form-group">
|
||||
<label>用户名</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder="输入用户名"
|
||||
autoComplete="username"
|
||||
autoFocus
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') document.getElementById('loginPw').focus(); }}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>密码</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<input
|
||||
id="loginPw"
|
||||
type={showPw ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="输入密码"
|
||||
autoComplete="current-password"
|
||||
style={{ width: '100%', paddingRight: 44 }}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') handleLogin(); }}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex="-1"
|
||||
style={{ position: 'absolute', right: 8, top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', color: 'var(--md-ref-on-surface-variant)', cursor: 'pointer', padding: 4, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
onClick={() => setShowPw((v) => !v)}
|
||||
>
|
||||
<span className="material-icons" style={{ fontSize: 20 }}>{showPw ? 'visibility' : 'visibility_off'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <div style={{ color: 'var(--md-ref-error)', fontSize: 14, marginBottom: 12 }}>{error}</div>}
|
||||
|
||||
{capRequired && (
|
||||
<button
|
||||
className="btn w-full"
|
||||
onClick={doCaptcha}
|
||||
disabled={capDone}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
marginBottom: 8,
|
||||
justifyContent: 'center',
|
||||
height: 44,
|
||||
...(capDone
|
||||
? { background: '#e8f5e9', borderColor: '#4caf50', color: '#2e7d32' }
|
||||
: { background: '#fff', color: '#333', border: '1px solid #333' }),
|
||||
}}
|
||||
>
|
||||
<span className="material-icons" style={{ fontSize: 20 }}>{capDone ? 'check_circle' : 'verified_user'}</span>
|
||||
<span>{capDone ? '验证通过' : '点击进行人机验证'}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button className="btn btn-filled w-full" onClick={handleLogin} disabled={busy}>
|
||||
{busy ? '登录中...' : '登录'}
|
||||
</button>
|
||||
|
||||
<div style={{ textAlign: 'center', marginTop: 16 }}>
|
||||
<span className="text-muted">没有账户?</span>
|
||||
<Link to="/register.html" className="btn-text btn" style={{ fontSize: 14 }}>注册</Link>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', marginTop: 8 }}>
|
||||
<Link to="/" className="btn-text btn" style={{ fontSize: 14 }}>← 返回主页</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user