PasswordResetConfirm.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  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, { useEffect, useState } from 'react';
  16. import { API, copy, showError, showNotice, getLogo, getSystemName } from '../../helpers';
  17. import { useSearchParams, Link } from 'react-router-dom';
  18. import { Button, Card, Form, Typography, Banner } from '@douyinfe/semi-ui';
  19. import { IconMail, IconLock, IconCopy } from '@douyinfe/semi-icons';
  20. import { useTranslation } from 'react-i18next';
  21. const { Text, Title } = Typography;
  22. const PasswordResetConfirm = () => {
  23. const { t } = useTranslation();
  24. const [inputs, setInputs] = useState({
  25. email: '',
  26. token: '',
  27. });
  28. const { email, token } = inputs;
  29. const isValidResetLink = email && token;
  30. const [loading, setLoading] = useState(false);
  31. const [disableButton, setDisableButton] = useState(false);
  32. const [countdown, setCountdown] = useState(30);
  33. const [newPassword, setNewPassword] = useState('');
  34. const [searchParams, setSearchParams] = useSearchParams();
  35. const [formApi, setFormApi] = useState(null);
  36. const logo = getLogo();
  37. const systemName = getSystemName();
  38. useEffect(() => {
  39. let token = searchParams.get('token');
  40. let email = searchParams.get('email');
  41. setInputs({
  42. token: token || '',
  43. email: email || '',
  44. });
  45. if (formApi) {
  46. formApi.setValues({
  47. email: email || '',
  48. newPassword: newPassword || ''
  49. });
  50. }
  51. }, [searchParams, newPassword, formApi]);
  52. useEffect(() => {
  53. let countdownInterval = null;
  54. if (disableButton && countdown > 0) {
  55. countdownInterval = setInterval(() => {
  56. setCountdown(countdown - 1);
  57. }, 1000);
  58. } else if (countdown === 0) {
  59. setDisableButton(false);
  60. setCountdown(30);
  61. }
  62. return () => clearInterval(countdownInterval);
  63. }, [disableButton, countdown]);
  64. async function handleSubmit(e) {
  65. if (!email || !token) {
  66. showError(t('无效的重置链接,请重新发起密码重置请求'));
  67. return;
  68. }
  69. setDisableButton(true);
  70. setLoading(true);
  71. const res = await API.post(`/api/user/reset`, {
  72. email,
  73. token,
  74. });
  75. const { success, message } = res.data;
  76. if (success) {
  77. let password = res.data.data;
  78. setNewPassword(password);
  79. await copy(password);
  80. showNotice(`${t('密码已重置并已复制到剪贴板:')} ${password}`);
  81. } else {
  82. showError(message);
  83. }
  84. setLoading(false);
  85. }
  86. return (
  87. <div className="relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
  88. {/* 背景模糊晕染球 */}
  89. <div className="blur-ball blur-ball-indigo" style={{ top: '-80px', right: '-80px', transform: 'none' }} />
  90. <div className="blur-ball blur-ball-teal" style={{ top: '50%', left: '-120px' }} />
  91. <div className="w-full max-w-sm mt-[60px]">
  92. <div className="flex flex-col items-center">
  93. <div className="w-full max-w-md">
  94. <div className="flex items-center justify-center mb-6 gap-2">
  95. <img src={logo} alt="Logo" className="h-10 rounded-full" />
  96. <Title heading={3} className='!text-gray-800'>{systemName}</Title>
  97. </div>
  98. <Card className="shadow-xl border-0 !rounded-2xl overflow-hidden">
  99. <div className="flex justify-center pt-6 pb-2">
  100. <Title heading={3} className="text-gray-800 dark:text-gray-200">{t('密码重置确认')}</Title>
  101. </div>
  102. <div className="px-2 py-8">
  103. {!isValidResetLink && (
  104. <Banner
  105. type="danger"
  106. description={t('无效的重置链接,请重新发起密码重置请求')}
  107. className="mb-4 !rounded-lg"
  108. closeIcon={null}
  109. />
  110. )}
  111. <Form
  112. getFormApi={(api) => setFormApi(api)}
  113. initValues={{ email: email || '', newPassword: newPassword || '' }}
  114. className="space-y-4"
  115. >
  116. <Form.Input
  117. field="email"
  118. label={t('邮箱')}
  119. name="email"
  120. size="large"
  121. disabled={true}
  122. prefix={<IconMail />}
  123. placeholder={email ? '' : t('等待获取邮箱信息...')}
  124. />
  125. {newPassword && (
  126. <Form.Input
  127. field="newPassword"
  128. label={t('新密码')}
  129. name="newPassword"
  130. size="large"
  131. disabled={true}
  132. prefix={<IconLock />}
  133. suffix={
  134. <Button
  135. icon={<IconCopy />}
  136. type="tertiary"
  137. theme="borderless"
  138. onClick={async () => {
  139. await copy(newPassword);
  140. showNotice(`${t('密码已复制到剪贴板:')} ${newPassword}`);
  141. }}
  142. >
  143. {t('复制')}
  144. </Button>
  145. }
  146. />
  147. )}
  148. <div className="space-y-2 pt-2">
  149. <Button
  150. theme="solid"
  151. className="w-full !rounded-full"
  152. type="primary"
  153. htmlType="submit"
  154. size="large"
  155. onClick={handleSubmit}
  156. loading={loading}
  157. disabled={disableButton || newPassword || !isValidResetLink}
  158. >
  159. {newPassword ? t('密码重置完成') : t('确认重置密码')}
  160. </Button>
  161. </div>
  162. </Form>
  163. <div className="mt-6 text-center text-sm">
  164. <Text><Link to="/login" className="text-blue-600 hover:text-blue-800 font-medium">{t('返回登录')}</Link></Text>
  165. </div>
  166. </div>
  167. </Card>
  168. </div>
  169. </div>
  170. </div>
  171. </div>
  172. );
  173. };
  174. export default PasswordResetConfirm;