Ver código fonte

chore: optimize frontend (#293)

* main

* chore: update style

---------

Co-authored-by: JustSong <[email protected]>
Yolo° 2 anos atrás
pai
commit
9b4d1964d4

+ 3 - 2
controller/misc.go

@@ -127,8 +127,9 @@ func SendPasswordResetEmail(c *gin.Context) {
 	link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", common.ServerAddress, email, code)
 	subject := fmt.Sprintf("%s密码重置", common.SystemName)
 	content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+
-		"<p>点击<a href='%s'>此处</a>进行密码重置。</p>"+
-		"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, common.VerificationValidMinutes)
+		"<p>点击 <a href='%s'>此处</a> 进行密码重置。</p>"+
+		"<p>如果链接无法点击,请尝试点击下面的链接或将其复制到浏览器中打开:<br> %s </p>"+
+		"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, link, common.VerificationValidMinutes)
 	err := common.SendEmail(subject, email, content)
 	if err != nil {
 		c.JSON(http.StatusOK, gin.H{

+ 29 - 40
web/src/components/LoginForm.js

@@ -1,36 +1,25 @@
 import React, { useContext, useEffect, useState } from 'react';
-import {
-  Button,
-  Divider,
-  Form,
-  Grid,
-  Header,
-  Image,
-  Message,
-  Modal,
-  Segment,
-} from 'semantic-ui-react';
+import { Button, Divider, Form, Grid, Header, Image, Message, Modal, Segment } from 'semantic-ui-react';
 import { Link, useNavigate, useSearchParams } from 'react-router-dom';
 import { UserContext } from '../context/User';
-import { API, getLogo, showError, showSuccess, showInfo } from '../helpers';
+import { API, getLogo, showError, showSuccess } from '../helpers';
 
 const LoginForm = () => {
   const [inputs, setInputs] = useState({
     username: '',
     password: '',
-    wechat_verification_code: '',
+    wechat_verification_code: ''
   });
   const [searchParams, setSearchParams] = useSearchParams();
   const [submitted, setSubmitted] = useState(false);
   const { username, password } = inputs;
   const [userState, userDispatch] = useContext(UserContext);
   let navigate = useNavigate();
-
   const [status, setStatus] = useState({});
   const logo = getLogo();
 
   useEffect(() => {
-    if (searchParams.get("expired")) {
+    if (searchParams.get('expired')) {
       showError('未登录或登录已过期,请重新登录!');
     }
     let status = localStorage.getItem('status');
@@ -78,7 +67,7 @@ const LoginForm = () => {
     if (username && password) {
       const res = await API.post(`/api/user/login`, {
         username,
-        password,
+        password
       });
       const { success, message, data } = res.data;
       if (success) {
@@ -93,44 +82,44 @@ const LoginForm = () => {
   }
 
   return (
-    <Grid textAlign="center" style={{ marginTop: '48px' }}>
+    <Grid textAlign='center' style={{ marginTop: '48px' }}>
       <Grid.Column style={{ maxWidth: 450 }}>
-        <Header as="h2" color="" textAlign="center">
+        <Header as='h2' color='' textAlign='center'>
           <Image src={logo} /> 用户登录
         </Header>
-        <Form size="large">
+        <Form size='large'>
           <Segment>
             <Form.Input
               fluid
-              icon="user"
-              iconPosition="left"
-              placeholder="用户名"
-              name="username"
+              icon='user'
+              iconPosition='left'
+              placeholder='用户名'
+              name='username'
               value={username}
               onChange={handleChange}
             />
             <Form.Input
               fluid
-              icon="lock"
-              iconPosition="left"
-              placeholder="密码"
-              name="password"
-              type="password"
+              icon='lock'
+              iconPosition='left'
+              placeholder='密码'
+              name='password'
+              type='password'
               value={password}
               onChange={handleChange}
             />
-            <Button color="" fluid size="large" onClick={handleSubmit}>
+            <Button color='green' fluid size='large' onClick={handleSubmit}>
               登录
             </Button>
           </Segment>
         </Form>
         <Message>
           忘记密码?
-          <Link to="/reset" className="btn btn-link">
+          <Link to='/reset' className='btn btn-link'>
             点击重置
           </Link>
           ; 没有账户?
-          <Link to="/register" className="btn btn-link">
+          <Link to='/register' className='btn btn-link'>
             点击注册
           </Link>
         </Message>
@@ -140,8 +129,8 @@ const LoginForm = () => {
             {status.github_oauth ? (
               <Button
                 circular
-                color="black"
-                icon="github"
+                color='black'
+                icon='github'
                 onClick={onGitHubOAuthClicked}
               />
             ) : (
@@ -150,8 +139,8 @@ const LoginForm = () => {
             {status.wechat_login ? (
               <Button
                 circular
-                color="green"
-                icon="wechat"
+                color='green'
+                icon='wechat'
                 onClick={onWeChatLoginClicked}
               />
             ) : (
@@ -175,18 +164,18 @@ const LoginForm = () => {
                   微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
                 </p>
               </div>
-              <Form size="large">
+              <Form size='large'>
                 <Form.Input
                   fluid
-                  placeholder="验证码"
-                  name="wechat_verification_code"
+                  placeholder='验证码'
+                  name='wechat_verification_code'
                   value={inputs.wechat_verification_code}
                   onChange={handleChange}
                 />
                 <Button
-                  color=""
+                  color=''
                   fluid
-                  size="large"
+                  size='large'
                   onClick={onSubmitWeChatVerificationCode}
                 >
                   登录

+ 42 - 5
web/src/components/PasswordResetConfirm.js

@@ -12,6 +12,11 @@ const PasswordResetConfirm = () => {
 
   const [loading, setLoading] = useState(false);
 
+  const [disableButton, setDisableButton] = useState(false);
+  const [countdown, setCountdown] = useState(30);
+
+  const [newPassword, setNewPassword] = useState('');
+
   const [searchParams, setSearchParams] = useSearchParams();
   useEffect(() => {
     let token = searchParams.get('token');
@@ -22,7 +27,21 @@ const PasswordResetConfirm = () => {
     });
   }, []);
 
+  useEffect(() => {
+    let countdownInterval = null;
+    if (disableButton && countdown > 0) {
+      countdownInterval = setInterval(() => {
+        setCountdown(countdown - 1);
+      }, 1000);
+    } else if (countdown === 0) {
+      setDisableButton(false);
+      setCountdown(30);
+    }
+    return () => clearInterval(countdownInterval); 
+  }, [disableButton, countdown]);
+
   async function handleSubmit(e) {
+    setDisableButton(true);
     if (!email) return;
     setLoading(true);
     const res = await API.post(`/api/user/reset`, {
@@ -32,14 +51,15 @@ const PasswordResetConfirm = () => {
     const { success, message } = res.data;
     if (success) {
       let password = res.data.data;
+      setNewPassword(password);
       await copy(password);
-      showNotice(`密码已重置并已复制到剪贴板:${password}`);
+      showNotice(`密码已复制到剪贴板:${password}`);
     } else {
       showError(message);
     }
     setLoading(false);
   }
-
+  
   return (
     <Grid textAlign='center' style={{ marginTop: '48px' }}>
       <Grid.Column style={{ maxWidth: 450 }}>
@@ -57,20 +77,37 @@ const PasswordResetConfirm = () => {
               value={email}
               readOnly
             />
+            {newPassword && (
+              <Form.Input
+              fluid
+              icon='lock'
+              iconPosition='left'
+              placeholder='新密码'
+              name='newPassword'
+              value={newPassword}
+              readOnly
+              onClick={(e) => {
+                e.target.select();
+                navigator.clipboard.writeText(newPassword);
+                showNotice(`密码已复制到剪贴板:${newPassword}`);
+              }}
+            />            
+            )}
             <Button
-              color=''
+              color='green'
               fluid
               size='large'
               onClick={handleSubmit}
               loading={loading}
+              disabled={disableButton}
             >
-              提交
+              {disableButton ? `密码重置完成` : '提交'}
             </Button>
           </Segment>
         </Form>
       </Grid.Column>
     </Grid>
-  );
+  );  
 };
 
 export default PasswordResetConfirm;

+ 18 - 12
web/src/components/PasswordResetForm.js

@@ -5,7 +5,7 @@ import Turnstile from 'react-turnstile';
 
 const PasswordResetForm = () => {
   const [inputs, setInputs] = useState({
-    email: '',
+    email: ''
   });
   const { email } = inputs;
 
@@ -13,24 +13,29 @@ const PasswordResetForm = () => {
   const [turnstileEnabled, setTurnstileEnabled] = useState(false);
   const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
   const [turnstileToken, setTurnstileToken] = useState('');
+  const [disableButton, setDisableButton] = useState(false);
+  const [countdown, setCountdown] = useState(30);
 
   useEffect(() => {
-    let status = localStorage.getItem('status');
-    if (status) {
-      status = JSON.parse(status);
-      if (status.turnstile_check) {
-        setTurnstileEnabled(true);
-        setTurnstileSiteKey(status.turnstile_site_key);
-      }
+    let countdownInterval = null;
+    if (disableButton && countdown > 0) {
+      countdownInterval = setInterval(() => {
+        setCountdown(countdown - 1);
+      }, 1000);
+    } else if (countdown === 0) {
+      setDisableButton(false);
+      setCountdown(30);
     }
-  }, []);
+    return () => clearInterval(countdownInterval);
+  }, [disableButton, countdown]);
 
   function handleChange(e) {
     const { name, value } = e.target;
-    setInputs((inputs) => ({ ...inputs, [name]: value }));
+    setInputs(inputs => ({ ...inputs, [name]: value }));
   }
 
   async function handleSubmit(e) {
+    setDisableButton(true);
     if (!email) return;
     if (turnstileEnabled && turnstileToken === '') {
       showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
@@ -78,13 +83,14 @@ const PasswordResetForm = () => {
               <></>
             )}
             <Button
-              color=''
+              color='green'
               fluid
               size='large'
               onClick={handleSubmit}
               loading={loading}
+              disabled={disableButton}
             >
-              提交
+              {disableButton ? `重试 (${countdown})` : '提交'}
             </Button>
           </Segment>
         </Form>

+ 18 - 2
web/src/components/PersonalSetting.js

@@ -17,6 +17,8 @@ const PersonalSetting = () => {
   const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
   const [turnstileToken, setTurnstileToken] = useState('');
   const [loading, setLoading] = useState(false);
+  const [disableButton, setDisableButton] = useState(false);
+  const [countdown, setCountdown] = useState(30);
 
   useEffect(() => {
     let status = localStorage.getItem('status');
@@ -30,6 +32,19 @@ const PersonalSetting = () => {
     }
   }, []);
 
+  useEffect(() => {
+    let countdownInterval = null;
+    if (disableButton && countdown > 0) {
+      countdownInterval = setInterval(() => {
+        setCountdown(countdown - 1);
+      }, 1000);
+    } else if (countdown === 0) {
+      setDisableButton(false);
+      setCountdown(30);
+    }
+    return () => clearInterval(countdownInterval); // Clean up on unmount
+  }, [disableButton, countdown]);
+
   const handleInputChange = (e, { name, value }) => {
     setInputs((inputs) => ({ ...inputs, [name]: value }));
   };
@@ -78,6 +93,7 @@ const PersonalSetting = () => {
   };
 
   const sendVerificationCode = async () => {
+    setDisableButton(true);
     if (inputs.email === '') return;
     if (turnstileEnabled && turnstileToken === '') {
       showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
@@ -195,8 +211,8 @@ const PersonalSetting = () => {
                 name='email'
                 type='email'
                 action={
-                  <Button onClick={sendVerificationCode} disabled={loading}>
-                    获取验证码
+                  <Button onClick={sendVerificationCode} disabled={disableButton || loading}>
+                    {disableButton ? `重新发送(${countdown})` : '获取验证码'}
                   </Button>
                 }
               />

+ 3 - 11
web/src/components/RegisterForm.js

@@ -1,13 +1,5 @@
 import React, { useEffect, useState } from 'react';
-import {
-  Button,
-  Form,
-  Grid,
-  Header,
-  Image,
-  Message,
-  Segment,
-} from 'semantic-ui-react';
+import { Button, Form, Grid, Header, Image, Message, Segment } from 'semantic-ui-react';
 import { Link, useNavigate } from 'react-router-dom';
 import { API, getLogo, showError, showInfo, showSuccess } from '../helpers';
 import Turnstile from 'react-turnstile';
@@ -18,7 +10,7 @@ const RegisterForm = () => {
     password: '',
     password2: '',
     email: '',
-    verification_code: '',
+    verification_code: ''
   });
   const { username, password, password2 } = inputs;
   const [showEmailVerification, setShowEmailVerification] = useState(false);
@@ -178,7 +170,7 @@ const RegisterForm = () => {
               <></>
             )}
             <Button
-              color=''
+              color='green'
               fluid
               size='large'
               onClick={handleSubmit}

+ 1 - 3
web/src/pages/About/index.js

@@ -46,9 +46,7 @@ const About = () => {
             about.startsWith('https://') ? <iframe
               src={about}
               style={{ width: '100%', height: '100vh', border: 'none' }}
-            /> : <Segment>
-              <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: about }}></div>
-            </Segment>
+            /> : <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: about }}></div>
           }
         </>
       }

+ 22 - 15
web/src/pages/TopUp/index.js

@@ -7,24 +7,32 @@ const TopUp = () => {
   const [redemptionCode, setRedemptionCode] = useState('');
   const [topUpLink, setTopUpLink] = useState('');
   const [userQuota, setUserQuota] = useState(0);
+  const [isSubmitting, setIsSubmitting] = useState(false);
 
   const topUp = async () => {
     if (redemptionCode === '') {
       showInfo('请输入充值码!')
       return;
     }
-    const res = await API.post('/api/user/topup', {
-      key: redemptionCode
-    });
-    const { success, message, data } = res.data;
-    if (success) {
-      showSuccess('充值成功!');
-      setUserQuota((quota) => {
-        return quota + data;
+    setIsSubmitting(true);
+    try {
+      const res = await API.post('/api/user/topup', {
+        key: redemptionCode
       });
-      setRedemptionCode('');
-    } else {
-      showError(message);
+      const { success, message, data } = res.data;
+      if (success) {
+        showSuccess('充值成功!');
+        setUserQuota((quota) => {
+          return quota + data;
+        });
+        setRedemptionCode('');
+      } else {
+        showError(message);
+      }
+    } catch (err) {
+      showError('请求失败');
+    } finally {
+      setIsSubmitting(false); 
     }
   };
 
@@ -74,8 +82,8 @@ const TopUp = () => {
             <Button color='green' onClick={openTopUpLink}>
               获取兑换码
             </Button>
-            <Button color='yellow' onClick={topUp}>
-              充值
+            <Button color='yellow' onClick={topUp} disabled={isSubmitting}>
+                {isSubmitting ? '兑换中...' : '兑换'}
             </Button>
           </Form>
         </Grid.Column>
@@ -92,5 +100,4 @@ const TopUp = () => {
   );
 };
 
-
-export default TopUp;
+export default TopUp;