P4: 登录/注册/个人中心迁移(验证码 proof/邮箱验证/头像/改密)

This commit is contained in:
2026-08-06 22:48:48 +08:00
parent be6f644154
commit f4a5276fb8
7 changed files with 564 additions and 16 deletions
+129 -3
View File
@@ -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>
);
}