♻️ refactor(auth, ui): simplify Loading component & optimize OAuth2Callback flow

* Removed `prompt` prop from `Loading` and switched to built-in Spin indicator with default size `small`
* Dropped overlay background to make the spinner more reusable
* Replaced custom text span; callers can now supply tip via their own UI if needed
* Cleaned up `OAuth2Callback`:
  - Eliminated unused state/variables
  - Added MAX_RETRIES with incremental back-off
  - Centralized error handling via try/catch
  - Streamlined navigation logic on success/failure
  - Updated imports to match new Loading signature

BREAKING CHANGE: `Loading` no longer accepts a `prompt` prop. Update all invocations accordingly.
This commit is contained in:
t0ng7u
2025-07-16 04:21:13 +08:00
parent 8604c9f9d5
commit bbc5584f80
3 changed files with 45 additions and 42 deletions

View File

@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { API, showError, showSuccess, updateAPI, setUserData } from '../../helpers'; import { API, showError, showSuccess, updateAPI, setUserData } from '../../helpers';
@@ -7,22 +7,28 @@ import Loading from '../common/Loading';
const OAuth2Callback = (props) => { const OAuth2Callback = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [, userDispatch] = useContext(UserContext);
const navigate = useNavigate();
const [userState, userDispatch] = useContext(UserContext); // 最大重试次数
const [prompt, setPrompt] = useState(t('处理中...')); const MAX_RETRIES = 3;
let navigate = useNavigate(); const sendCode = async (code, state, retry = 0) => {
try {
const { data: resData } = await API.get(
`/api/oauth/${props.type}?code=${code}&state=${state}`,
);
const { success, message, data } = resData;
if (!success) {
throw new Error(message || 'OAuth2 callback error');
}
const sendCode = async (code, state, count) => {
const res = await API.get(
`/api/oauth/${props.type}?code=${code}&state=${state}`,
);
const { success, message, data } = res.data;
if (success) {
if (message === 'bind') { if (message === 'bind') {
showSuccess(t('绑定成功!')); showSuccess(t('绑定成功!'));
navigate('/console/setting'); navigate('/console/personal');
} else { } else {
userDispatch({ type: 'login', payload: data }); userDispatch({ type: 'login', payload: data });
localStorage.setItem('user', JSON.stringify(data)); localStorage.setItem('user', JSON.stringify(data));
@@ -31,27 +37,34 @@ const OAuth2Callback = (props) => {
showSuccess(t('登录成功!')); showSuccess(t('登录成功!'));
navigate('/console/token'); navigate('/console/token');
} }
} else { } catch (error) {
showError(message); if (retry < MAX_RETRIES) {
if (count === 0) { // 递增的退避等待
setPrompt(t('操作失败,重定向至登录界面中...')); await new Promise((resolve) => setTimeout(resolve, (retry + 1) * 2000));
navigate('/console/setting'); // in case this is failed to bind GitHub return sendCode(code, state, retry + 1);
return;
} }
count++;
setPrompt(t('出现错误,第 ${count} 次重试中...', { count })); // 重试次数耗尽,提示错误并返回设置页面
await new Promise((resolve) => setTimeout(resolve, count * 2000)); showError(error.message || t('授权失败'));
await sendCode(code, state, count); navigate('/console/personal');
} }
}; };
useEffect(() => { useEffect(() => {
let code = searchParams.get('code'); const code = searchParams.get('code');
let state = searchParams.get('state'); const state = searchParams.get('state');
sendCode(code, state, 0).then();
// 参数缺失直接返回
if (!code) {
showError(t('未获取到授权码'));
navigate('/console/personal');
return;
}
sendCode(code, state);
}, []); }, []);
return <Loading prompt={prompt} />; return <Loading />;
}; };
export default OAuth2Callback; export default OAuth2Callback;

View File

@@ -1,22 +1,14 @@
import React from 'react'; import React from 'react';
import { Spin } from '@douyinfe/semi-ui'; import { Spin } from '@douyinfe/semi-ui';
import { useTranslation } from 'react-i18next';
const Loading = ({ prompt: name = '', size = 'large' }) => { const Loading = ({ size = 'small' }) => {
const { t } = useTranslation();
return ( return (
<div className="fixed inset-0 w-screen h-screen flex items-center justify-center bg-white/80 z-[1000]"> <div className="fixed inset-0 w-screen h-screen flex items-center justify-center">
<div className="flex flex-col items-center"> <Spin
<Spin size={size}
size={size} spinning={true}
spinning={true} />
tip={null}
/>
<span className="whitespace-nowrap mt-2 text-center" style={{ color: 'var(--semi-color-primary)' }}>
{name ? t('{{name}}', { name }) : t('加载中...')}
</span>
</div>
</div> </div>
); );
}; };

View File

@@ -179,7 +179,6 @@
"注销": "Logout", "注销": "Logout",
"登录": "Sign in", "登录": "Sign in",
"注册": "Sign up", "注册": "Sign up",
"加载{name}中...": "Loading {name}...",
"未登录或登录已过期,请重新登录!": "Not logged in or session expired. Please login again!", "未登录或登录已过期,请重新登录!": "Not logged in or session expired. Please login again!",
"用户登录": "User Login", "用户登录": "User Login",
"密码": "Password", "密码": "Password",
@@ -933,7 +932,6 @@
"更新令牌后需等待几分钟生效": "It will take a few minutes to take effect after updating the token.", "更新令牌后需等待几分钟生效": "It will take a few minutes to take effect after updating the token.",
"一小时": "One hour", "一小时": "One hour",
"新建数量": "New quantity", "新建数量": "New quantity",
"加载失败,请稍后重试": "Loading failed, please try again later",
"未设置": "Not set", "未设置": "Not set",
"API文档": "API documentation", "API文档": "API documentation",
"不是合法的 JSON 字符串": "Not a valid JSON string", "不是合法的 JSON 字符串": "Not a valid JSON string",