فهرست منبع

✨feat(ui): Add loading states to all authentication buttons

Add loading indicators to improve user experience during authentication processes:
- Implement loading states for all login and registration buttons
- Add try/catch/finally structure to properly handle async operations
- Create wrapper functions for OAuth redirect operations
- Set loading state for verification code submission
- Update modal confirmation buttons with loading state
- Add proper error handling with user feedback
- Split generic loading state into specific state variables

This change enhances user experience by providing clear visual feedback
during authentication actions.
Apple\Apple 11 ماه پیش
والد
کامیت
80765bddcd
2فایلهای تغییر یافته به همراه293 افزوده شده و 125 حذف شده
  1. 152 60
      web/src/components/LoginForm.js
  2. 141 65
      web/src/components/RegisterForm.js

+ 152 - 60
web/src/components/LoginForm.js

@@ -53,6 +53,15 @@ const LoginForm = () => {
   const [status, setStatus] = useState({});
   const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
   const [showEmailLogin, setShowEmailLogin] = useState(false);
+  const [wechatLoading, setWechatLoading] = useState(false);
+  const [githubLoading, setGithubLoading] = useState(false);
+  const [oidcLoading, setOidcLoading] = useState(false);
+  const [linuxdoLoading, setLinuxdoLoading] = useState(false);
+  const [emailLoginLoading, setEmailLoginLoading] = useState(false);
+  const [loginLoading, setLoginLoading] = useState(false);
+  const [resetPasswordLoading, setResetPasswordLoading] = useState(false);
+  const [otherLoginOptionsLoading, setOtherLoginOptionsLoading] = useState(false);
+  const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false);
   const { t } = useTranslation();
 
   const logo = getLogo();
@@ -79,7 +88,9 @@ const LoginForm = () => {
   }, []);
 
   const onWeChatLoginClicked = () => {
+    setWechatLoading(true);
     setShowWeChatLoginModal(true);
+    setWechatLoading(false);
   };
 
   const onSubmitWeChatVerificationCode = async () => {
@@ -87,20 +98,27 @@ const LoginForm = () => {
       showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
       return;
     }
-    const res = await API.get(
-      `/api/oauth/wechat?code=${inputs.wechat_verification_code}`,
-    );
-    const { success, message, data } = res.data;
-    if (success) {
-      userDispatch({ type: 'login', payload: data });
-      localStorage.setItem('user', JSON.stringify(data));
-      setUserData(data);
-      updateAPI();
-      navigate('/');
-      showSuccess('登录成功!');
-      setShowWeChatLoginModal(false);
-    } else {
-      showError(message);
+    setWechatCodeSubmitLoading(true);
+    try {
+      const res = await API.get(
+        `/api/oauth/wechat?code=${inputs.wechat_verification_code}`,
+      );
+      const { success, message, data } = res.data;
+      if (success) {
+        userDispatch({ type: 'login', payload: data });
+        localStorage.setItem('user', JSON.stringify(data));
+        setUserData(data);
+        updateAPI();
+        navigate('/');
+        showSuccess('登录成功!');
+        setShowWeChatLoginModal(false);
+      } else {
+        showError(message);
+      }
+    } catch (error) {
+      showError('登录失败,请重试');
+    } finally {
+      setWechatCodeSubmitLoading(false);
     }
   };
 
@@ -114,33 +132,40 @@ const LoginForm = () => {
       return;
     }
     setSubmitted(true);
-    if (username && password) {
-      const res = await API.post(
-        `/api/user/login?turnstile=${turnstileToken}`,
-        {
-          username,
-          password,
-        },
-      );
-      const { success, message, data } = res.data;
-      if (success) {
-        userDispatch({ type: 'login', payload: data });
-        setUserData(data);
-        updateAPI();
-        showSuccess('登录成功!');
-        if (username === 'root' && password === '123456') {
-          Modal.error({
-            title: '您正在使用默认密码!',
-            content: '请立刻修改默认密码!',
-            centered: true,
-          });
+    setLoginLoading(true);
+    try {
+      if (username && password) {
+        const res = await API.post(
+          `/api/user/login?turnstile=${turnstileToken}`,
+          {
+            username,
+            password,
+          },
+        );
+        const { success, message, data } = res.data;
+        if (success) {
+          userDispatch({ type: 'login', payload: data });
+          setUserData(data);
+          updateAPI();
+          showSuccess('登录成功!');
+          if (username === 'root' && password === '123456') {
+            Modal.error({
+              title: '您正在使用默认密码!',
+              content: '请立刻修改默认密码!',
+              centered: true,
+            });
+          }
+          navigate('/console');
+        } else {
+          showError(message);
         }
-        navigate('/console');
       } else {
-        showError(message);
+        showError('请输入用户名和密码!');
       }
-    } else {
-      showError('请输入用户名和密码!');
+    } catch (error) {
+      showError('登录失败,请重试');
+    } finally {
+      setLoginLoading(false);
     }
   }
 
@@ -162,20 +187,81 @@ const LoginForm = () => {
         params[field] = response[field];
       }
     });
-    const res = await API.get(`/api/oauth/telegram/login`, { params });
-    const { success, message, data } = res.data;
-    if (success) {
-      userDispatch({ type: 'login', payload: data });
-      localStorage.setItem('user', JSON.stringify(data));
-      showSuccess('登录成功!');
-      setUserData(data);
-      updateAPI();
-      navigate('/');
-    } else {
-      showError(message);
+    try {
+      const res = await API.get(`/api/oauth/telegram/login`, { params });
+      const { success, message, data } = res.data;
+      if (success) {
+        userDispatch({ type: 'login', payload: data });
+        localStorage.setItem('user', JSON.stringify(data));
+        showSuccess('登录成功!');
+        setUserData(data);
+        updateAPI();
+        navigate('/');
+      } else {
+        showError(message);
+      }
+    } catch (error) {
+      showError('登录失败,请重试');
+    }
+  };
+
+  // 包装的GitHub登录点击处理
+  const handleGitHubClick = () => {
+    setGithubLoading(true);
+    try {
+      onGitHubOAuthClicked(status.github_client_id);
+    } finally {
+      // 由于重定向,这里不会执行到,但为了完整性添加
+      setTimeout(() => setGithubLoading(false), 3000);
+    }
+  };
+
+  // 包装的OIDC登录点击处理
+  const handleOIDCClick = () => {
+    setOidcLoading(true);
+    try {
+      onOIDCClicked(
+        status.oidc_authorization_endpoint,
+        status.oidc_client_id
+      );
+    } finally {
+      // 由于重定向,这里不会执行到,但为了完整性添加
+      setTimeout(() => setOidcLoading(false), 3000);
+    }
+  };
+
+  // 包装的LinuxDO登录点击处理
+  const handleLinuxDOClick = () => {
+    setLinuxdoLoading(true);
+    try {
+      onLinuxDOOAuthClicked(status.linuxdo_client_id);
+    } finally {
+      // 由于重定向,这里不会执行到,但为了完整性添加
+      setTimeout(() => setLinuxdoLoading(false), 3000);
     }
   };
 
+  // 包装的邮箱登录选项点击处理
+  const handleEmailLoginClick = () => {
+    setEmailLoginLoading(true);
+    setShowEmailLogin(true);
+    setEmailLoginLoading(false);
+  };
+
+  // 包装的重置密码点击处理
+  const handleResetPasswordClick = () => {
+    setResetPasswordLoading(true);
+    navigate('/reset');
+    setResetPasswordLoading(false);
+  };
+
+  // 包装的其他登录选项点击处理
+  const handleOtherLoginOptionsClick = () => {
+    setOtherLoginOptionsLoading(true);
+    setShowEmailLogin(false);
+    setOtherLoginOptionsLoading(false);
+  };
+
   const renderOAuthOptions = () => {
     return (
       <div className="flex flex-col items-center">
@@ -199,6 +285,7 @@ const LoginForm = () => {
                     icon={<Icon svg={<WeChatIcon />} style={{ color: '#07C160' }} />}
                     size="large"
                     onClick={onWeChatLoginClicked}
+                    loading={wechatLoading}
                   >
                     <span className="ml-3">{t('使用 微信 继续')}</span>
                   </Button>
@@ -211,7 +298,8 @@ const LoginForm = () => {
                     type="tertiary"
                     icon={<IconGithubLogo size="large" style={{ color: '#24292e' }} />}
                     size="large"
-                    onClick={() => onGitHubOAuthClicked(status.github_client_id)}
+                    onClick={handleGitHubClick}
+                    loading={githubLoading}
                   >
                     <span className="ml-3">{t('使用 GitHub 继续')}</span>
                   </Button>
@@ -224,12 +312,8 @@ const LoginForm = () => {
                     type="tertiary"
                     icon={<OIDCIcon style={{ color: '#1877F2' }} />}
                     size="large"
-                    onClick={() =>
-                      onOIDCClicked(
-                        status.oidc_authorization_endpoint,
-                        status.oidc_client_id
-                      )
-                    }
+                    onClick={handleOIDCClick}
+                    loading={oidcLoading}
                   >
                     <span className="ml-3">{t('使用 OIDC 继续')}</span>
                   </Button>
@@ -242,7 +326,8 @@ const LoginForm = () => {
                     type="tertiary"
                     icon={<LinuxDoIcon style={{ color: '#E95420', width: '20px', height: '20px' }} />}
                     size="large"
-                    onClick={() => onLinuxDOOAuthClicked(status.linuxdo_client_id)}
+                    onClick={handleLinuxDOClick}
+                    loading={linuxdoLoading}
                   >
                     <span className="ml-3">{t('使用 LinuxDO 继续')}</span>
                   </Button>
@@ -267,7 +352,8 @@ const LoginForm = () => {
                   className="w-full h-12 flex items-center justify-center bg-black text-white !rounded-full hover:bg-gray-800 transition-colors"
                   icon={<IconMail size="large" />}
                   size="large"
-                  onClick={() => setShowEmailLogin(true)}
+                  onClick={handleEmailLoginClick}
+                  loading={emailLoginLoading}
                 >
                   <span className="ml-3">{t('使用 邮箱 登录')}</span>
                 </Button>
@@ -340,6 +426,7 @@ const LoginForm = () => {
                     htmlType="submit"
                     size="large"
                     onClick={handleSubmit}
+                    loading={loginLoading}
                   >
                     {t('继续')}
                   </Button>
@@ -349,7 +436,8 @@ const LoginForm = () => {
                     type='tertiary'
                     className="w-full !rounded-full"
                     size="large"
-                    onClick={() => navigate('/reset')}
+                    onClick={handleResetPasswordClick}
+                    loading={resetPasswordLoading}
                   >
                     {t('忘记密码?')}
                   </Button>
@@ -366,7 +454,8 @@ const LoginForm = () => {
                   type="tertiary"
                   className="w-full !rounded-full"
                   size="large"
-                  onClick={() => setShowEmailLogin(false)}
+                  onClick={handleOtherLoginOptionsClick}
+                  loading={otherLoginOptionsLoading}
                 >
                   {t('其他登录选项')}
                 </Button>
@@ -390,6 +479,9 @@ const LoginForm = () => {
         okText={t('登录')}
         size="small"
         centered={true}
+        okButtonProps={{
+          loading: wechatCodeSubmitLoading,
+        }}
       >
         <div className="flex flex-col items-center">
           <img src={status.wechat_qrcode} alt="微信二维码" className="mb-4" />

+ 141 - 65
web/src/components/RegisterForm.js

@@ -55,6 +55,15 @@ const RegisterForm = () => {
   const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
   const [showEmailRegister, setShowEmailRegister] = useState(false);
   const [status, setStatus] = useState({});
+  const [wechatLoading, setWechatLoading] = useState(false);
+  const [githubLoading, setGithubLoading] = useState(false);
+  const [oidcLoading, setOidcLoading] = useState(false);
+  const [linuxdoLoading, setLinuxdoLoading] = useState(false);
+  const [emailRegisterLoading, setEmailRegisterLoading] = useState(false);
+  const [registerLoading, setRegisterLoading] = useState(false);
+  const [verificationCodeLoading, setVerificationCodeLoading] = useState(false);
+  const [otherRegisterOptionsLoading, setOtherRegisterOptionsLoading] = useState(false);
+  const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false);
   let navigate = useNavigate();
 
   const logo = getLogo();
@@ -79,7 +88,9 @@ const RegisterForm = () => {
   }, []);
 
   const onWeChatLoginClicked = () => {
+    setWechatLoading(true);
     setShowWeChatLoginModal(true);
+    setWechatLoading(false);
   };
 
   const onSubmitWeChatVerificationCode = async () => {
@@ -87,20 +98,27 @@ const RegisterForm = () => {
       showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
       return;
     }
-    const res = await API.get(
-      `/api/oauth/wechat?code=${inputs.wechat_verification_code}`,
-    );
-    const { success, message, data } = res.data;
-    if (success) {
-      userDispatch({ type: 'login', payload: data });
-      localStorage.setItem('user', JSON.stringify(data));
-      setUserData(data);
-      updateAPI();
-      navigate('/');
-      showSuccess('登录成功!');
-      setShowWeChatLoginModal(false);
-    } else {
-      showError(message);
+    setWechatCodeSubmitLoading(true);
+    try {
+      const res = await API.get(
+        `/api/oauth/wechat?code=${inputs.wechat_verification_code}`,
+      );
+      const { success, message, data } = res.data;
+      if (success) {
+        userDispatch({ type: 'login', payload: data });
+        localStorage.setItem('user', JSON.stringify(data));
+        setUserData(data);
+        updateAPI();
+        navigate('/');
+        showSuccess('登录成功!');
+        setShowWeChatLoginModal(false);
+      } else {
+        showError(message);
+      }
+    } catch (error) {
+      showError('登录失败,请重试');
+    } finally {
+      setWechatCodeSubmitLoading(false);
     }
   };
 
@@ -122,23 +140,28 @@ const RegisterForm = () => {
         showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
         return;
       }
-      setLoading(true);
-      if (!affCode) {
-        affCode = localStorage.getItem('aff');
+      setRegisterLoading(true);
+      try {
+        if (!affCode) {
+          affCode = localStorage.getItem('aff');
+        }
+        inputs.aff_code = affCode;
+        const res = await API.post(
+          `/api/user/register?turnstile=${turnstileToken}`,
+          inputs,
+        );
+        const { success, message } = res.data;
+        if (success) {
+          navigate('/login');
+          showSuccess('注册成功!');
+        } else {
+          showError(message);
+        }
+      } catch (error) {
+        showError('注册失败,请重试');
+      } finally {
+        setRegisterLoading(false);
       }
-      inputs.aff_code = affCode;
-      const res = await API.post(
-        `/api/user/register?turnstile=${turnstileToken}`,
-        inputs,
-      );
-      const { success, message } = res.data;
-      if (success) {
-        navigate('/login');
-        showSuccess('注册成功!');
-      } else {
-        showError(message);
-      }
-      setLoading(false);
     }
   }
 
@@ -148,17 +171,64 @@ const RegisterForm = () => {
       showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
       return;
     }
-    setLoading(true);
-    const res = await API.get(
-      `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`,
-    );
-    const { success, message } = res.data;
-    if (success) {
-      showSuccess('验证码发送成功,请检查你的邮箱!');
-    } else {
-      showError(message);
+    setVerificationCodeLoading(true);
+    try {
+      const res = await API.get(
+        `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`,
+      );
+      const { success, message } = res.data;
+      if (success) {
+        showSuccess('验证码发送成功,请检查你的邮箱!');
+      } else {
+        showError(message);
+      }
+    } catch (error) {
+      showError('发送验证码失败,请重试');
+    } finally {
+      setVerificationCodeLoading(false);
     }
-    setLoading(false);
+  };
+
+  const handleGitHubClick = () => {
+    setGithubLoading(true);
+    try {
+      onGitHubOAuthClicked(status.github_client_id);
+    } finally {
+      setTimeout(() => setGithubLoading(false), 3000);
+    }
+  };
+
+  const handleOIDCClick = () => {
+    setOidcLoading(true);
+    try {
+      onOIDCClicked(
+        status.oidc_authorization_endpoint,
+        status.oidc_client_id
+      );
+    } finally {
+      setTimeout(() => setOidcLoading(false), 3000);
+    }
+  };
+
+  const handleLinuxDOClick = () => {
+    setLinuxdoLoading(true);
+    try {
+      onLinuxDOOAuthClicked(status.linuxdo_client_id);
+    } finally {
+      setTimeout(() => setLinuxdoLoading(false), 3000);
+    }
+  };
+
+  const handleEmailRegisterClick = () => {
+    setEmailRegisterLoading(true);
+    setShowEmailRegister(true);
+    setEmailRegisterLoading(false);
+  };
+
+  const handleOtherRegisterOptionsClick = () => {
+    setOtherRegisterOptionsLoading(true);
+    setShowEmailRegister(false);
+    setOtherRegisterOptionsLoading(false);
   };
 
   const onTelegramLoginClicked = async (response) => {
@@ -178,17 +248,21 @@ const RegisterForm = () => {
         params[field] = response[field];
       }
     });
-    const res = await API.get(`/api/oauth/telegram/login`, { params });
-    const { success, message, data } = res.data;
-    if (success) {
-      userDispatch({ type: 'login', payload: data });
-      localStorage.setItem('user', JSON.stringify(data));
-      showSuccess('登录成功!');
-      setUserData(data);
-      updateAPI();
-      navigate('/');
-    } else {
-      showError(message);
+    try {
+      const res = await API.get(`/api/oauth/telegram/login`, { params });
+      const { success, message, data } = res.data;
+      if (success) {
+        userDispatch({ type: 'login', payload: data });
+        localStorage.setItem('user', JSON.stringify(data));
+        showSuccess('登录成功!');
+        setUserData(data);
+        updateAPI();
+        navigate('/');
+      } else {
+        showError(message);
+      }
+    } catch (error) {
+      showError('登录失败,请重试');
     }
   };
 
@@ -215,6 +289,7 @@ const RegisterForm = () => {
                     icon={<Icon svg={<WeChatIcon />} style={{ color: '#07C160' }} />}
                     size="large"
                     onClick={onWeChatLoginClicked}
+                    loading={wechatLoading}
                   >
                     <span className="ml-3">{t('使用 微信 继续')}</span>
                   </Button>
@@ -227,7 +302,8 @@ const RegisterForm = () => {
                     type="tertiary"
                     icon={<IconGithubLogo size="large" style={{ color: '#24292e' }} />}
                     size="large"
-                    onClick={() => onGitHubOAuthClicked(status.github_client_id)}
+                    onClick={handleGitHubClick}
+                    loading={githubLoading}
                   >
                     <span className="ml-3">{t('使用 GitHub 继续')}</span>
                   </Button>
@@ -240,12 +316,8 @@ const RegisterForm = () => {
                     type="tertiary"
                     icon={<OIDCIcon style={{ color: '#1877F2' }} />}
                     size="large"
-                    onClick={() =>
-                      onOIDCClicked(
-                        status.oidc_authorization_endpoint,
-                        status.oidc_client_id
-                      )
-                    }
+                    onClick={handleOIDCClick}
+                    loading={oidcLoading}
                   >
                     <span className="ml-3">{t('使用 OIDC 继续')}</span>
                   </Button>
@@ -258,7 +330,8 @@ const RegisterForm = () => {
                     type="tertiary"
                     icon={<LinuxDoIcon style={{ color: '#E95420', width: '20px', height: '20px' }} />}
                     size="large"
-                    onClick={() => onLinuxDOOAuthClicked(status.linuxdo_client_id)}
+                    onClick={handleLinuxDOClick}
+                    loading={linuxdoLoading}
                   >
                     <span className="ml-3">{t('使用 LinuxDO 继续')}</span>
                   </Button>
@@ -283,7 +356,8 @@ const RegisterForm = () => {
                   className="w-full h-12 flex items-center justify-center bg-black text-white !rounded-full hover:bg-gray-800 transition-colors"
                   icon={<IconMail size="large" />}
                   size="large"
-                  onClick={() => setShowEmailRegister(true)}
+                  onClick={handleEmailRegisterClick}
+                  loading={emailRegisterLoading}
                 >
                   <span className="ml-3">{t('使用 邮箱 注册')}</span>
                 </Button>
@@ -375,7 +449,7 @@ const RegisterForm = () => {
                       suffix={
                         <Button
                           onClick={sendVerificationCode}
-                          disabled={loading}
+                          loading={verificationCodeLoading}
                           size="small"
                           className="!rounded-md mr-2"
                         >
@@ -404,6 +478,7 @@ const RegisterForm = () => {
                     htmlType="submit"
                     size="large"
                     onClick={handleSubmit}
+                    loading={registerLoading}
                   >
                     {t('注册')}
                   </Button>
@@ -420,7 +495,8 @@ const RegisterForm = () => {
                   type="tertiary"
                   className="w-full !rounded-full"
                   size="large"
-                  onClick={() => setShowEmailRegister(false)}
+                  onClick={handleOtherRegisterOptionsClick}
+                  loading={otherRegisterOptionsLoading}
                 >
                   {t('其他注册选项')}
                 </Button>
@@ -436,7 +512,6 @@ const RegisterForm = () => {
     );
   };
 
-  // 微信登录模态框
   const renderWeChatLoginModal = () => {
     return (
       <Modal
@@ -448,6 +523,9 @@ const RegisterForm = () => {
         okText={t('登录')}
         size="small"
         centered={true}
+        okButtonProps={{
+          loading: wechatCodeSubmitLoading,
+        }}
       >
         <div className="flex flex-col items-center">
           <img src={status.wechat_qrcode} alt="微信二维码" className="mb-4" />
@@ -472,7 +550,6 @@ const RegisterForm = () => {
 
   return (
     <div className="min-h-screen relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden">
-      {/* 背景图片容器 - 放大并保持居中 */}
       <div
         className="absolute inset-0 z-0 bg-cover bg-center scale-125 opacity-100"
         style={{
@@ -480,7 +557,6 @@ const RegisterForm = () => {
         }}
       ></div>
 
-      {/* 半透明遮罩层 */}
       <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>
 
       <div className="w-full max-w-md relative z-10">