Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 

806 Zeilen
27 KiB

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