crush-level-web/src/lib/providers.tsx

133 lines
4.1 KiB
TypeScript
Raw Normal View History

2025-12-11 11:31:56 +00:00
'use client';
import { ApiError } from '@/types/api';
import { MutationCache, QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import React, { useState, useRef, type ReactNode } from 'react';
import { toast, Toaster } from 'sonner';
import { tokenManager } from './auth/token';
import { COIN_INSUFFICIENT_ERROR_CODE } from '@/hooks/useWallet';
import { walletKeys } from './query-keys';
2025-11-13 08:38:25 +00:00
interface ProvidersProps {
2025-12-11 11:31:56 +00:00
children: ReactNode;
2025-11-13 08:38:25 +00:00
}
2025-12-11 11:31:56 +00:00
// const ReactQueryDevtoolsProduction = React.lazy(() =>
// import('@tanstack/react-query-devtools/build/modern/production.js').then((d) => ({
// default: d.ReactQueryDevtools,
// }))
// );
2025-11-13 08:38:25 +00:00
2025-12-11 11:31:56 +00:00
const EXPIRED_ERROR_CODES = [
'10050001',
'10050002',
'10050003',
'10050004',
'10050005',
'10050006',
];
2025-11-13 08:38:25 +00:00
export function Providers({ children }: ProvidersProps) {
2025-12-11 11:31:56 +00:00
const router = useRouter();
2025-11-13 08:38:25 +00:00
// 用于错误去重的引用
2025-12-11 11:31:56 +00:00
const errorTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const lastErrorRef = useRef<string>('');
2025-11-28 06:31:36 +00:00
2025-11-13 08:38:25 +00:00
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// 全局查询配置
staleTime: 5 * 60 * 1000, // 5分钟后数据过期
gcTime: 10 * 60 * 1000, // 10分钟后清除缓存
refetchOnWindowFocus: false, // 窗口聚焦时不重新请求
retry: 0, // 失败重试次数
},
mutations: {
// 全局修改配置
retry: 0,
},
},
queryCache: new QueryCache({
onError: (error) => {
2025-12-11 11:31:56 +00:00
handleError(error as ApiError);
2025-11-28 06:31:36 +00:00
},
2025-11-13 08:38:25 +00:00
}),
mutationCache: new MutationCache({
onError: (error) => {
2025-12-11 11:31:56 +00:00
handleError(error as ApiError);
2025-11-28 06:31:36 +00:00
},
}),
2025-11-13 08:38:25 +00:00
})
2025-12-11 11:31:56 +00:00
);
const pathname = usePathname();
const searchParams = useSearchParams();
2025-11-13 08:38:25 +00:00
2025-12-11 11:31:56 +00:00
const searchParamsString = searchParams.toString();
const redirectURL = `${pathname}${searchParamsString ? `?${searchParamsString}` : ''}`;
2025-11-13 08:38:25 +00:00
const handleError = (error: ApiError) => {
if (error.errorCode === COIN_INSUFFICIENT_ERROR_CODE) {
2025-12-11 11:31:56 +00:00
queryClient.invalidateQueries({ queryKey: walletKeys.getWalletBalance() });
2025-11-13 08:38:25 +00:00
}
if (EXPIRED_ERROR_CODES.includes(error.errorCode)) {
2025-12-15 11:31:18 +00:00
// TODO: 清除 cookie 中的 st
// tokenManager.removeToken();
// router.push('/login?redirect=' + encodeURIComponent(redirectURL));
2025-12-11 11:31:56 +00:00
return; // 对于登录过期错误不显示错误toast直接跳转
2025-11-13 08:38:25 +00:00
}
if (error.ignoreError) {
2025-12-11 11:31:56 +00:00
return;
2025-11-13 08:38:25 +00:00
}
// 错误去重逻辑:只显示最后一次的错误
2025-12-11 11:31:56 +00:00
const errorKey = `${error.errorCode}:${error.errorMsg}`;
2025-11-28 06:31:36 +00:00
2025-11-13 08:38:25 +00:00
// 清除之前的定时器
if (errorTimeoutRef.current) {
2025-12-11 11:31:56 +00:00
clearTimeout(errorTimeoutRef.current);
2025-11-13 08:38:25 +00:00
}
2025-11-28 06:31:36 +00:00
2025-11-13 08:38:25 +00:00
// 更新最后一次错误信息
2025-12-11 11:31:56 +00:00
lastErrorRef.current = errorKey;
2025-11-28 06:31:36 +00:00
2025-11-13 08:38:25 +00:00
// 设置新的定时器,延迟显示错误
errorTimeoutRef.current = setTimeout(() => {
// 只有当前错误仍然是最后一次错误时才显示
if (lastErrorRef.current === errorKey) {
2025-12-11 11:31:56 +00:00
toast.error(error.errorMsg);
2025-11-13 08:38:25 +00:00
}
2025-12-11 11:31:56 +00:00
}, 100); // 100ms 延迟,确保能捕获到快速连续的错误
};
2025-11-13 08:38:25 +00:00
return (
<QueryClientProvider client={queryClient}>
{children}
2025-11-28 06:31:36 +00:00
<Toaster
2025-11-13 08:38:25 +00:00
position="top-center"
richColors
// closeButton=
visibleToasts={1}
duration={4000}
// duration={2000000}
icons={{
error: <img src="/icons/status-error.svg" />,
success: <img src="/icons/status-successful.svg" />,
}}
toastOptions={{
2025-11-28 06:31:36 +00:00
className:
'!bg-surface-base-normal !border-none !px-4 !py-3 !rounded-m !txt-body-m !text-txt-primary-normal',
2025-11-13 08:38:25 +00:00
}}
/>
2025-12-11 11:31:56 +00:00
{/* <ReactQueryDevtools initialIsOpen={true} /> */}
2025-11-13 08:38:25 +00:00
2025-12-11 11:31:56 +00:00
{/* {showDevtools && (
2025-11-13 08:38:25 +00:00
<React.Suspense fallback={null}>
<ReactQueryDevtoolsProduction />
</React.Suspense>
2025-12-11 11:31:56 +00:00
)} */}
2025-11-13 08:38:25 +00:00
</QueryClientProvider>
2025-12-11 11:31:56 +00:00
);
2025-11-28 06:31:36 +00:00
}