179 lines
7.5 KiB
React
179 lines
7.5 KiB
React
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>
|
||
);
|
||
}
|