RegisterForm.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. import React, { useContext, useEffect, useState } from 'react';
  2. import { Link, useNavigate } from 'react-router-dom';
  3. import {
  4. API,
  5. getLogo,
  6. showError,
  7. showInfo,
  8. showSuccess,
  9. updateAPI,
  10. getSystemName,
  11. setUserData
  12. } from '../../helpers/index.js';
  13. import Turnstile from 'react-turnstile';
  14. import {
  15. Button,
  16. Card,
  17. Divider,
  18. Form,
  19. Icon,
  20. Modal,
  21. } from '@douyinfe/semi-ui';
  22. import Title from '@douyinfe/semi-ui/lib/es/typography/title';
  23. import Text from '@douyinfe/semi-ui/lib/es/typography/text';
  24. import { IconGithubLogo, IconMail, IconUser, IconLock, IconKey } from '@douyinfe/semi-icons';
  25. import {
  26. onGitHubOAuthClicked,
  27. onLinuxDOOAuthClicked,
  28. onOIDCClicked,
  29. } from '../../helpers/index.js';
  30. import OIDCIcon from '../common/logo/OIDCIcon.js';
  31. import LinuxDoIcon from '../common/logo/LinuxDoIcon.js';
  32. import WeChatIcon from '../common/logo/WeChatIcon.js';
  33. import TelegramLoginButton from 'react-telegram-login/src';
  34. import { UserContext } from '../../context/User/index.js';
  35. import { useTranslation } from 'react-i18next';
  36. const RegisterForm = () => {
  37. let navigate = useNavigate();
  38. const { t } = useTranslation();
  39. const [inputs, setInputs] = useState({
  40. username: '',
  41. password: '',
  42. password2: '',
  43. email: '',
  44. verification_code: '',
  45. wechat_verification_code: '',
  46. });
  47. const { username, password, password2 } = inputs;
  48. const [userState, userDispatch] = useContext(UserContext);
  49. const [turnstileEnabled, setTurnstileEnabled] = useState(false);
  50. const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
  51. const [turnstileToken, setTurnstileToken] = useState('');
  52. const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
  53. const [showEmailRegister, setShowEmailRegister] = useState(false);
  54. const [wechatLoading, setWechatLoading] = useState(false);
  55. const [githubLoading, setGithubLoading] = useState(false);
  56. const [oidcLoading, setOidcLoading] = useState(false);
  57. const [linuxdoLoading, setLinuxdoLoading] = useState(false);
  58. const [emailRegisterLoading, setEmailRegisterLoading] = useState(false);
  59. const [registerLoading, setRegisterLoading] = useState(false);
  60. const [verificationCodeLoading, setVerificationCodeLoading] = useState(false);
  61. const [otherRegisterOptionsLoading, setOtherRegisterOptionsLoading] = useState(false);
  62. const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false);
  63. const logo = getLogo();
  64. const systemName = getSystemName();
  65. let affCode = new URLSearchParams(window.location.search).get('aff');
  66. if (affCode) {
  67. localStorage.setItem('aff', affCode);
  68. }
  69. const [status] = useState(() => {
  70. const savedStatus = localStorage.getItem('status');
  71. return savedStatus ? JSON.parse(savedStatus) : {};
  72. });
  73. const [showEmailVerification, setShowEmailVerification] = useState(() => {
  74. return status.email_verification ?? false;
  75. });
  76. useEffect(() => {
  77. setShowEmailVerification(status.email_verification);
  78. if (status.turnstile_check) {
  79. setTurnstileEnabled(true);
  80. setTurnstileSiteKey(status.turnstile_site_key);
  81. }
  82. }, [status]);
  83. const onWeChatLoginClicked = () => {
  84. setWechatLoading(true);
  85. setShowWeChatLoginModal(true);
  86. setWechatLoading(false);
  87. };
  88. const onSubmitWeChatVerificationCode = async () => {
  89. if (turnstileEnabled && turnstileToken === '') {
  90. showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
  91. return;
  92. }
  93. setWechatCodeSubmitLoading(true);
  94. try {
  95. const res = await API.get(
  96. `/api/oauth/wechat?code=${inputs.wechat_verification_code}`,
  97. );
  98. const { success, message, data } = res.data;
  99. if (success) {
  100. userDispatch({ type: 'login', payload: data });
  101. localStorage.setItem('user', JSON.stringify(data));
  102. setUserData(data);
  103. updateAPI();
  104. navigate('/');
  105. showSuccess('登录成功!');
  106. setShowWeChatLoginModal(false);
  107. } else {
  108. showError(message);
  109. }
  110. } catch (error) {
  111. showError('登录失败,请重试');
  112. } finally {
  113. setWechatCodeSubmitLoading(false);
  114. }
  115. };
  116. function handleChange(name, value) {
  117. setInputs((inputs) => ({ ...inputs, [name]: value }));
  118. }
  119. async function handleSubmit(e) {
  120. if (password.length < 8) {
  121. showInfo('密码长度不得小于 8 位!');
  122. return;
  123. }
  124. if (password !== password2) {
  125. showInfo('两次输入的密码不一致');
  126. return;
  127. }
  128. if (username && password) {
  129. if (turnstileEnabled && turnstileToken === '') {
  130. showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
  131. return;
  132. }
  133. setRegisterLoading(true);
  134. try {
  135. if (!affCode) {
  136. affCode = localStorage.getItem('aff');
  137. }
  138. inputs.aff_code = affCode;
  139. const res = await API.post(
  140. `/api/user/register?turnstile=${turnstileToken}`,
  141. inputs,
  142. );
  143. const { success, message } = res.data;
  144. if (success) {
  145. navigate('/login');
  146. showSuccess('注册成功!');
  147. } else {
  148. showError(message);
  149. }
  150. } catch (error) {
  151. showError('注册失败,请重试');
  152. } finally {
  153. setRegisterLoading(false);
  154. }
  155. }
  156. }
  157. const sendVerificationCode = async () => {
  158. if (inputs.email === '') return;
  159. if (turnstileEnabled && turnstileToken === '') {
  160. showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
  161. return;
  162. }
  163. setVerificationCodeLoading(true);
  164. try {
  165. const res = await API.get(
  166. `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`,
  167. );
  168. const { success, message } = res.data;
  169. if (success) {
  170. showSuccess('验证码发送成功,请检查你的邮箱!');
  171. } else {
  172. showError(message);
  173. }
  174. } catch (error) {
  175. showError('发送验证码失败,请重试');
  176. } finally {
  177. setVerificationCodeLoading(false);
  178. }
  179. };
  180. const handleGitHubClick = () => {
  181. setGithubLoading(true);
  182. try {
  183. onGitHubOAuthClicked(status.github_client_id);
  184. } finally {
  185. setTimeout(() => setGithubLoading(false), 3000);
  186. }
  187. };
  188. const handleOIDCClick = () => {
  189. setOidcLoading(true);
  190. try {
  191. onOIDCClicked(
  192. status.oidc_authorization_endpoint,
  193. status.oidc_client_id
  194. );
  195. } finally {
  196. setTimeout(() => setOidcLoading(false), 3000);
  197. }
  198. };
  199. const handleLinuxDOClick = () => {
  200. setLinuxdoLoading(true);
  201. try {
  202. onLinuxDOOAuthClicked(status.linuxdo_client_id);
  203. } finally {
  204. setTimeout(() => setLinuxdoLoading(false), 3000);
  205. }
  206. };
  207. const handleEmailRegisterClick = () => {
  208. setEmailRegisterLoading(true);
  209. setShowEmailRegister(true);
  210. setEmailRegisterLoading(false);
  211. };
  212. const handleOtherRegisterOptionsClick = () => {
  213. setOtherRegisterOptionsLoading(true);
  214. setShowEmailRegister(false);
  215. setOtherRegisterOptionsLoading(false);
  216. };
  217. const onTelegramLoginClicked = async (response) => {
  218. const fields = [
  219. 'id',
  220. 'first_name',
  221. 'last_name',
  222. 'username',
  223. 'photo_url',
  224. 'auth_date',
  225. 'hash',
  226. 'lang',
  227. ];
  228. const params = {};
  229. fields.forEach((field) => {
  230. if (response[field]) {
  231. params[field] = response[field];
  232. }
  233. });
  234. try {
  235. const res = await API.get(`/api/oauth/telegram/login`, { params });
  236. const { success, message, data } = res.data;
  237. if (success) {
  238. userDispatch({ type: 'login', payload: data });
  239. localStorage.setItem('user', JSON.stringify(data));
  240. showSuccess('登录成功!');
  241. setUserData(data);
  242. updateAPI();
  243. navigate('/');
  244. } else {
  245. showError(message);
  246. }
  247. } catch (error) {
  248. showError('登录失败,请重试');
  249. }
  250. };
  251. const renderOAuthOptions = () => {
  252. return (
  253. <div className="flex flex-col items-center">
  254. <div className="w-full max-w-md">
  255. <div className="flex items-center justify-center mb-6 gap-2">
  256. <img src={logo} alt="Logo" className="h-10 rounded-full" />
  257. <Title heading={3} className='!text-gray-800'>{systemName}</Title>
  258. </div>
  259. <Card className="shadow-xl border-0 !rounded-2xl overflow-hidden">
  260. <div className="flex justify-center pt-6 pb-2">
  261. <Title heading={3} className="text-gray-800 dark:text-gray-200">{t('注 册')}</Title>
  262. </div>
  263. <div className="px-2 py-8">
  264. <div className="space-y-3">
  265. {status.wechat_login && (
  266. <Button
  267. theme='outline'
  268. className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
  269. type="tertiary"
  270. icon={<Icon svg={<WeChatIcon />} style={{ color: '#07C160' }} />}
  271. size="large"
  272. onClick={onWeChatLoginClicked}
  273. loading={wechatLoading}
  274. >
  275. <span className="ml-3">{t('使用 微信 继续')}</span>
  276. </Button>
  277. )}
  278. {status.github_oauth && (
  279. <Button
  280. theme='outline'
  281. className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
  282. type="tertiary"
  283. icon={<IconGithubLogo size="large" />}
  284. size="large"
  285. onClick={handleGitHubClick}
  286. loading={githubLoading}
  287. >
  288. <span className="ml-3">{t('使用 GitHub 继续')}</span>
  289. </Button>
  290. )}
  291. {status.oidc_enabled && (
  292. <Button
  293. theme='outline'
  294. className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
  295. type="tertiary"
  296. icon={<OIDCIcon style={{ color: '#1877F2' }} />}
  297. size="large"
  298. onClick={handleOIDCClick}
  299. loading={oidcLoading}
  300. >
  301. <span className="ml-3">{t('使用 OIDC 继续')}</span>
  302. </Button>
  303. )}
  304. {status.linuxdo_oauth && (
  305. <Button
  306. theme='outline'
  307. className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
  308. type="tertiary"
  309. icon={<LinuxDoIcon style={{ color: '#E95420', width: '20px', height: '20px' }} />}
  310. size="large"
  311. onClick={handleLinuxDOClick}
  312. loading={linuxdoLoading}
  313. >
  314. <span className="ml-3">{t('使用 LinuxDO 继续')}</span>
  315. </Button>
  316. )}
  317. {status.telegram_oauth && (
  318. <div className="flex justify-center my-2">
  319. <TelegramLoginButton
  320. dataOnauth={onTelegramLoginClicked}
  321. botName={status.telegram_bot_name}
  322. />
  323. </div>
  324. )}
  325. <Divider margin='12px' align='center'>
  326. {t('或')}
  327. </Divider>
  328. <Button
  329. theme="solid"
  330. type="primary"
  331. className="w-full h-12 flex items-center justify-center bg-black text-white !rounded-full hover:bg-gray-800 transition-colors"
  332. icon={<IconMail size="large" />}
  333. size="large"
  334. onClick={handleEmailRegisterClick}
  335. loading={emailRegisterLoading}
  336. >
  337. <span className="ml-3">{t('使用 用户名 注册')}</span>
  338. </Button>
  339. </div>
  340. <div className="mt-6 text-center text-sm">
  341. <Text>{t('已有账户?')} <Link to="/login" className="text-blue-600 hover:text-blue-800 font-medium">{t('登录')}</Link></Text>
  342. </div>
  343. </div>
  344. </Card>
  345. </div>
  346. </div>
  347. );
  348. };
  349. const renderEmailRegisterForm = () => {
  350. return (
  351. <div className="flex flex-col items-center">
  352. <div className="w-full max-w-md">
  353. <div className="flex items-center justify-center mb-6 gap-2">
  354. <img src={logo} alt="Logo" className="h-10 rounded-full" />
  355. <Title heading={3} className='!text-gray-800'>{systemName}</Title>
  356. </div>
  357. <Card className="shadow-xl border-0 !rounded-2xl overflow-hidden">
  358. <div className="flex justify-center pt-6 pb-2">
  359. <Title heading={3} className="text-gray-800 dark:text-gray-200">{t('注 册')}</Title>
  360. </div>
  361. <div className="px-2 py-8">
  362. <Form className="space-y-3">
  363. <Form.Input
  364. field="username"
  365. label={t('用户名')}
  366. placeholder={t('请输入用户名')}
  367. name="username"
  368. size="large"
  369. onChange={(value) => handleChange('username', value)}
  370. prefix={<IconUser />}
  371. />
  372. <Form.Input
  373. field="password"
  374. label={t('密码')}
  375. placeholder={t('输入密码,最短 8 位,最长 20 位')}
  376. name="password"
  377. mode="password"
  378. size="large"
  379. onChange={(value) => handleChange('password', value)}
  380. prefix={<IconLock />}
  381. />
  382. <Form.Input
  383. field="password2"
  384. label={t('确认密码')}
  385. placeholder={t('确认密码')}
  386. name="password2"
  387. mode="password"
  388. size="large"
  389. onChange={(value) => handleChange('password2', value)}
  390. prefix={<IconLock />}
  391. />
  392. {showEmailVerification && (
  393. <>
  394. <Form.Input
  395. field="email"
  396. label={t('邮箱')}
  397. placeholder={t('输入邮箱地址')}
  398. name="email"
  399. type="email"
  400. size="large"
  401. onChange={(value) => handleChange('email', value)}
  402. prefix={<IconMail />}
  403. suffix={
  404. <Button
  405. onClick={sendVerificationCode}
  406. loading={verificationCodeLoading}
  407. size="small"
  408. >
  409. {t('获取验证码')}
  410. </Button>
  411. }
  412. />
  413. <Form.Input
  414. field="verification_code"
  415. label={t('验证码')}
  416. placeholder={t('输入验证码')}
  417. name="verification_code"
  418. size="large"
  419. onChange={(value) => handleChange('verification_code', value)}
  420. prefix={<IconKey />}
  421. />
  422. </>
  423. )}
  424. <div className="space-y-2 pt-2">
  425. <Button
  426. theme="solid"
  427. className="w-full !rounded-full"
  428. type="primary"
  429. htmlType="submit"
  430. size="large"
  431. onClick={handleSubmit}
  432. loading={registerLoading}
  433. >
  434. {t('注册')}
  435. </Button>
  436. </div>
  437. </Form>
  438. {(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth) && (
  439. <>
  440. <Divider margin='12px' align='center'>
  441. {t('或')}
  442. </Divider>
  443. <div className="mt-4 text-center">
  444. <Button
  445. theme="outline"
  446. type="tertiary"
  447. className="w-full !rounded-full"
  448. size="large"
  449. onClick={handleOtherRegisterOptionsClick}
  450. loading={otherRegisterOptionsLoading}
  451. >
  452. {t('其他注册选项')}
  453. </Button>
  454. </div>
  455. </>
  456. )}
  457. <div className="mt-6 text-center text-sm">
  458. <Text>{t('已有账户?')} <Link to="/login" className="text-blue-600 hover:text-blue-800 font-medium">{t('登录')}</Link></Text>
  459. </div>
  460. </div>
  461. </Card>
  462. </div>
  463. </div>
  464. );
  465. };
  466. const renderWeChatLoginModal = () => {
  467. return (
  468. <Modal
  469. title={t('微信扫码登录')}
  470. visible={showWeChatLoginModal}
  471. maskClosable={true}
  472. onOk={onSubmitWeChatVerificationCode}
  473. onCancel={() => setShowWeChatLoginModal(false)}
  474. okText={t('登录')}
  475. size="small"
  476. centered={true}
  477. okButtonProps={{
  478. loading: wechatCodeSubmitLoading,
  479. }}
  480. >
  481. <div className="flex flex-col items-center">
  482. <img src={status.wechat_qrcode} alt="微信二维码" className="mb-4" />
  483. </div>
  484. <div className="text-center mb-4">
  485. <p>{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}</p>
  486. </div>
  487. <Form size="large">
  488. <Form.Input
  489. field="wechat_verification_code"
  490. placeholder={t('验证码')}
  491. label={t('验证码')}
  492. value={inputs.wechat_verification_code}
  493. onChange={(value) => handleChange('wechat_verification_code', value)}
  494. />
  495. </Form>
  496. </Modal>
  497. );
  498. };
  499. return (
  500. <div className="relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
  501. {/* 背景模糊晕染球 */}
  502. <div className="blur-ball blur-ball-indigo" style={{ top: '-80px', right: '-80px', transform: 'none' }} />
  503. <div className="blur-ball blur-ball-teal" style={{ top: '50%', left: '-120px' }} />
  504. <div className="w-full max-w-sm mt-[60px]">
  505. {showEmailRegister || !(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth)
  506. ? renderEmailRegisterForm()
  507. : renderOAuthOptions()}
  508. {renderWeChatLoginModal()}
  509. {turnstileEnabled && (
  510. <div className="flex justify-center mt-6">
  511. <Turnstile
  512. sitekey={turnstileSiteKey}
  513. onVerify={(token) => {
  514. setTurnstileToken(token);
  515. }}
  516. />
  517. </div>
  518. )}
  519. </div>
  520. </div>
  521. );
  522. };
  523. export default RegisterForm;