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.
This commit is contained in:
Apple\Apple
2025-05-22 21:42:21 +08:00
parent e07163c568
commit 0f3216564d
2 changed files with 293 additions and 125 deletions

View File

@@ -53,6 +53,15 @@ const LoginForm = () => {
const [status, setStatus] = useState({}); const [status, setStatus] = useState({});
const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false); const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
const [showEmailLogin, setShowEmailLogin] = 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 { t } = useTranslation();
const logo = getLogo(); const logo = getLogo();
@@ -79,7 +88,9 @@ const LoginForm = () => {
}, []); }, []);
const onWeChatLoginClicked = () => { const onWeChatLoginClicked = () => {
setWechatLoading(true);
setShowWeChatLoginModal(true); setShowWeChatLoginModal(true);
setWechatLoading(false);
}; };
const onSubmitWeChatVerificationCode = async () => { const onSubmitWeChatVerificationCode = async () => {
@@ -87,6 +98,8 @@ const LoginForm = () => {
showInfo('请稍后几秒重试Turnstile 正在检查用户环境!'); showInfo('请稍后几秒重试Turnstile 正在检查用户环境!');
return; return;
} }
setWechatCodeSubmitLoading(true);
try {
const res = await API.get( const res = await API.get(
`/api/oauth/wechat?code=${inputs.wechat_verification_code}`, `/api/oauth/wechat?code=${inputs.wechat_verification_code}`,
); );
@@ -102,6 +115,11 @@ const LoginForm = () => {
} else { } else {
showError(message); showError(message);
} }
} catch (error) {
showError('登录失败,请重试');
} finally {
setWechatCodeSubmitLoading(false);
}
}; };
function handleChange(name, value) { function handleChange(name, value) {
@@ -114,6 +132,8 @@ const LoginForm = () => {
return; return;
} }
setSubmitted(true); setSubmitted(true);
setLoginLoading(true);
try {
if (username && password) { if (username && password) {
const res = await API.post( const res = await API.post(
`/api/user/login?turnstile=${turnstileToken}`, `/api/user/login?turnstile=${turnstileToken}`,
@@ -142,6 +162,11 @@ const LoginForm = () => {
} else { } else {
showError('请输入用户名和密码!'); showError('请输入用户名和密码!');
} }
} catch (error) {
showError('登录失败,请重试');
} finally {
setLoginLoading(false);
}
} }
// 添加Telegram登录处理函数 // 添加Telegram登录处理函数
@@ -162,6 +187,7 @@ const LoginForm = () => {
params[field] = response[field]; params[field] = response[field];
} }
}); });
try {
const res = await API.get(`/api/oauth/telegram/login`, { params }); const res = await API.get(`/api/oauth/telegram/login`, { params });
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
@@ -174,6 +200,66 @@ const LoginForm = () => {
} else { } else {
showError(message); 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 = () => { const renderOAuthOptions = () => {
@@ -199,6 +285,7 @@ const LoginForm = () => {
icon={<Icon svg={<WeChatIcon />} style={{ color: '#07C160' }} />} icon={<Icon svg={<WeChatIcon />} style={{ color: '#07C160' }} />}
size="large" size="large"
onClick={onWeChatLoginClicked} onClick={onWeChatLoginClicked}
loading={wechatLoading}
> >
<span className="ml-3">{t('使用 微信 继续')}</span> <span className="ml-3">{t('使用 微信 继续')}</span>
</Button> </Button>
@@ -211,7 +298,8 @@ const LoginForm = () => {
type="tertiary" type="tertiary"
icon={<IconGithubLogo size="large" style={{ color: '#24292e' }} />} icon={<IconGithubLogo size="large" style={{ color: '#24292e' }} />}
size="large" size="large"
onClick={() => onGitHubOAuthClicked(status.github_client_id)} onClick={handleGitHubClick}
loading={githubLoading}
> >
<span className="ml-3">{t('使用 GitHub 继续')}</span> <span className="ml-3">{t('使用 GitHub 继续')}</span>
</Button> </Button>
@@ -224,12 +312,8 @@ const LoginForm = () => {
type="tertiary" type="tertiary"
icon={<OIDCIcon style={{ color: '#1877F2' }} />} icon={<OIDCIcon style={{ color: '#1877F2' }} />}
size="large" size="large"
onClick={() => onClick={handleOIDCClick}
onOIDCClicked( loading={oidcLoading}
status.oidc_authorization_endpoint,
status.oidc_client_id
)
}
> >
<span className="ml-3">{t('使用 OIDC 继续')}</span> <span className="ml-3">{t('使用 OIDC 继续')}</span>
</Button> </Button>
@@ -242,7 +326,8 @@ const LoginForm = () => {
type="tertiary" type="tertiary"
icon={<LinuxDoIcon style={{ color: '#E95420', width: '20px', height: '20px' }} />} icon={<LinuxDoIcon style={{ color: '#E95420', width: '20px', height: '20px' }} />}
size="large" size="large"
onClick={() => onLinuxDOOAuthClicked(status.linuxdo_client_id)} onClick={handleLinuxDOClick}
loading={linuxdoLoading}
> >
<span className="ml-3">{t('使用 LinuxDO 继续')}</span> <span className="ml-3">{t('使用 LinuxDO 继续')}</span>
</Button> </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" 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" />} icon={<IconMail size="large" />}
size="large" size="large"
onClick={() => setShowEmailLogin(true)} onClick={handleEmailLoginClick}
loading={emailLoginLoading}
> >
<span className="ml-3">{t('使用 邮箱 登录')}</span> <span className="ml-3">{t('使用 邮箱 登录')}</span>
</Button> </Button>
@@ -340,6 +426,7 @@ const LoginForm = () => {
htmlType="submit" htmlType="submit"
size="large" size="large"
onClick={handleSubmit} onClick={handleSubmit}
loading={loginLoading}
> >
{t('继续')} {t('继续')}
</Button> </Button>
@@ -349,7 +436,8 @@ const LoginForm = () => {
type='tertiary' type='tertiary'
className="w-full !rounded-full" className="w-full !rounded-full"
size="large" size="large"
onClick={() => navigate('/reset')} onClick={handleResetPasswordClick}
loading={resetPasswordLoading}
> >
{t('忘记密码?')} {t('忘记密码?')}
</Button> </Button>
@@ -366,7 +454,8 @@ const LoginForm = () => {
type="tertiary" type="tertiary"
className="w-full !rounded-full" className="w-full !rounded-full"
size="large" size="large"
onClick={() => setShowEmailLogin(false)} onClick={handleOtherLoginOptionsClick}
loading={otherLoginOptionsLoading}
> >
{t('其他登录选项')} {t('其他登录选项')}
</Button> </Button>
@@ -390,6 +479,9 @@ const LoginForm = () => {
okText={t('登录')} okText={t('登录')}
size="small" size="small"
centered={true} centered={true}
okButtonProps={{
loading: wechatCodeSubmitLoading,
}}
> >
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<img src={status.wechat_qrcode} alt="微信二维码" className="mb-4" /> <img src={status.wechat_qrcode} alt="微信二维码" className="mb-4" />

View File

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