返回 AiToEarn
Providers.tsx
根目录 / project / aitoearn-web / src / app / layout / Providers.tsx
1 /**
2 * Providers - 全局 Provider 组件
3 * 包含 Google OAuth、Ant Design 配置、Toast、主题等全局配置
4 */
5
6 'use client'
7
8 import { GoogleOAuthProvider } from '@react-oauth/google'
9 import { ThemeProvider } from 'next-themes'
10 import { usePathname } from 'next/navigation'
11 import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState } from 'react'
12 import { useShallow } from 'zustand/shallow'
13 import ConfigManagerDialog from '@/app/layout/ConfigManagerDialog'
14 import LoginDialog from '@/app/layout/LoginDialog'
15 import SettingsModal from '@/app/layout/SettingsModal'
16 import { isPublicPage } from '@/app/layout/shared/utils/routeUtils'
17 import { WechatBrowserOverlay } from '@/components/common/WechatBrowserOverlay'
18 import { PluginPublishingFloatButton } from '@/components/Plugin'
19 import NotificationCenter from '@/components/ui/NotificationCenter'
20 import { Toaster } from '@/components/ui/sonner'
21 import { useConfigManagerDialogStore } from '@/store/configManagerDialog'
22 import { useLoginDialogStore } from '@/store/login-dialog'
23 import { usePlatformMetadataStore } from '@/store/platformMetadata'
24 import { useSettingsModalStore } from '@/store/settingsModal'
25 import { useUserStore } from '@/store/user'
26
27 const PublicRouteContext = createContext(false)
28
29 export function usePublicRoute() {
30 return useContext(PublicRouteContext)
31 }
32
33 export function Providers({
34 children,
35 lng,
36 autoLoginToken,
37 }: {
38 children: React.ReactNode
39 lng: string
40 autoLoginToken?: string
41 }) {
42 const pathname = usePathname()
43 const publicRoute = isPublicPage(pathname)
44 // 用于追踪是否已经在当前路由弹出过登录框,避免重复弹出
45 const hasPromptedRef = useRef(false)
46 const [authInitialized, setAuthInitialized] = useState(false)
47
48 const { _hasHydrated, token } = useUserStore(
49 useShallow(state => ({
50 _hasHydrated: state._hasHydrated,
51 token: state.token,
52 })),
53 )
54
55 useEffect(() => {
56 if (usePlatformMetadataStore.getState().loadedLng === lng)
57 return
58 usePlatformMetadataStore.getState().ensureLoaded(lng)
59 }, [lng])
60
61 // 全局设置弹框状态
62 const { settingsVisible, settingsDefaultTab, closeSettings } = useSettingsModalStore()
63 const { open: configManagerOpen, closeDialog: closeConfigManagerDialog } = useConfigManagerDialogStore()
64
65 useEffect(() => {
66 if (!_hasHydrated) {
67 return
68 }
69
70 useUserStore.getState().appInit(autoLoginToken)
71 setAuthInitialized(true)
72 }, [_hasHydrated, autoLoginToken])
73
74 useEffect(() => {
75 useUserStore.getState().setLang(lng)
76 }, [lng])
77
78 // 未登录用户访问非公开页面时,跳转到登录页
79 useEffect(() => {
80 // 等待持久化数据同步完成
81 if (!_hasHydrated || !authInitialized) {
82 return
83 }
84
85 // 已登录用户不需要跳转
86 if (token) {
87 hasPromptedRef.current = false
88 return
89 }
90
91 // 公开页面不需要跳转
92 if (publicRoute) {
93 hasPromptedRef.current = false
94 return
95 }
96
97 // 避免重复跳转
98 if (hasPromptedRef.current) {
99 return
100 }
101
102 // 在当前页面弹出登录框,不跳转
103 hasPromptedRef.current = true
104 useLoginDialogStore.getState().openLoginDialog({ fromGuard: true })
105 }, [_hasHydrated, authInitialized, token, pathname, publicRoute])
106
107 // 拦截 @react-oauth/google 的脚本加载,添加 ?hl= 参数以设置按钮语言
108 useLayoutEffect(() => {
109 const hl = lng.replace('-', '_')
110 const GIS_URL = 'https://accounts.google.com/gsi/client'
111 const originalAppendChild = document.body.appendChild.bind(document.body)
112
113 document.body.appendChild = function <T extends Node>(node: T): T {
114 if (node instanceof HTMLScriptElement && node.src === GIS_URL) {
115 node.src = `${GIS_URL}?hl=${hl}`
116 }
117 return originalAppendChild(node)
118 }
119
120 return () => {
121 document.body.appendChild = originalAppendChild
122 }
123 }, [lng])
124
125 return (
126 <PublicRouteContext.Provider value={publicRoute}>
127 <ThemeProvider attribute="class" defaultTheme="light" enableSystem disableTransitionOnChange>
128 <GoogleOAuthProvider clientId="1094109734611-flskoscgp609mecqk9ablvc6i3205vqk.apps.googleusercontent.com">
129 <Toaster position="top-center" richColors />
130 {/* 专用右上角通知中心(不影响现有 toast) */}
131 <NotificationCenter />
132 <PluginPublishingFloatButton />
133 <WechatBrowserOverlay />
134 {/* 全局登录弹框 */}
135 <LoginDialog />
136 {/* 全局配置管理弹框 */}
137 <ConfigManagerDialog open={configManagerOpen} onClose={closeConfigManagerDialog} />
138 {/* 全局设置弹框 - 统一在此渲染,避免多处重复 */}
139 <SettingsModal
140 open={settingsVisible}
141 onClose={closeSettings}
142 defaultTab={settingsDefaultTab}
143 />
144 {children}
145 </GoogleOAuthProvider>
146 </ThemeProvider>
147 </PublicRouteContext.Provider>
148 )
149 }
150
150 lines Plain Text