Files
rainblogweb/frontend/src/pages/Register.jsx
T

179 lines
7.5 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import * as authApi from '../api/auth.js';
import * as emailApi from '../api/email.js';
import { required as captchaRequired } from '../api/captcha.js';
import { showCaptcha } from '../components/CaptchaModal.jsx';
import { getToken } from '../api/client.js';
import { showSnackbar } from '../lib/utils.js';
/**
* 注册页(迁移自 register.html):
* 注册 → 后端返回 requires_verification 时进入邮箱验证步骤(8 位验证码 → /api/email/complete-register),
* 支持重新发送验证码(/api/email/send-verify);验证码 proof 流转与登录一致。
*/
export default function Register() {
const navigate = useNavigate();
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirm, setConfirm] = useState('');
const [error, setError] = useState('');
const [busy, setBusy] = useState(false);
const [verifyMode, setVerifyMode] = useState(false);
const [code, setCode] = useState('');
const [capRequired, setCapRequired] = useState(false);
const [capDone, setCapDone] = useState(false);
const [capResult, setCapResult] = useState(null);
useEffect(() => {
if (getToken()) { navigate('/', { replace: true }); return; }
captchaRequired('register')
.then((r) => { if (r && r.required) setCapRequired(true); })
.catch(() => {});
}, [navigate]);
const doCaptcha = async () => {
const result = await showCaptcha('register');
if (result) { setCapResult(result); setCapDone(true); }
};
const handleRegister = async () => {
if (!username.trim() || !email.trim() || !password) { setError('请填写所有必填项'); return; }
if (password.length < 6) { setError('密码至少6位'); return; }
if (password !== confirm) { setError('两次密码不一致'); return; }
if (capRequired && !capDone) { setError('请先点击验证按钮完成验证'); return; }
setError('');
setBusy(true);
try {
const data = await authApi.register({
username: username.trim(),
password,
email: email.trim(),
captcha: capResult || undefined,
});
if (data.requires_verification) {
setVerifyMode(true); // 进入邮箱验证步骤
} else {
showSnackbar('注册成功,请登录');
setTimeout(() => navigate('/login.html'), 1000);
}
} catch (e) {
setError(e.message || '注册失败');
}
setBusy(false);
};
const handleVerify = async () => {
if (!code || code.length !== 8) { setError('请输入完整的 8 位验证码'); return; }
setError('');
try {
await emailApi.completeRegister(code.trim(), username.trim(), password);
showSnackbar('注册成功,请登录');
setTimeout(() => navigate('/login.html'), 1000);
} catch (e) {
setError(e.message);
}
};
const resendCode = async () => {
try {
await emailApi.sendVerify(email.trim(), username.trim());
showSnackbar('验证码已重新发送');
} catch (e) {
showSnackbar(e.message);
}
};
return (
<div className="register-page" style={{ minHeight: '70vh' }}>
<div className="card register-card" style={{ width: '100%', maxWidth: 420, padding: '40px 32px' }}>
<h1 style={{ fontSize: 28, fontWeight: 500, textAlign: 'center', marginBottom: 8 }}>
{verifyMode ? '邮箱验证' : '创建账户'}
</h1>
<p className="subtitle" style={{ textAlign: 'center', color: 'var(--md-ref-on-surface-variant)', marginBottom: 28, fontSize: 14 }}>
{verifyMode ? '请输入邮箱中的验证码' : '加入 RainWeb'}
</p>
{!verifyMode && (
<>
<div className="form-group">
<label>用户名 *</label>
<input type="text" value={username} onChange={(e) => setUsername(e.target.value)} placeholder="用户名" autoComplete="username" />
</div>
<div className="form-group">
<label>邮箱 *</label>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="your@email.com" autoComplete="email" />
</div>
<div className="form-group">
<label>密码 *</label>
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="至少6位" autoComplete="new-password" />
</div>
<div className="form-group">
<label>确认密码 *</label>
<input type="password" value={confirm} onChange={(e) => setConfirm(e.target.value)} placeholder="再次输入密码" autoComplete="new-password" />
</div>
</>
)}
{verifyMode && (
<div style={{ textAlign: 'center', marginBottom: 16 }}>
<div className="form-group">
<label style={{ fontSize: 16, fontWeight: 600, marginBottom: 8 }}>邮箱验证码</label>
<p className="text-muted" style={{ fontSize: 14, marginBottom: 12 }}>验证码已发送到您的邮箱请输入 8 位数字验证码</p>
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="输入 8 位验证码"
maxLength={8}
inputMode="numeric"
style={{ textAlign: 'center', fontSize: 24, letterSpacing: 8 }}
onKeyDown={(e) => { if (e.key === 'Enter') handleVerify(); }}
/>
</div>
<button className="btn btn-filled w-full" onClick={handleVerify} disabled={busy}>验证并完成注册</button>
<button className="btn btn-text btn-sm mt-16" onClick={resendCode} style={{ marginTop: 8 }}>重新发送验证码</button>
</div>
)}
{error && <div style={{ color: 'var(--md-ref-error)', fontSize: 14, marginBottom: 12 }}>{error}</div>}
{!verifyMode && (
<>
{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={handleRegister} disabled={busy}>
{busy ? '注册中...' : '注册'}
</button>
<div style={{ textAlign: 'center', marginTop: 16 }}>
<span className="text-muted">已有账户</span>
<Link to="/login.html" className="btn-text btn" style={{ fontSize: 14 }}>登录</Link>
</div>
</>
)}
<div style={{ textAlign: 'center', marginTop: verifyMode ? 16 : 8 }}>
<Link to="/" className="btn-text btn" style={{ fontSize: 14 }}> 返回主页</Link>
</div>
</div>
</div>
);
}