Register.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. import { encryptPassword } from '../utils/cryptots';
  2. import React, { useState, useEffect } from 'react';
  3. import { useNavigate, Link } from 'react-router-dom';
  4. import { authService, RegisterRequest } from '../services/authService';
  5. import { userApi } from '../services/userApi';
  6. import { Mail, Lock, User, Phone, GraduationCap, AlertCircle, CheckCircle2 } from 'lucide-react';
  7. import { Loader2 } from '../icons/commonIcons';
  8. import PasswordStrengthIndicator from '../components/PasswordStrengthIndicator';
  9. import { BrandingContext } from '../App';
  10. const Register: React.FC = () => {
  11. const navigate = useNavigate();
  12. const branding = React.useContext(BrandingContext);
  13. const [formData, setFormData] = useState<RegisterRequest>({
  14. username: '',
  15. password: '',
  16. email: '',
  17. phone: '',
  18. nickname: '',
  19. });
  20. const [smsCode, setSmsCode] = useState('');
  21. const [smsSending, setSmsSending] = useState(false);
  22. const [smsCountdown, setSmsCountdown] = useState(0);
  23. const [confirmPassword, setConfirmPassword] = useState('');
  24. const [loading, setLoading] = useState(false);
  25. const [error, setError] = useState<string | null>(null);
  26. const [success, setSuccess] = useState(false);
  27. const [autoLoginLoading, setAutoLoginLoading] = useState(false);
  28. const [passwordMeetsRequirements, setPasswordMeetsRequirements] = useState(false);
  29. // 验证码倒计时
  30. useEffect(() => {
  31. if (smsCountdown <= 0) return;
  32. const timer = setTimeout(() => setSmsCountdown(c => c - 1), 1000);
  33. return () => clearTimeout(timer);
  34. }, [smsCountdown]);
  35. const handleSendSms = async () => {
  36. if (!formData.phone || formData.phone.length !== 11) {
  37. setError('请先填写正确的手机号');
  38. return;
  39. }
  40. setSmsSending(true);
  41. setError(null);
  42. try {
  43. await userApi.sendSmsCode(formData.phone, 'register');
  44. setSmsCountdown(60);
  45. } catch (e: any) {
  46. setError(e.message || '发送失败');
  47. } finally {
  48. setSmsSending(false);
  49. }
  50. };
  51. const handleInputChange = (field: keyof RegisterRequest, value: string) => {
  52. setFormData(prev => ({ ...prev, [field]: value }));
  53. setError(null);
  54. };
  55. const validateForm = (): boolean => {
  56. if (!formData.username.trim()) { setError('请输入用户名'); return false; }
  57. if (formData.username.length < 3) { setError('用户名至少需要3个字符'); return false; }
  58. if (!formData.password) { setError('请输入密码'); return false; }
  59. if (formData.password.length < 6) { setError('密码至少需要6个字符'); return false; }
  60. if (!passwordMeetsRequirements) { setError('密码强度太弱,无法注册。请添加大写字母、数字或特殊符号'); return false; }
  61. if (formData.password !== confirmPassword) { setError('两次输入的密码不一致'); return false; }
  62. if (!formData.phone) { setError('请输入手机号'); return false; }
  63. if (!smsCode) { setError('请输入手机验证码'); return false; }
  64. return true;
  65. };
  66. const handleRegister = async (e: React.FormEvent) => {
  67. e.preventDefault();
  68. setError(null);
  69. if (!validateForm()) return;
  70. setLoading(true);
  71. try {
  72. const encryptedPassword = encryptPassword(formData.password);
  73. const registerData: RegisterRequest = {
  74. username: formData.username,
  75. password: encryptedPassword,
  76. nickname: formData.nickname || formData.username,
  77. email: formData.email,
  78. phone: formData.phone,
  79. sms_code: smsCode || undefined,
  80. };
  81. const response = await authService.register(registerData);
  82. if (response.code === 200) {
  83. setSuccess(true);
  84. } else {
  85. setError(response.message || '注册失败,请稍后重试');
  86. }
  87. } catch (err) {
  88. setError(err instanceof Error ? err.message : '注册失败,请稍后重试');
  89. } finally {
  90. setLoading(false);
  91. }
  92. };
  93. // 自动登录
  94. const handleAutoLogin = async () => {
  95. setAutoLoginLoading(true);
  96. try {
  97. const encryptedPassword = encryptPassword(formData.password);
  98. const loginResponse = await authService.login(formData.username, encryptedPassword);
  99. if (loginResponse.code === 200) {
  100. navigate('/');
  101. } else {
  102. navigate('/login');
  103. }
  104. } catch (err) {
  105. navigate('/login');
  106. } finally {
  107. setAutoLoginLoading(false);
  108. }
  109. };
  110. if (success) {
  111. return (
  112. <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-white to-indigo-50 px-4">
  113. <div className="w-full max-w-md">
  114. <div className="bg-white rounded-2xl shadow-2xl border border-gray-100 p-8 text-center">
  115. <CheckCircle2 className="w-16 h-16 text-green-500 mx-auto mb-6" />
  116. <h1 className="text-2xl font-bold text-gray-900 mb-2">注册成功!</h1>
  117. <p className="text-sm text-gray-500 mb-6">您的账号已创建成功</p>
  118. <button
  119. onClick={handleAutoLogin}
  120. disabled={autoLoginLoading}
  121. 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"
  122. >
  123. {autoLoginLoading ? (
  124. <span className="flex items-center gap-2">
  125. <Loader2 className="w-4 h-4 animate-spin" />
  126. 登录中...
  127. </span>
  128. ) : (
  129. '立即登录'
  130. )}
  131. </button>
  132. </div>
  133. </div>
  134. </div>
  135. );
  136. }
  137. return (
  138. <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">
  139. <div className="w-full max-w-md">
  140. <div className="bg-white rounded-2xl shadow-2xl border border-gray-100 p-8">
  141. {/* Logo/标题 */}
  142. <div className="text-center mb-8">
  143. <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">
  144. {branding.system_logo
  145. ? <img src={branding.system_logo} alt="logo" className="w-full h-full object-contain" />
  146. : <GraduationCap className="w-8 h-8 text-white" />}
  147. </div>
  148. <h1 className="text-2xl font-bold text-gray-900 mb-2">注册{branding.system_name}</h1>
  149. <p className="text-sm text-gray-500">创建您的账号</p>
  150. </div>
  151. {/* 错误提示 */}
  152. {error && (
  153. <div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg flex items-center gap-2">
  154. <AlertCircle className="w-4 h-4 text-red-500 flex-shrink-0" />
  155. <p className="text-sm text-red-600">{error}</p>
  156. </div>
  157. )}
  158. {/* 注册表单 */}
  159. <form onSubmit={handleRegister} className="space-y-4">
  160. <div>
  161. <label className="block text-sm font-bold text-gray-700 mb-2">
  162. 用户名 <span className="text-red-500">*</span>
  163. </label>
  164. <div className="relative">
  165. <User className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
  166. <input
  167. type="text"
  168. value={formData.username}
  169. onChange={(e) => handleInputChange('username', e.target.value)}
  170. placeholder="请输入用户名(至少3个字符)"
  171. 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"
  172. required
  173. />
  174. </div>
  175. </div>
  176. <div>
  177. <label className="block text-sm font-bold text-gray-700 mb-2">
  178. 邮箱 <span className="text-gray-400 font-normal text-xs">(可选)</span>
  179. </label>
  180. <div className="relative">
  181. <Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
  182. <input
  183. type="email"
  184. value={formData.email}
  185. onChange={(e) => handleInputChange('email', e.target.value)}
  186. placeholder="请输入邮箱(可选)"
  187. 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"
  188. />
  189. </div>
  190. </div>
  191. <div>
  192. <label className="block text-sm font-bold text-gray-700 mb-2">
  193. 手机号 <span className="text-red-500">*</span>
  194. </label>
  195. <div className="relative">
  196. <Phone className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
  197. <input
  198. type="tel"
  199. value={formData.phone}
  200. onChange={(e) => handleInputChange('phone', e.target.value)}
  201. placeholder="请输入手机号"
  202. maxLength={11}
  203. 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"
  204. required
  205. />
  206. </div>
  207. </div>
  208. {/* 手机号验证码 */}
  209. <div>
  210. <label className="block text-sm font-bold text-gray-700 mb-2">
  211. 手机验证码 <span className="text-red-500">*</span>
  212. </label>
  213. <div className="flex gap-2">
  214. <input
  215. type="text"
  216. value={smsCode}
  217. onChange={(e) => setSmsCode(e.target.value)}
  218. placeholder="请输入验证码"
  219. maxLength={6}
  220. 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"
  221. required
  222. />
  223. <button
  224. type="button"
  225. onClick={handleSendSms}
  226. disabled={smsSending || smsCountdown > 0}
  227. 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"
  228. >
  229. {smsSending ? '发送中...' : smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码'}
  230. </button>
  231. </div>
  232. </div>
  233. <div>
  234. <label className="block text-sm font-bold text-gray-700 mb-2">
  235. 昵称 <span className="text-gray-400 font-normal text-xs">(可选)</span>
  236. </label>
  237. <div className="relative">
  238. <User className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
  239. <input
  240. type="text"
  241. value={formData.nickname}
  242. onChange={(e) => handleInputChange('nickname', e.target.value)}
  243. placeholder="请输入昵称(可选,默认为用户名)"
  244. 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"
  245. />
  246. </div>
  247. </div>
  248. <div>
  249. <label className="block text-sm font-bold text-gray-700 mb-2">
  250. 密码 <span className="text-red-500">*</span>
  251. </label>
  252. <div className="relative">
  253. <Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
  254. <input
  255. type="password"
  256. value={formData.password}
  257. onChange={(e) => handleInputChange('password', e.target.value)}
  258. placeholder="请输入密码(至少6个字符)"
  259. 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"
  260. required
  261. />
  262. </div>
  263. {formData.password && (
  264. <div className="mt-2">
  265. <PasswordStrengthIndicator
  266. password={formData.password}
  267. onStrengthChange={setPasswordMeetsRequirements}
  268. />
  269. </div>
  270. )}
  271. </div>
  272. <div>
  273. <label className="block text-sm font-bold text-gray-700 mb-2">
  274. 确认密码 <span className="text-red-500">*</span>
  275. </label>
  276. <div className="relative">
  277. <Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
  278. <input
  279. type="password"
  280. value={confirmPassword}
  281. onChange={(e) => {
  282. setConfirmPassword(e.target.value);
  283. setError(null);
  284. }}
  285. placeholder="请再次输入密码"
  286. 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"
  287. required
  288. />
  289. </div>
  290. </div>
  291. <div className="flex items-start gap-2 text-sm">
  292. <input
  293. type="checkbox"
  294. className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
  295. required
  296. />
  297. <label className="text-gray-600">
  298. 我已阅读并同意{' '}
  299. <a href="#/user-agreement" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:text-blue-700 font-medium">
  300. 用户协议
  301. </a>
  302. {' '}和{' '}
  303. <a href="#/privacy-policy" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:text-blue-700 font-medium">
  304. 隐私政策
  305. </a>
  306. </label>
  307. </div>
  308. <button
  309. type="submit"
  310. disabled={loading}
  311. 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"
  312. >
  313. {loading ? (
  314. <>
  315. <Loader2 className="w-5 h-5 animate-spin" />
  316. <span>注册中...</span>
  317. </>
  318. ) : (
  319. <span>立即注册</span>
  320. )}
  321. </button>
  322. </form>
  323. {/* 已有账号 */}
  324. <div className="mt-6 text-center">
  325. <p className="text-sm text-gray-600">
  326. 已有账号?{' '}
  327. <Link to="/login" className="text-blue-600 hover:text-blue-700 font-bold">
  328. 立即登录
  329. </Link>
  330. </p>
  331. </div>
  332. </div>
  333. </div>
  334. </div>
  335. );
  336. };
  337. export default Register;