| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- import { encryptPassword } from '../utils/cryptots';
- import React, { useState, useEffect } from 'react';
- import { useNavigate, Link } from 'react-router-dom';
- import { authService, RegisterRequest } from '../services/authService';
- import { userApi } from '../services/userApi';
- import { Mail, Lock, User, Phone, GraduationCap, AlertCircle, CheckCircle2 } from 'lucide-react';
- import { Loader2 } from '../icons/commonIcons';
- import PasswordStrengthIndicator from '../components/PasswordStrengthIndicator';
- import { BrandingContext } from '../App';
- const Register: React.FC = () => {
- const navigate = useNavigate();
- const branding = React.useContext(BrandingContext);
- const [formData, setFormData] = useState<RegisterRequest>({
- username: '',
- password: '',
- email: '',
- phone: '',
- nickname: '',
- });
- const [smsCode, setSmsCode] = useState('');
- const [smsSending, setSmsSending] = useState(false);
- const [smsCountdown, setSmsCountdown] = useState(0);
- const [confirmPassword, setConfirmPassword] = useState('');
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState<string | null>(null);
- const [success, setSuccess] = useState(false);
- const [autoLoginLoading, setAutoLoginLoading] = useState(false);
- const [passwordMeetsRequirements, setPasswordMeetsRequirements] = useState(false);
- // 验证码倒计时
- useEffect(() => {
- if (smsCountdown <= 0) return;
- const timer = setTimeout(() => setSmsCountdown(c => c - 1), 1000);
- return () => clearTimeout(timer);
- }, [smsCountdown]);
- const handleSendSms = async () => {
- if (!formData.phone || formData.phone.length !== 11) {
- setError('请先填写正确的手机号');
- return;
- }
- setSmsSending(true);
- setError(null);
- try {
- await userApi.sendSmsCode(formData.phone, 'register');
- setSmsCountdown(60);
- } catch (e: any) {
- setError(e.message || '发送失败');
- } finally {
- setSmsSending(false);
- }
- };
- const handleInputChange = (field: keyof RegisterRequest, value: string) => {
- setFormData(prev => ({ ...prev, [field]: value }));
- setError(null);
- };
- const validateForm = (): boolean => {
- if (!formData.username.trim()) { setError('请输入用户名'); return false; }
- if (formData.username.length < 3) { setError('用户名至少需要3个字符'); return false; }
- if (!formData.password) { setError('请输入密码'); return false; }
- if (formData.password.length < 6) { setError('密码至少需要6个字符'); return false; }
- if (!passwordMeetsRequirements) { setError('密码强度太弱,无法注册。请添加大写字母、数字或特殊符号'); return false; }
- if (formData.password !== confirmPassword) { setError('两次输入的密码不一致'); return false; }
- if (!formData.phone) { setError('请输入手机号'); return false; }
- if (!smsCode) { setError('请输入手机验证码'); return false; }
- return true;
- };
- const handleRegister = async (e: React.FormEvent) => {
- e.preventDefault();
- setError(null);
- if (!validateForm()) return;
- setLoading(true);
- try {
- const encryptedPassword = encryptPassword(formData.password);
- const registerData: RegisterRequest = {
- username: formData.username,
- password: encryptedPassword,
- nickname: formData.nickname || formData.username,
- email: formData.email,
- phone: formData.phone,
- sms_code: smsCode || undefined,
- };
- const response = await authService.register(registerData);
- if (response.code === 200) {
- setSuccess(true);
- } else {
- setError(response.message || '注册失败,请稍后重试');
- }
- } catch (err) {
- setError(err instanceof Error ? err.message : '注册失败,请稍后重试');
- } finally {
- setLoading(false);
- }
- };
- // 自动登录
- const handleAutoLogin = async () => {
- setAutoLoginLoading(true);
- try {
- const encryptedPassword = encryptPassword(formData.password);
- const loginResponse = await authService.login(formData.username, encryptedPassword);
- if (loginResponse.code === 200) {
- navigate('/');
- } else {
- navigate('/login');
- }
- } catch (err) {
- navigate('/login');
- } finally {
- setAutoLoginLoading(false);
- }
- };
- if (success) {
- return (
- <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-white to-indigo-50 px-4">
- <div className="w-full max-w-md">
- <div className="bg-white rounded-2xl shadow-2xl border border-gray-100 p-8 text-center">
- <CheckCircle2 className="w-16 h-16 text-green-500 mx-auto mb-6" />
- <h1 className="text-2xl font-bold text-gray-900 mb-2">注册成功!</h1>
- <p className="text-sm text-gray-500 mb-6">您的账号已创建成功</p>
- <button
- onClick={handleAutoLogin}
- disabled={autoLoginLoading}
- className="inline-block px-6 py-2 bg-blue-600 text-white rounded-lg font-bold hover:bg-blue-700 transition-colors disabled:opacity-50"
- >
- {autoLoginLoading ? (
- <span className="flex items-center gap-2">
- <Loader2 className="w-4 h-4 animate-spin" />
- 登录中...
- </span>
- ) : (
- '立即登录'
- )}
- </button>
- </div>
- </div>
- </div>
- );
- }
- return (
- <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-white to-indigo-50 px-4 py-8">
- <div className="w-full max-w-md">
- <div className="bg-white rounded-2xl shadow-2xl border border-gray-100 p-8">
- {/* Logo/标题 */}
- <div className="text-center mb-8">
- <div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-2xl mb-4 overflow-hidden">
- {branding.system_logo
- ? <img src={branding.system_logo} alt="logo" className="w-full h-full object-contain" />
- : <GraduationCap className="w-8 h-8 text-white" />}
- </div>
- <h1 className="text-2xl font-bold text-gray-900 mb-2">注册{branding.system_name}</h1>
- <p className="text-sm text-gray-500">创建您的账号</p>
- </div>
- {/* 错误提示 */}
- {error && (
- <div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg flex items-center gap-2">
- <AlertCircle className="w-4 h-4 text-red-500 flex-shrink-0" />
- <p className="text-sm text-red-600">{error}</p>
- </div>
- )}
- {/* 注册表单 */}
- <form onSubmit={handleRegister} className="space-y-4">
- <div>
- <label className="block text-sm font-bold text-gray-700 mb-2">
- 用户名 <span className="text-red-500">*</span>
- </label>
- <div className="relative">
- <User className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
- <input
- type="text"
- value={formData.username}
- onChange={(e) => handleInputChange('username', e.target.value)}
- placeholder="请输入用户名(至少3个字符)"
- className="w-full pl-10 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
- required
- />
- </div>
- </div>
- <div>
- <label className="block text-sm font-bold text-gray-700 mb-2">
- 邮箱 <span className="text-gray-400 font-normal text-xs">(可选)</span>
- </label>
- <div className="relative">
- <Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
- <input
- type="email"
- value={formData.email}
- onChange={(e) => handleInputChange('email', e.target.value)}
- placeholder="请输入邮箱(可选)"
- className="w-full pl-10 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
- />
- </div>
- </div>
- <div>
- <label className="block text-sm font-bold text-gray-700 mb-2">
- 手机号 <span className="text-red-500">*</span>
- </label>
- <div className="relative">
- <Phone className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
- <input
- type="tel"
- value={formData.phone}
- onChange={(e) => handleInputChange('phone', e.target.value)}
- placeholder="请输入手机号"
- maxLength={11}
- className="w-full pl-10 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
- required
- />
- </div>
- </div>
- {/* 手机号验证码 */}
- <div>
- <label className="block text-sm font-bold text-gray-700 mb-2">
- 手机验证码 <span className="text-red-500">*</span>
- </label>
- <div className="flex gap-2">
- <input
- type="text"
- value={smsCode}
- onChange={(e) => setSmsCode(e.target.value)}
- placeholder="请输入验证码"
- maxLength={6}
- className="flex-1 px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
- required
- />
- <button
- type="button"
- onClick={handleSendSms}
- disabled={smsSending || smsCountdown > 0}
- className="px-4 py-3 bg-blue-600 text-white rounded-xl text-sm font-bold hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
- >
- {smsSending ? '发送中...' : smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码'}
- </button>
- </div>
- </div>
- <div>
- <label className="block text-sm font-bold text-gray-700 mb-2">
- 昵称 <span className="text-gray-400 font-normal text-xs">(可选)</span>
- </label>
- <div className="relative">
- <User className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
- <input
- type="text"
- value={formData.nickname}
- onChange={(e) => handleInputChange('nickname', e.target.value)}
- placeholder="请输入昵称(可选,默认为用户名)"
- className="w-full pl-10 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
- />
- </div>
- </div>
- <div>
- <label className="block text-sm font-bold text-gray-700 mb-2">
- 密码 <span className="text-red-500">*</span>
- </label>
- <div className="relative">
- <Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
- <input
- type="password"
- value={formData.password}
- onChange={(e) => handleInputChange('password', e.target.value)}
- placeholder="请输入密码(至少6个字符)"
- className="w-full pl-10 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
- required
- />
- </div>
- {formData.password && (
- <div className="mt-2">
- <PasswordStrengthIndicator
- password={formData.password}
- onStrengthChange={setPasswordMeetsRequirements}
- />
- </div>
- )}
- </div>
- <div>
- <label className="block text-sm font-bold text-gray-700 mb-2">
- 确认密码 <span className="text-red-500">*</span>
- </label>
- <div className="relative">
- <Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
- <input
- type="password"
- value={confirmPassword}
- onChange={(e) => {
- setConfirmPassword(e.target.value);
- setError(null);
- }}
- placeholder="请再次输入密码"
- className="w-full pl-10 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
- required
- />
- </div>
- </div>
- <div className="flex items-start gap-2 text-sm">
- <input
- type="checkbox"
- className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
- required
- />
- <label className="text-gray-600">
- 我已阅读并同意{' '}
- <a href="#/user-agreement" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:text-blue-700 font-medium">
- 用户协议
- </a>
- {' '}和{' '}
- <a href="#/privacy-policy" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:text-blue-700 font-medium">
- 隐私政策
- </a>
- </label>
- </div>
- <button
- type="submit"
- disabled={loading}
- className="w-full py-3 bg-gradient-to-r from-blue-600 to-indigo-600 text-white rounded-xl font-bold shadow-lg hover:shadow-xl hover:from-blue-700 hover:to-indigo-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
- >
- {loading ? (
- <>
- <Loader2 className="w-5 h-5 animate-spin" />
- <span>注册中...</span>
- </>
- ) : (
- <span>立即注册</span>
- )}
- </button>
- </form>
- {/* 已有账号 */}
- <div className="mt-6 text-center">
- <p className="text-sm text-gray-600">
- 已有账号?{' '}
- <Link to="/login" className="text-blue-600 hover:text-blue-700 font-bold">
- 立即登录
- </Link>
- </p>
- </div>
- </div>
- </div>
- </div>
- );
- };
- export default Register;
|