'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' interface ProvidersProps { children: ReactNode } const ReactQueryDevtoolsProduction = React.lazy(() => import('@tanstack/react-query-devtools/build/modern/production.js').then((d) => ({ default: d.ReactQueryDevtools, })) ) const EXPIRED_ERROR_CODES = ['10050001', '10050002', '10050003', '10050004', '10050005', '10050006'] export function Providers({ children }: ProvidersProps) { const router = useRouter() // 用于错误去重的引用 const errorTimeoutRef = useRef(null) const lastErrorRef = useRef('') const [showDevtools, setShowDevtools] = React.useState(false) 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) => { handleError(error as ApiError) }, }), mutationCache: new MutationCache({ onError: (error) => { handleError(error as ApiError) }, }), }) ) const pathname = usePathname() const searchParams = useSearchParams() const searchParamsString = searchParams.toString() const redirectURL = `${pathname}${searchParamsString ? `?${searchParamsString}` : ''}` const handleError = (error: ApiError) => { if (error.errorCode === COIN_INSUFFICIENT_ERROR_CODE) { queryClient.invalidateQueries({ queryKey: walletKeys.getWalletBalance() }) } if (EXPIRED_ERROR_CODES.includes(error.errorCode)) { // 清除 cookie 中的 st tokenManager.removeToken() router.push('/login?redirect=' + encodeURIComponent(redirectURL)) return // 对于登录过期错误,不显示错误toast,直接跳转 } if (error.ignoreError) { return } // 错误去重逻辑:只显示最后一次的错误 const errorKey = `${error.errorCode}:${error.errorMsg}` // 清除之前的定时器 if (errorTimeoutRef.current) { clearTimeout(errorTimeoutRef.current) } // 更新最后一次错误信息 lastErrorRef.current = errorKey // 设置新的定时器,延迟显示错误 errorTimeoutRef.current = setTimeout(() => { // 只有当前错误仍然是最后一次错误时才显示 if (lastErrorRef.current === errorKey) { toast.error(error.errorMsg) } }, 100) // 100ms 延迟,确保能捕获到快速连续的错误 } React.useEffect(() => { // @ts-ignore window.toggleDevtools = () => setShowDevtools((old) => !old) }, []) return ( {children} , success: , }} toastOptions={{ className: '!bg-surface-base-normal !border-none !px-4 !py-3 !rounded-m !txt-body-m !text-txt-primary-normal', }} /> {showDevtools && ( )} ) }