PasswordResetConfirm.js 6.1 KB

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