PasswordResetConfirm.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  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 } from '@douyinfe/semi-ui';
  5. import { IconMail, IconLock } from '@douyinfe/semi-icons';
  6. import { useTranslation } from 'react-i18next';
  7. import Background from '/example.png';
  8. const { Text, Title } = Typography;
  9. const PasswordResetConfirm = () => {
  10. const { t } = useTranslation();
  11. const [inputs, setInputs] = useState({
  12. email: '',
  13. token: '',
  14. });
  15. const { email, token } = inputs;
  16. const isValidResetLink = email && token;
  17. const [loading, setLoading] = useState(false);
  18. const [disableButton, setDisableButton] = useState(false);
  19. const [countdown, setCountdown] = useState(30);
  20. const [newPassword, setNewPassword] = useState('');
  21. const [searchParams, setSearchParams] = useSearchParams();
  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. }, [searchParams]);
  32. useEffect(() => {
  33. let countdownInterval = null;
  34. if (disableButton && countdown > 0) {
  35. countdownInterval = setInterval(() => {
  36. setCountdown(countdown - 1);
  37. }, 1000);
  38. } else if (countdown === 0) {
  39. setDisableButton(false);
  40. setCountdown(30);
  41. }
  42. return () => clearInterval(countdownInterval);
  43. }, [disableButton, countdown]);
  44. async function handleSubmit(e) {
  45. if (!email || !token) {
  46. showError(t('无效的重置链接,请重新发起密码重置请求'));
  47. return;
  48. }
  49. setDisableButton(true);
  50. setLoading(true);
  51. const res = await API.post(`/api/user/reset`, {
  52. email,
  53. token,
  54. });
  55. const { success, message } = res.data;
  56. if (success) {
  57. let password = res.data.data;
  58. setNewPassword(password);
  59. await copy(password);
  60. showNotice(`${t('密码已重置并已复制到剪贴板')}: ${password}`);
  61. } else {
  62. showError(message);
  63. }
  64. setLoading(false);
  65. }
  66. return (
  67. <div className="min-h-screen relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden">
  68. {/* 背景图片容器 - 放大并保持居中 */}
  69. <div
  70. className="absolute inset-0 z-0 bg-cover bg-center scale-125 opacity-100"
  71. style={{
  72. backgroundImage: `url(${Background})`
  73. }}
  74. ></div>
  75. {/* 半透明遮罩层 */}
  76. <div className="absolute inset-0 bg-gradient-to-br from-teal-500/30 via-blue-500/30 to-purple-500/30 backdrop-blur-sm z-0"></div>
  77. <div className="w-full max-w-sm relative z-10">
  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-white'>{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. <div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
  91. <Text>{t('无效的重置链接,请重新发起密码重置请求')}</Text>
  92. </div>
  93. )}
  94. <Form className="space-y-3">
  95. <Form.Input
  96. field="email"
  97. label={t('邮箱')}
  98. name="email"
  99. size="large"
  100. className="!rounded-md"
  101. value={email || ''}
  102. disabled={true}
  103. prefix={<IconMail />}
  104. placeholder={email ? '' : t('等待获取邮箱信息...')}
  105. />
  106. {newPassword && (
  107. <Form.Input
  108. field="newPassword"
  109. label={t('新密码')}
  110. name="newPassword"
  111. size="large"
  112. className="!rounded-md"
  113. value={newPassword}
  114. disabled={true}
  115. prefix={<IconLock />}
  116. onClick={(e) => {
  117. e.target.select();
  118. navigator.clipboard.writeText(newPassword);
  119. showNotice(`${t('密码已复制到剪贴板')}: ${newPassword}`);
  120. }}
  121. />
  122. )}
  123. <div className="space-y-2 pt-2">
  124. <Button
  125. theme="solid"
  126. className="w-full !rounded-full"
  127. type="primary"
  128. htmlType="submit"
  129. size="large"
  130. onClick={handleSubmit}
  131. loading={loading}
  132. disabled={disableButton || newPassword || !isValidResetLink}
  133. >
  134. {newPassword ? t('密码重置完成') : t('确认重置密码')}
  135. </Button>
  136. </div>
  137. </Form>
  138. <div className="mt-6 text-center text-sm">
  139. <Text><Link to="/login" className="text-blue-600 hover:text-blue-800 font-medium">{t('返回登录')}</Link></Text>
  140. </div>
  141. </div>
  142. </Card>
  143. </div>
  144. </div>
  145. </div>
  146. </div>
  147. );
  148. };
  149. export default PasswordResetConfirm;