2025-11-28 06:31:36 +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 {
|
|
|
|
|
|
children: ReactNode
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const ReactQueryDevtoolsProduction = React.lazy(() =>
|
2025-11-28 06:31:36 +00:00
|
|
|
|
import('@tanstack/react-query-devtools/build/modern/production.js').then((d) => ({
|
|
|
|
|
|
default: d.ReactQueryDevtools,
|
|
|
|
|
|
}))
|
2025-11-13 08:38:25 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-11-28 06:31:36 +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-11-28 06:31:36 +00:00
|
|
|
|
const router = useRouter()
|
2025-11-13 08:38:25 +00:00
|
|
|
|
// 用于错误去重的引用
|
2025-11-28 06:31:36 +00:00
|
|
|
|
const errorTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
|
|
|
|
|
const lastErrorRef = useRef<string>('')
|
2025-11-13 08:38:25 +00:00
|
|
|
|
const [showDevtools, setShowDevtools] = React.useState(false)
|
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) => {
|
|
|
|
|
|
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) => {
|
|
|
|
|
|
handleError(error as ApiError)
|
2025-11-28 06:31:36 +00:00
|
|
|
|
},
|
|
|
|
|
|
}),
|
2025-11-13 08:38:25 +00:00
|
|
|
|
})
|
|
|
|
|
|
)
|
2025-11-28 06:31:36 +00:00
|
|
|
|
const pathname = usePathname()
|
|
|
|
|
|
const searchParams = useSearchParams()
|
2025-11-13 08:38:25 +00:00
|
|
|
|
|
|
|
|
|
|
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
|
2025-11-28 06:31:36 +00:00
|
|
|
|
tokenManager.removeToken()
|
|
|
|
|
|
router.push('/login?redirect=' + encodeURIComponent(redirectURL))
|
|
|
|
|
|
return // 对于登录过期错误,不显示错误toast,直接跳转
|
2025-11-13 08:38:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
if (error.ignoreError) {
|
2025-11-28 06:31:36 +00:00
|
|
|
|
return
|
2025-11-13 08:38:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 错误去重逻辑:只显示最后一次的错误
|
2025-11-28 06:31:36 +00:00
|
|
|
|
const errorKey = `${error.errorCode}:${error.errorMsg}`
|
|
|
|
|
|
|
2025-11-13 08:38:25 +00:00
|
|
|
|
// 清除之前的定时器
|
|
|
|
|
|
if (errorTimeoutRef.current) {
|
2025-11-28 06:31:36 +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-11-28 06:31:36 +00:00
|
|
|
|
lastErrorRef.current = errorKey
|
|
|
|
|
|
|
2025-11-13 08:38:25 +00:00
|
|
|
|
// 设置新的定时器,延迟显示错误
|
|
|
|
|
|
errorTimeoutRef.current = setTimeout(() => {
|
|
|
|
|
|
// 只有当前错误仍然是最后一次错误时才显示
|
|
|
|
|
|
if (lastErrorRef.current === errorKey) {
|
|
|
|
|
|
toast.error(error.errorMsg)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 100) // 100ms 延迟,确保能捕获到快速连续的错误
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
|
window.toggleDevtools = () => setShowDevtools((old) => !old)
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<ReactQueryDevtools initialIsOpen={true} />
|
|
|
|
|
|
|
|
|
|
|
|
{showDevtools && (
|
|
|
|
|
|
<React.Suspense fallback={null}>
|
|
|
|
|
|
<ReactQueryDevtoolsProduction />
|
|
|
|
|
|
</React.Suspense>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</QueryClientProvider>
|
|
|
|
|
|
)
|
2025-11-28 06:31:36 +00:00
|
|
|
|
}
|