RegisterForm.jsx 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useContext, useEffect, useRef, useState } from 'react';
  16. import { Link, useNavigate } from 'react-router-dom';
  17. import {
  18. API,
  19. getLogo,
  20. showError,
  21. showInfo,
  22. showSuccess,
  23. updateAPI,
  24. getSystemName,
  25. setUserData,
  26. onDiscordOAuthClicked,
  27. } from '../../helpers';
  28. import Turnstile from 'react-turnstile';
  29. import {
  30. Button,
  31. Card,
  32. Checkbox,
  33. Divider,
  34. Form,
  35. Icon,
  36. Modal,
  37. } from '@douyinfe/semi-ui';
  38. import Title from '@douyinfe/semi-ui/lib/es/typography/title';
  39. import Text from '@douyinfe/semi-ui/lib/es/typography/text';
  40. import {
  41. IconGithubLogo,
  42. IconMail,
  43. IconUser,
  44. IconLock,
  45. IconKey,
  46. } from '@douyinfe/semi-icons';
  47. import {
  48. onGitHubOAuthClicked,
  49. onLinuxDOOAuthClicked,
  50. onOIDCClicked,
  51. } from '../../helpers';
  52. import OIDCIcon from '../common/logo/OIDCIcon';
  53. import LinuxDoIcon from '../common/logo/LinuxDoIcon';
  54. import WeChatIcon from '../common/logo/WeChatIcon';
  55. import TelegramLoginButton from 'react-telegram-login/src';
  56. import { UserContext } from '../../context/User';
  57. import { useTranslation } from 'react-i18next';
  58. import { SiDiscord } from 'react-icons/si';
  59. const RegisterForm = () => {
  60. let navigate = useNavigate();
  61. const { t } = useTranslation();
  62. const githubButtonTextKeyByState = {
  63. idle: '使用 GitHub 继续',
  64. redirecting: '正在跳转 GitHub...',
  65. timeout: '请求超时,请刷新页面后重新发起 GitHub 登录',
  66. };
  67. const [inputs, setInputs] = useState({
  68. username: '',
  69. password: '',
  70. password2: '',
  71. email: '',
  72. verification_code: '',
  73. wechat_verification_code: '',
  74. });
  75. const { username, password, password2 } = inputs;
  76. const [userState, userDispatch] = useContext(UserContext);
  77. const [turnstileEnabled, setTurnstileEnabled] = useState(false);
  78. const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
  79. const [turnstileToken, setTurnstileToken] = useState('');
  80. const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
  81. const [showEmailRegister, setShowEmailRegister] = useState(false);
  82. const [wechatLoading, setWechatLoading] = useState(false);
  83. const [githubLoading, setGithubLoading] = useState(false);
  84. const [discordLoading, setDiscordLoading] = useState(false);
  85. const [oidcLoading, setOidcLoading] = useState(false);
  86. const [linuxdoLoading, setLinuxdoLoading] = useState(false);
  87. const [emailRegisterLoading, setEmailRegisterLoading] = useState(false);
  88. const [registerLoading, setRegisterLoading] = useState(false);
  89. const [verificationCodeLoading, setVerificationCodeLoading] = useState(false);
  90. const [otherRegisterOptionsLoading, setOtherRegisterOptionsLoading] =
  91. useState(false);
  92. const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false);
  93. const [disableButton, setDisableButton] = useState(false);
  94. const [countdown, setCountdown] = useState(30);
  95. const [agreedToTerms, setAgreedToTerms] = useState(false);
  96. const [hasUserAgreement, setHasUserAgreement] = useState(false);
  97. const [hasPrivacyPolicy, setHasPrivacyPolicy] = useState(false);
  98. const [githubButtonState, setGithubButtonState] = useState('idle');
  99. const [githubButtonDisabled, setGithubButtonDisabled] = useState(false);
  100. const githubTimeoutRef = useRef(null);
  101. const githubButtonText = t(githubButtonTextKeyByState[githubButtonState]);
  102. const logo = getLogo();
  103. const systemName = getSystemName();
  104. let affCode = new URLSearchParams(window.location.search).get('aff');
  105. if (affCode) {
  106. localStorage.setItem('aff', affCode);
  107. }
  108. const [status] = useState(() => {
  109. const savedStatus = localStorage.getItem('status');
  110. return savedStatus ? JSON.parse(savedStatus) : {};
  111. });
  112. const [showEmailVerification, setShowEmailVerification] = useState(() => {
  113. return status.email_verification ?? false;
  114. });
  115. useEffect(() => {
  116. setShowEmailVerification(status.email_verification);
  117. if (status.turnstile_check) {
  118. setTurnstileEnabled(true);
  119. setTurnstileSiteKey(status.turnstile_site_key);
  120. }
  121. // 从 status 获取用户协议和隐私政策的启用状态
  122. setHasUserAgreement(status.user_agreement_enabled || false);
  123. setHasPrivacyPolicy(status.privacy_policy_enabled || false);
  124. }, [status]);
  125. useEffect(() => {
  126. let countdownInterval = null;
  127. if (disableButton && countdown > 0) {
  128. countdownInterval = setInterval(() => {
  129. setCountdown(countdown - 1);
  130. }, 1000);
  131. } else if (countdown === 0) {
  132. setDisableButton(false);
  133. setCountdown(30);
  134. }
  135. return () => clearInterval(countdownInterval); // Clean up on unmount
  136. }, [disableButton, countdown]);
  137. useEffect(() => {
  138. return () => {
  139. if (githubTimeoutRef.current) {
  140. clearTimeout(githubTimeoutRef.current);
  141. }
  142. };
  143. }, []);
  144. const onWeChatLoginClicked = () => {
  145. setWechatLoading(true);
  146. setShowWeChatLoginModal(true);
  147. setWechatLoading(false);
  148. };
  149. const onSubmitWeChatVerificationCode = async () => {
  150. if (turnstileEnabled && turnstileToken === '') {
  151. showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
  152. return;
  153. }
  154. setWechatCodeSubmitLoading(true);
  155. try {
  156. const res = await API.get(
  157. `/api/oauth/wechat?code=${inputs.wechat_verification_code}`,
  158. );
  159. const { success, message, data } = res.data;
  160. if (success) {
  161. userDispatch({ type: 'login', payload: data });
  162. localStorage.setItem('user', JSON.stringify(data));
  163. setUserData(data);
  164. updateAPI();
  165. navigate('/');
  166. showSuccess('登录成功!');
  167. setShowWeChatLoginModal(false);
  168. } else {
  169. showError(message);
  170. }
  171. } catch (error) {
  172. showError('登录失败,请重试');
  173. } finally {
  174. setWechatCodeSubmitLoading(false);
  175. }
  176. };
  177. function handleChange(name, value) {
  178. setInputs((inputs) => ({ ...inputs, [name]: value }));
  179. }
  180. async function handleSubmit(e) {
  181. if (password.length < 8) {
  182. showInfo('密码长度不得小于 8 位!');
  183. return;
  184. }
  185. if (password !== password2) {
  186. showInfo('两次输入的密码不一致');
  187. return;
  188. }
  189. if (username && password) {
  190. if (turnstileEnabled && turnstileToken === '') {
  191. showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
  192. return;
  193. }
  194. setRegisterLoading(true);
  195. try {
  196. if (!affCode) {
  197. affCode = localStorage.getItem('aff');
  198. }
  199. inputs.aff_code = affCode;
  200. const res = await API.post(
  201. `/api/user/register?turnstile=${turnstileToken}`,
  202. inputs,
  203. );
  204. const { success, message } = res.data;
  205. if (success) {
  206. navigate('/login');
  207. showSuccess('注册成功!');
  208. } else {
  209. showError(message);
  210. }
  211. } catch (error) {
  212. showError('注册失败,请重试');
  213. } finally {
  214. setRegisterLoading(false);
  215. }
  216. }
  217. }
  218. const sendVerificationCode = async () => {
  219. if (inputs.email === '') return;
  220. if (turnstileEnabled && turnstileToken === '') {
  221. showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
  222. return;
  223. }
  224. setVerificationCodeLoading(true);
  225. try {
  226. const res = await API.get(
  227. `/api/verification?email=${encodeURIComponent(inputs.email)}&turnstile=${turnstileToken}`,
  228. );
  229. const { success, message } = res.data;
  230. if (success) {
  231. showSuccess('验证码发送成功,请检查你的邮箱!');
  232. setDisableButton(true); // 发送成功后禁用按钮,开始倒计时
  233. } else {
  234. showError(message);
  235. }
  236. } catch (error) {
  237. showError('发送验证码失败,请重试');
  238. } finally {
  239. setVerificationCodeLoading(false);
  240. }
  241. };
  242. const handleGitHubClick = () => {
  243. if (githubButtonDisabled) {
  244. return;
  245. }
  246. setGithubLoading(true);
  247. setGithubButtonDisabled(true);
  248. setGithubButtonState('redirecting');
  249. if (githubTimeoutRef.current) {
  250. clearTimeout(githubTimeoutRef.current);
  251. }
  252. githubTimeoutRef.current = setTimeout(() => {
  253. setGithubLoading(false);
  254. setGithubButtonState('timeout');
  255. setGithubButtonDisabled(true);
  256. }, 20000);
  257. try {
  258. onGitHubOAuthClicked(status.github_client_id, { shouldLogout: true });
  259. } finally {
  260. setTimeout(() => setGithubLoading(false), 3000);
  261. }
  262. };
  263. const handleDiscordClick = () => {
  264. setDiscordLoading(true);
  265. try {
  266. onDiscordOAuthClicked(status.discord_client_id, { shouldLogout: true });
  267. } finally {
  268. setTimeout(() => setDiscordLoading(false), 3000);
  269. }
  270. };
  271. const handleOIDCClick = () => {
  272. setOidcLoading(true);
  273. try {
  274. onOIDCClicked(
  275. status.oidc_authorization_endpoint,
  276. status.oidc_client_id,
  277. false,
  278. { shouldLogout: true },
  279. );
  280. } finally {
  281. setTimeout(() => setOidcLoading(false), 3000);
  282. }
  283. };
  284. const handleLinuxDOClick = () => {
  285. setLinuxdoLoading(true);
  286. try {
  287. onLinuxDOOAuthClicked(status.linuxdo_client_id, { shouldLogout: true });
  288. } finally {
  289. setTimeout(() => setLinuxdoLoading(false), 3000);
  290. }
  291. };
  292. const handleEmailRegisterClick = () => {
  293. setEmailRegisterLoading(true);
  294. setShowEmailRegister(true);
  295. setEmailRegisterLoading(false);
  296. };
  297. const handleOtherRegisterOptionsClick = () => {
  298. setOtherRegisterOptionsLoading(true);
  299. setShowEmailRegister(false);
  300. setOtherRegisterOptionsLoading(false);
  301. };
  302. const onTelegramLoginClicked = async (response) => {
  303. const fields = [
  304. 'id',
  305. 'first_name',
  306. 'last_name',
  307. 'username',
  308. 'photo_url',
  309. 'auth_date',
  310. 'hash',
  311. 'lang',
  312. ];
  313. const params = {};
  314. fields.forEach((field) => {
  315. if (response[field]) {
  316. params[field] = response[field];
  317. }
  318. });
  319. try {
  320. const res = await API.get(`/api/oauth/telegram/login`, { params });
  321. const { success, message, data } = res.data;
  322. if (success) {
  323. userDispatch({ type: 'login', payload: data });
  324. localStorage.setItem('user', JSON.stringify(data));
  325. showSuccess('登录成功!');
  326. setUserData(data);
  327. updateAPI();
  328. navigate('/');
  329. } else {
  330. showError(message);
  331. }
  332. } catch (error) {
  333. showError('登录失败,请重试');
  334. }
  335. };
  336. const renderOAuthOptions = () => {
  337. return (
  338. <div className='flex flex-col items-center'>
  339. <div className='w-full max-w-md'>
  340. <div className='flex items-center justify-center mb-6 gap-2'>
  341. <img src={logo} alt='Logo' className='h-10 rounded-full' />
  342. <Title heading={3} className='!text-gray-800'>
  343. {systemName}
  344. </Title>
  345. </div>
  346. <Card className='border-0 !rounded-2xl overflow-hidden'>
  347. <div className='flex justify-center pt-6 pb-2'>
  348. <Title heading={3} className='text-gray-800 dark:text-gray-200'>
  349. {t('注 册')}
  350. </Title>
  351. </div>
  352. <div className='px-2 py-8'>
  353. <div className='space-y-3'>
  354. {status.wechat_login && (
  355. <Button
  356. theme='outline'
  357. className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
  358. type='tertiary'
  359. icon={
  360. <Icon svg={<WeChatIcon />} style={{ color: '#07C160' }} />
  361. }
  362. onClick={onWeChatLoginClicked}
  363. loading={wechatLoading}
  364. >
  365. <span className='ml-3'>{t('使用 微信 继续')}</span>
  366. </Button>
  367. )}
  368. {status.github_oauth && (
  369. <Button
  370. theme='outline'
  371. className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
  372. type='tertiary'
  373. icon={<IconGithubLogo size='large' />}
  374. onClick={handleGitHubClick}
  375. loading={githubLoading}
  376. disabled={githubButtonDisabled}
  377. >
  378. <span className='ml-3'>{githubButtonText}</span>
  379. </Button>
  380. )}
  381. {status.discord_oauth && (
  382. <Button
  383. theme='outline'
  384. className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
  385. type='tertiary'
  386. icon={
  387. <SiDiscord
  388. style={{
  389. color: '#5865F2',
  390. width: '20px',
  391. height: '20px',
  392. }}
  393. />
  394. }
  395. onClick={handleDiscordClick}
  396. loading={discordLoading}
  397. >
  398. <span className='ml-3'>{t('使用 Discord 继续')}</span>
  399. </Button>
  400. )}
  401. {status.oidc_enabled && (
  402. <Button
  403. theme='outline'
  404. className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
  405. type='tertiary'
  406. icon={<OIDCIcon style={{ color: '#1877F2' }} />}
  407. onClick={handleOIDCClick}
  408. loading={oidcLoading}
  409. >
  410. <span className='ml-3'>{t('使用 OIDC 继续')}</span>
  411. </Button>
  412. )}
  413. {status.linuxdo_oauth && (
  414. <Button
  415. theme='outline'
  416. className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
  417. type='tertiary'
  418. icon={
  419. <LinuxDoIcon
  420. style={{
  421. color: '#E95420',
  422. width: '20px',
  423. height: '20px',
  424. }}
  425. />
  426. }
  427. onClick={handleLinuxDOClick}
  428. loading={linuxdoLoading}
  429. >
  430. <span className='ml-3'>{t('使用 LinuxDO 继续')}</span>
  431. </Button>
  432. )}
  433. {status.telegram_oauth && (
  434. <div className='flex justify-center my-2'>
  435. <TelegramLoginButton
  436. dataOnauth={onTelegramLoginClicked}
  437. botName={status.telegram_bot_name}
  438. />
  439. </div>
  440. )}
  441. <Divider margin='12px' align='center'>
  442. {t('或')}
  443. </Divider>
  444. <Button
  445. theme='solid'
  446. type='primary'
  447. className='w-full h-12 flex items-center justify-center bg-black text-white !rounded-full hover:bg-gray-800 transition-colors'
  448. icon={<IconMail size='large' />}
  449. onClick={handleEmailRegisterClick}
  450. loading={emailRegisterLoading}
  451. >
  452. <span className='ml-3'>{t('使用 用户名 注册')}</span>
  453. </Button>
  454. </div>
  455. <div className='mt-6 text-center text-sm'>
  456. <Text>
  457. {t('已有账户?')}{' '}
  458. <Link
  459. to='/login'
  460. className='text-blue-600 hover:text-blue-800 font-medium'
  461. >
  462. {t('登录')}
  463. </Link>
  464. </Text>
  465. </div>
  466. </div>
  467. </Card>
  468. </div>
  469. </div>
  470. );
  471. };
  472. const renderEmailRegisterForm = () => {
  473. return (
  474. <div className='flex flex-col items-center'>
  475. <div className='w-full max-w-md'>
  476. <div className='flex items-center justify-center mb-6 gap-2'>
  477. <img src={logo} alt='Logo' className='h-10 rounded-full' />
  478. <Title heading={3} className='!text-gray-800'>
  479. {systemName}
  480. </Title>
  481. </div>
  482. <Card className='border-0 !rounded-2xl overflow-hidden'>
  483. <div className='flex justify-center pt-6 pb-2'>
  484. <Title heading={3} className='text-gray-800 dark:text-gray-200'>
  485. {t('注 册')}
  486. </Title>
  487. </div>
  488. <div className='px-2 py-8'>
  489. <Form className='space-y-3'>
  490. <Form.Input
  491. field='username'
  492. label={t('用户名')}
  493. placeholder={t('请输入用户名')}
  494. name='username'
  495. onChange={(value) => handleChange('username', value)}
  496. prefix={<IconUser />}
  497. />
  498. <Form.Input
  499. field='password'
  500. label={t('密码')}
  501. placeholder={t('输入密码,最短 8 位,最长 20 位')}
  502. name='password'
  503. mode='password'
  504. onChange={(value) => handleChange('password', value)}
  505. prefix={<IconLock />}
  506. />
  507. <Form.Input
  508. field='password2'
  509. label={t('确认密码')}
  510. placeholder={t('确认密码')}
  511. name='password2'
  512. mode='password'
  513. onChange={(value) => handleChange('password2', value)}
  514. prefix={<IconLock />}
  515. />
  516. {showEmailVerification && (
  517. <>
  518. <Form.Input
  519. field='email'
  520. label={t('邮箱')}
  521. placeholder={t('输入邮箱地址')}
  522. name='email'
  523. type='email'
  524. onChange={(value) => handleChange('email', value)}
  525. prefix={<IconMail />}
  526. suffix={
  527. <Button
  528. onClick={sendVerificationCode}
  529. loading={verificationCodeLoading}
  530. disabled={disableButton || verificationCodeLoading}
  531. >
  532. {disableButton
  533. ? `${t('重新发送')} (${countdown})`
  534. : t('获取验证码')}
  535. </Button>
  536. }
  537. />
  538. <Form.Input
  539. field='verification_code'
  540. label={t('验证码')}
  541. placeholder={t('输入验证码')}
  542. name='verification_code'
  543. onChange={(value) =>
  544. handleChange('verification_code', value)
  545. }
  546. prefix={<IconKey />}
  547. />
  548. </>
  549. )}
  550. {(hasUserAgreement || hasPrivacyPolicy) && (
  551. <div className='pt-4'>
  552. <Checkbox
  553. checked={agreedToTerms}
  554. onChange={(e) => setAgreedToTerms(e.target.checked)}
  555. >
  556. <Text size='small' className='text-gray-600'>
  557. {t('我已阅读并同意')}
  558. {hasUserAgreement && (
  559. <>
  560. <a
  561. href='/user-agreement'
  562. target='_blank'
  563. rel='noopener noreferrer'
  564. className='text-blue-600 hover:text-blue-800 mx-1'
  565. >
  566. {t('用户协议')}
  567. </a>
  568. </>
  569. )}
  570. {hasUserAgreement && hasPrivacyPolicy && t('和')}
  571. {hasPrivacyPolicy && (
  572. <>
  573. <a
  574. href='/privacy-policy'
  575. target='_blank'
  576. rel='noopener noreferrer'
  577. className='text-blue-600 hover:text-blue-800 mx-1'
  578. >
  579. {t('隐私政策')}
  580. </a>
  581. </>
  582. )}
  583. </Text>
  584. </Checkbox>
  585. </div>
  586. )}
  587. <div className='space-y-2 pt-2'>
  588. <Button
  589. theme='solid'
  590. className='w-full !rounded-full'
  591. type='primary'
  592. htmlType='submit'
  593. onClick={handleSubmit}
  594. loading={registerLoading}
  595. disabled={
  596. (hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms
  597. }
  598. >
  599. {t('注册')}
  600. </Button>
  601. </div>
  602. </Form>
  603. {(status.github_oauth ||
  604. status.discord_oauth ||
  605. status.oidc_enabled ||
  606. status.wechat_login ||
  607. status.linuxdo_oauth ||
  608. status.telegram_oauth) && (
  609. <>
  610. <Divider margin='12px' align='center'>
  611. {t('或')}
  612. </Divider>
  613. <div className='mt-4 text-center'>
  614. <Button
  615. theme='outline'
  616. type='tertiary'
  617. className='w-full !rounded-full'
  618. onClick={handleOtherRegisterOptionsClick}
  619. loading={otherRegisterOptionsLoading}
  620. >
  621. {t('其他注册选项')}
  622. </Button>
  623. </div>
  624. </>
  625. )}
  626. <div className='mt-6 text-center text-sm'>
  627. <Text>
  628. {t('已有账户?')}{' '}
  629. <Link
  630. to='/login'
  631. className='text-blue-600 hover:text-blue-800 font-medium'
  632. >
  633. {t('登录')}
  634. </Link>
  635. </Text>
  636. </div>
  637. </div>
  638. </Card>
  639. </div>
  640. </div>
  641. );
  642. };
  643. const renderWeChatLoginModal = () => {
  644. return (
  645. <Modal
  646. title={t('微信扫码登录')}
  647. visible={showWeChatLoginModal}
  648. maskClosable={true}
  649. onOk={onSubmitWeChatVerificationCode}
  650. onCancel={() => setShowWeChatLoginModal(false)}
  651. okText={t('登录')}
  652. centered={true}
  653. okButtonProps={{
  654. loading: wechatCodeSubmitLoading,
  655. }}
  656. >
  657. <div className='flex flex-col items-center'>
  658. <img src={status.wechat_qrcode} alt='微信二维码' className='mb-4' />
  659. </div>
  660. <div className='text-center mb-4'>
  661. <p>
  662. {t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}
  663. </p>
  664. </div>
  665. <Form>
  666. <Form.Input
  667. field='wechat_verification_code'
  668. placeholder={t('验证码')}
  669. label={t('验证码')}
  670. value={inputs.wechat_verification_code}
  671. onChange={(value) =>
  672. handleChange('wechat_verification_code', value)
  673. }
  674. />
  675. </Form>
  676. </Modal>
  677. );
  678. };
  679. return (
  680. <div className='relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'>
  681. {/* 背景模糊晕染球 */}
  682. <div
  683. className='blur-ball blur-ball-indigo'
  684. style={{ top: '-80px', right: '-80px', transform: 'none' }}
  685. />
  686. <div
  687. className='blur-ball blur-ball-teal'
  688. style={{ top: '50%', left: '-120px' }}
  689. />
  690. <div className='w-full max-w-sm mt-[60px]'>
  691. {showEmailRegister ||
  692. !(
  693. status.github_oauth ||
  694. status.discord_oauth ||
  695. status.oidc_enabled ||
  696. status.wechat_login ||
  697. status.linuxdo_oauth ||
  698. status.telegram_oauth
  699. )
  700. ? renderEmailRegisterForm()
  701. : renderOAuthOptions()}
  702. {renderWeChatLoginModal()}
  703. {turnstileEnabled && (
  704. <div className='flex justify-center mt-6'>
  705. <Turnstile
  706. sitekey={turnstileSiteKey}
  707. onVerify={(token) => {
  708. setTurnstileToken(token);
  709. }}
  710. />
  711. </div>
  712. )}
  713. </div>
  714. </div>
  715. );
  716. };
  717. export default RegisterForm;