You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

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