create-pardx-scaffold 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/apps/api/libs/domain/auth/src/auth.service.ts +27 -1
- package/template/apps/api/libs/infra/clients/internal/email/dto/email.dto.ts +3 -3
- package/template/apps/api/libs/infra/clients/internal/volcengine-tts/dto/tts.dto.ts +4 -4
- package/template/apps/api/libs/infra/common/common.module.ts +10 -0
- package/template/apps/api/libs/infra/common/config/validation/env.validation.ts +1 -1
- package/template/apps/api/libs/infra/common/config/validation/keys.validation.ts +2 -2
- package/template/apps/api/libs/infra/common/config/validation/yaml.validation.ts +3 -3
- package/template/apps/api/libs/infra/common/decorators/device-info.decorator.ts +58 -0
- package/template/apps/api/libs/infra/common/decorators/team-info.decorator.ts +122 -0
- package/template/apps/api/libs/infra/common/encryption.service.ts +70 -0
- package/template/apps/api/libs/infra/common/index.ts +9 -0
- package/template/apps/api/libs/infra/shared-services/email/dto/email.dto.ts +3 -3
- package/template/apps/api/libs/infra/shared-services/email/email.service.ts +5 -1
- package/template/apps/api/libs/infra/shared-services/notification/index.ts +13 -0
- package/template/apps/api/libs/infra/shared-services/notification/notification.module.ts +10 -0
- package/template/apps/api/libs/infra/shared-services/notification/notification.service.ts +791 -0
- package/template/apps/web/components/client-only.tsx +28 -0
- package/template/apps/web/components/index.ts +23 -0
- package/template/apps/web/components/layout/app-navbar.tsx +109 -0
- package/template/apps/web/components/layout/app-shell.tsx +30 -0
- package/template/apps/web/components/layout/app-sidebar.tsx +206 -0
- package/template/apps/web/components/layout/index.ts +4 -0
- package/template/apps/web/components/layout/locale-switcher.tsx +57 -0
- package/template/apps/web/components/runtime-i18n-bridge.tsx +32 -0
- package/template/apps/web/components/state-components.tsx +214 -0
- package/template/apps/web/config.ts +22 -2
- package/template/apps/web/lib/api/cache-config.ts +32 -0
- package/template/apps/web/lib/api/contracts/client.ts +43 -1
- package/template/apps/web/lib/api/contracts/hooks/analytics.ts +32 -0
- package/template/apps/web/lib/api/contracts/hooks/index.ts +41 -2
- package/template/apps/web/lib/api/contracts/hooks/message.ts +60 -0
- package/template/apps/web/lib/api/contracts/hooks/system.ts +42 -0
- package/template/apps/web/lib/api/contracts/hooks/task.ts +54 -0
- package/template/apps/web/lib/api/contracts/hooks/user.ts +45 -0
- package/template/apps/web/lib/api/contracts/server-client.ts +1 -1
- package/template/apps/web/lib/api/prefetch.ts +128 -0
- package/template/apps/web/lib/api/query-client.ts +37 -0
- package/template/apps/web/lib/i18n/runtime-translator.ts +48 -0
- package/template/apps/web/lib/requests.ts +1 -1
- package/template/apps/web/providers/app-provider.tsx +1 -1
- package/template/apps/web/providers/auth-provider.tsx +228 -0
- package/template/apps/web/providers/index.tsx +28 -9
- package/template/apps/web/providers/intl-client-provider.tsx +43 -0
- package/template/apps/web/vitest.config.ts +4 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 数据预加载工具
|
|
5
|
+
* 用于在用户可能需要数据之前预先加载,提升用户体验
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useEffect, useRef, useCallback, useMemo } from 'react';
|
|
9
|
+
import { usePathname } from '@/i18n/navigation';
|
|
10
|
+
import { queryClient } from './query-client';
|
|
11
|
+
import { cacheTime } from './cache-config';
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// 单项预加载函数(示例)
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 预加载用户信息
|
|
19
|
+
* 在应用初始化或登录后调用
|
|
20
|
+
*/
|
|
21
|
+
export async function prefetchUserInfo() {
|
|
22
|
+
// Implement based on your API contracts
|
|
23
|
+
// Example:
|
|
24
|
+
// return queryClient.prefetchQuery({
|
|
25
|
+
// queryKey: ['user', 'info'],
|
|
26
|
+
// queryFn: async () => {
|
|
27
|
+
// return userClient.getInfo({});
|
|
28
|
+
// },
|
|
29
|
+
// staleTime: cacheTime.short,
|
|
30
|
+
// });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 预加载仪表盘数据
|
|
35
|
+
* 在登录成功后或应用初始化时调用
|
|
36
|
+
*/
|
|
37
|
+
export async function prefetchDashboardData() {
|
|
38
|
+
await Promise.all([
|
|
39
|
+
// prefetchUserInfo(),
|
|
40
|
+
// Add other prefetch functions as needed
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// 预加载 Hooks
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 获取预加载函数的 Hook
|
|
50
|
+
* 返回可以在事件(如 hover)中调用的预加载函数
|
|
51
|
+
*/
|
|
52
|
+
export function usePrefetch() {
|
|
53
|
+
const createHoverHandler = useCallback(
|
|
54
|
+
(prefetchFn: () => Promise<unknown>) => {
|
|
55
|
+
let prefetched = false;
|
|
56
|
+
return () => {
|
|
57
|
+
if (!prefetched) {
|
|
58
|
+
prefetched = true;
|
|
59
|
+
prefetchFn();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
[],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
createHoverHandler,
|
|
68
|
+
prefetchUserInfo,
|
|
69
|
+
prefetchDashboardData,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 导航项预加载映射
|
|
75
|
+
* 根据路由路径返回对应的预加载函数
|
|
76
|
+
*/
|
|
77
|
+
export function useNavPrefetch() {
|
|
78
|
+
const { createHoverHandler } = usePrefetch();
|
|
79
|
+
|
|
80
|
+
const navPrefetchMap = useMemo(
|
|
81
|
+
() => ({
|
|
82
|
+
'/dashboard': createHoverHandler(prefetchDashboardData),
|
|
83
|
+
// Add more route prefetch mappings as needed
|
|
84
|
+
}),
|
|
85
|
+
[createHoverHandler],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return navPrefetchMap;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 路由预加载映射
|
|
93
|
+
* 根据当前路由路径预加载对应页面数据
|
|
94
|
+
*/
|
|
95
|
+
const routePrefetchMap: Record<string, () => Promise<unknown>> = {
|
|
96
|
+
'/dashboard': prefetchDashboardData,
|
|
97
|
+
// Add more route prefetch mappings as needed
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 路由变化时自动预加载 Hook
|
|
102
|
+
* 在组件中使用,当路由变化时自动预加载目标页面数据
|
|
103
|
+
*/
|
|
104
|
+
export function useRoutePrefetch() {
|
|
105
|
+
const pathname = usePathname();
|
|
106
|
+
const previousPathname = useRef<string | null>(null);
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
const currentPath = pathname || '/';
|
|
110
|
+
|
|
111
|
+
if (previousPathname.current === currentPath) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
previousPathname.current = currentPath;
|
|
116
|
+
|
|
117
|
+
const prefetchFn = routePrefetchMap[currentPath];
|
|
118
|
+
|
|
119
|
+
if (prefetchFn) {
|
|
120
|
+
prefetchFn().catch((error) => {
|
|
121
|
+
console.warn(
|
|
122
|
+
`Failed to prefetch data for route ${currentPath}:`,
|
|
123
|
+
error,
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}, [pathname]);
|
|
128
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Query Client 单例
|
|
5
|
+
* 用于在 hooks 外部访问 queryClient(如预加载)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
9
|
+
import { cacheTime, gcTime } from './cache-config';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 创建 QueryClient 实例
|
|
13
|
+
*/
|
|
14
|
+
function createQueryClient() {
|
|
15
|
+
return new QueryClient({
|
|
16
|
+
defaultOptions: {
|
|
17
|
+
queries: {
|
|
18
|
+
staleTime: cacheTime.medium,
|
|
19
|
+
gcTime: gcTime.medium,
|
|
20
|
+
retry: 1,
|
|
21
|
+
refetchOnWindowFocus: false,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 单例实例
|
|
28
|
+
let queryClientInstance: QueryClient | undefined;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 获取 QueryClient 实例
|
|
32
|
+
* 在组件外使用(如预加载)
|
|
33
|
+
*/
|
|
34
|
+
export const queryClient =
|
|
35
|
+
typeof window !== 'undefined'
|
|
36
|
+
? (queryClientInstance ??= createQueryClient())
|
|
37
|
+
: createQueryClient();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 运行时翻译注册表:供 fetch 层、非 React 模块在客户端使用 next-intl 文案。
|
|
3
|
+
* 由 RuntimeI18nBridge 在挂载时注入各 namespace 的 t 函数。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type NamespaceTranslate = (
|
|
7
|
+
key: string,
|
|
8
|
+
values?: Record<string, string | number>,
|
|
9
|
+
) => string;
|
|
10
|
+
|
|
11
|
+
const registry = new Map<string, NamespaceTranslate>();
|
|
12
|
+
|
|
13
|
+
export function registerIntlNamespace(
|
|
14
|
+
namespace: string,
|
|
15
|
+
translate: NamespaceTranslate | null,
|
|
16
|
+
): void {
|
|
17
|
+
if (translate) {
|
|
18
|
+
registry.set(namespace, translate);
|
|
19
|
+
} else {
|
|
20
|
+
registry.delete(namespace);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function intlNs(
|
|
25
|
+
namespace: string,
|
|
26
|
+
key: string,
|
|
27
|
+
values?: Record<string, string | number>,
|
|
28
|
+
): string {
|
|
29
|
+
const t = registry.get(namespace);
|
|
30
|
+
if (!t) {
|
|
31
|
+
return key;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return values && Object.keys(values).length > 0 ? t(key, values) : t(key);
|
|
35
|
+
} catch {
|
|
36
|
+
return key;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 带 locale 前缀的登录路径,供 401 跳转 */
|
|
41
|
+
export function getLocaleLoginPath(): string {
|
|
42
|
+
if (typeof window === 'undefined') {
|
|
43
|
+
return '/login';
|
|
44
|
+
}
|
|
45
|
+
const pathname = window.location.pathname;
|
|
46
|
+
const m = pathname.match(/^\/(en|zh-CN)(?=\/|$)/);
|
|
47
|
+
return m ? `${m[0]}/login` : '/login';
|
|
48
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Auth Context Provider
|
|
5
|
+
*
|
|
6
|
+
* Provides authentication state and methods throughout the application.
|
|
7
|
+
* Manages user info, tokens, login/logout functionality.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
createContext,
|
|
11
|
+
useContext,
|
|
12
|
+
useState,
|
|
13
|
+
useEffect,
|
|
14
|
+
useCallback,
|
|
15
|
+
type ReactNode,
|
|
16
|
+
} from 'react';
|
|
17
|
+
import { useRouter } from '@/i18n/navigation';
|
|
18
|
+
import type { UserInfo, LoginSuccess } from '@repo/contracts';
|
|
19
|
+
import {
|
|
20
|
+
getUser,
|
|
21
|
+
setUser,
|
|
22
|
+
getTokens,
|
|
23
|
+
setTokens,
|
|
24
|
+
clearAll,
|
|
25
|
+
isTokenExpired,
|
|
26
|
+
} from '@/lib/storage';
|
|
27
|
+
import { signClient } from '@/lib/api/contracts/client';
|
|
28
|
+
import { prefetchDashboardData } from '@/lib/api/prefetch';
|
|
29
|
+
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// Type Definitions
|
|
32
|
+
// ============================================================================
|
|
33
|
+
|
|
34
|
+
export interface AuthContextType {
|
|
35
|
+
// State
|
|
36
|
+
user: UserInfo | null;
|
|
37
|
+
isLoading: boolean;
|
|
38
|
+
isAuthenticated: boolean;
|
|
39
|
+
isAdmin: boolean;
|
|
40
|
+
|
|
41
|
+
// Actions
|
|
42
|
+
login: (data: LoginSuccess) => void;
|
|
43
|
+
logout: () => Promise<void>;
|
|
44
|
+
refreshUser: () => Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// Context Creation
|
|
49
|
+
// ============================================================================
|
|
50
|
+
|
|
51
|
+
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
52
|
+
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Provider Component
|
|
55
|
+
// ============================================================================
|
|
56
|
+
|
|
57
|
+
interface AuthProviderProps {
|
|
58
|
+
children: ReactNode;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function AuthProvider({ children }: AuthProviderProps) {
|
|
62
|
+
const [user, setUserState] = useState<UserInfo | null>(null);
|
|
63
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
64
|
+
const router = useRouter();
|
|
65
|
+
|
|
66
|
+
// Initialize user from localStorage on mount and prefetch dashboard data
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const storedUser = getUser();
|
|
69
|
+
if (storedUser) {
|
|
70
|
+
setUserState(storedUser);
|
|
71
|
+
// Prefetch dashboard data for returning authenticated users
|
|
72
|
+
prefetchDashboardData().catch((error) => {
|
|
73
|
+
console.warn('Failed to prefetch dashboard data on init:', error);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
setIsLoading(false);
|
|
77
|
+
|
|
78
|
+
// Listen for user info updates from other components
|
|
79
|
+
const handleUserUpdate = () => {
|
|
80
|
+
const updatedUser = getUser();
|
|
81
|
+
setUserState(updatedUser);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
window.addEventListener('userInfoUpdated', handleUserUpdate);
|
|
85
|
+
return () => {
|
|
86
|
+
window.removeEventListener('userInfoUpdated', handleUserUpdate);
|
|
87
|
+
};
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
// Login: store user and tokens, then prefetch dashboard data
|
|
91
|
+
const login = useCallback((data: LoginSuccess) => {
|
|
92
|
+
// Store user (merges with existing data if any)
|
|
93
|
+
setUser(data.user);
|
|
94
|
+
setTokens({
|
|
95
|
+
access: data.access,
|
|
96
|
+
refresh: data.refresh,
|
|
97
|
+
accessExpire: data.accessExpire,
|
|
98
|
+
expire: data.expire,
|
|
99
|
+
});
|
|
100
|
+
// Get the merged user from storage for state
|
|
101
|
+
const mergedUser = getUser();
|
|
102
|
+
setUserState(mergedUser);
|
|
103
|
+
// Prefetch dashboard data after successful login
|
|
104
|
+
prefetchDashboardData().catch((error) => {
|
|
105
|
+
console.warn('Failed to prefetch dashboard data:', error);
|
|
106
|
+
});
|
|
107
|
+
}, []);
|
|
108
|
+
|
|
109
|
+
// Logout: clear storage and redirect
|
|
110
|
+
const logout = useCallback(async () => {
|
|
111
|
+
try {
|
|
112
|
+
// Call sign out endpoint
|
|
113
|
+
await signClient.signOut({
|
|
114
|
+
body: {},
|
|
115
|
+
});
|
|
116
|
+
} catch (error) {
|
|
117
|
+
// Ignore errors during logout
|
|
118
|
+
console.warn('Sign out API call failed:', error);
|
|
119
|
+
} finally {
|
|
120
|
+
// Clear local storage
|
|
121
|
+
clearAll();
|
|
122
|
+
setUserState(null);
|
|
123
|
+
// Redirect to login page
|
|
124
|
+
router.push('/login');
|
|
125
|
+
}
|
|
126
|
+
}, [router]);
|
|
127
|
+
|
|
128
|
+
// Refresh user info from server
|
|
129
|
+
const refreshUser = useCallback(async () => {
|
|
130
|
+
const tokens = getTokens();
|
|
131
|
+
if (!tokens?.refresh) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const response = await signClient.refreshToken({
|
|
137
|
+
query: { refresh: tokens.refresh },
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (response.status === 200 && response.body.data) {
|
|
141
|
+
const data = response.body.data;
|
|
142
|
+
// Store user (merges with existing data)
|
|
143
|
+
setUser(data.user);
|
|
144
|
+
setTokens({
|
|
145
|
+
access: data.access,
|
|
146
|
+
refresh: data.refresh,
|
|
147
|
+
accessExpire: data.accessExpire,
|
|
148
|
+
expire: data.expire,
|
|
149
|
+
});
|
|
150
|
+
// Get the merged user from storage for state
|
|
151
|
+
const mergedUser = getUser();
|
|
152
|
+
setUserState(mergedUser);
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error('Failed to refresh user:', error);
|
|
156
|
+
}
|
|
157
|
+
}, []);
|
|
158
|
+
|
|
159
|
+
// Auto-refresh token before expiry
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (!user) return;
|
|
162
|
+
|
|
163
|
+
const checkAndRefresh = () => {
|
|
164
|
+
if (isTokenExpired()) {
|
|
165
|
+
refreshUser();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const interval = setInterval(checkAndRefresh, 60000);
|
|
170
|
+
return () => clearInterval(interval);
|
|
171
|
+
}, [user, refreshUser]);
|
|
172
|
+
|
|
173
|
+
const value: AuthContextType = {
|
|
174
|
+
user,
|
|
175
|
+
isLoading,
|
|
176
|
+
isAuthenticated: !!user && !isTokenExpired(),
|
|
177
|
+
isAdmin: user?.isAdmin ?? false,
|
|
178
|
+
login,
|
|
179
|
+
logout,
|
|
180
|
+
refreshUser,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ============================================================================
|
|
187
|
+
// Hook
|
|
188
|
+
// ============================================================================
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Hook to access auth context
|
|
192
|
+
* @throws Error if used outside AuthProvider
|
|
193
|
+
*/
|
|
194
|
+
export function useAuth(): AuthContextType {
|
|
195
|
+
const context = useContext(AuthContext);
|
|
196
|
+
if (context === undefined) {
|
|
197
|
+
throw new Error('useAuth must be used within an AuthProvider');
|
|
198
|
+
}
|
|
199
|
+
return context;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ============================================================================
|
|
203
|
+
// Utility Hooks
|
|
204
|
+
// ============================================================================
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Hook to check if user is authenticated
|
|
208
|
+
*/
|
|
209
|
+
export function useIsAuthenticated(): boolean {
|
|
210
|
+
const { isAuthenticated } = useAuth();
|
|
211
|
+
return isAuthenticated;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Hook to get current user ID
|
|
216
|
+
*/
|
|
217
|
+
export function useUserId(): string | null {
|
|
218
|
+
const { user } = useAuth();
|
|
219
|
+
return user?.id ?? null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Hook to check if current user is admin
|
|
224
|
+
*/
|
|
225
|
+
export function useIsAdmin(): boolean {
|
|
226
|
+
const { isAdmin } = useAuth();
|
|
227
|
+
return isAdmin;
|
|
228
|
+
}
|
|
@@ -11,35 +11,54 @@ import NextTopLoader from 'nextjs-toploader';
|
|
|
11
11
|
import { QueryProvider } from './query-provider';
|
|
12
12
|
import { ThemeProvider } from './theme-provider';
|
|
13
13
|
import { AppProvider } from './app-provider';
|
|
14
|
+
import { AuthProvider } from './auth-provider';
|
|
15
|
+
import { RuntimeI18nBridge } from '@/components/runtime-i18n-bridge';
|
|
14
16
|
|
|
15
17
|
export { QueryProvider } from './query-provider';
|
|
16
18
|
export { ThemeProvider } from './theme-provider';
|
|
17
19
|
export { AppProvider, useApp } from './app-provider';
|
|
20
|
+
export {
|
|
21
|
+
AuthProvider,
|
|
22
|
+
useAuth,
|
|
23
|
+
useIsAuthenticated,
|
|
24
|
+
useUserId,
|
|
25
|
+
useIsAdmin,
|
|
26
|
+
} from './auth-provider';
|
|
27
|
+
export { IntlClientProvider } from './intl-client-provider';
|
|
18
28
|
|
|
19
29
|
interface ProvidersProps {
|
|
20
30
|
children: ReactNode;
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
/**
|
|
24
|
-
* Root
|
|
34
|
+
* Root providers Component
|
|
25
35
|
* 根 Providers 组件
|
|
26
36
|
*
|
|
27
37
|
* Combines all providers in the correct order:
|
|
28
|
-
* 1. AppProvider - Application context
|
|
29
|
-
* 2. ThemeProvider - Theme management
|
|
30
|
-
* 3.
|
|
31
|
-
* 4.
|
|
38
|
+
* 1. AppProvider - Application context (brand, version)
|
|
39
|
+
* 2. ThemeProvider - Theme management (dark/light mode)
|
|
40
|
+
* 3. AuthProvider - Authentication state and methods
|
|
41
|
+
* 4. QueryProvider - React Query
|
|
42
|
+
* 5. UI components (Toaster, TopLoader)
|
|
32
43
|
*/
|
|
33
44
|
export function Providers({ children }: ProvidersProps) {
|
|
34
45
|
return (
|
|
35
46
|
<AppProvider>
|
|
36
47
|
<ThemeProvider>
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
<AuthProvider>
|
|
49
|
+
<NextTopLoader color="#000" showSpinner={false} height={2} />
|
|
50
|
+
<Toaster
|
|
51
|
+
position="top-center"
|
|
52
|
+
richColors
|
|
53
|
+
closeButton
|
|
54
|
+
duration={4000}
|
|
55
|
+
/>
|
|
56
|
+
<RuntimeI18nBridge />
|
|
57
|
+
<QueryProvider>{children}</QueryProvider>
|
|
58
|
+
</AuthProvider>
|
|
40
59
|
</ThemeProvider>
|
|
41
60
|
</AppProvider>
|
|
42
61
|
);
|
|
43
62
|
}
|
|
44
63
|
|
|
45
|
-
export default Providers;
|
|
64
|
+
export default Providers;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import type { AbstractIntlMessages } from 'next-intl';
|
|
5
|
+
import { NextIntlClientProvider, IntlErrorCode } from 'next-intl';
|
|
6
|
+
import type { Locale } from '@/i18n/config';
|
|
7
|
+
|
|
8
|
+
interface IntlClientProviderProps {
|
|
9
|
+
locale: Locale;
|
|
10
|
+
messages: AbstractIntlMessages;
|
|
11
|
+
/** Set from server layout using NODE_ENV so the client bundle avoids env reads. */
|
|
12
|
+
enableMissingMessageLogging?: boolean;
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Wraps next-intl client provider. When enableMissingMessageLogging is true,
|
|
18
|
+
* logs missing message keys to the console to catch JSON drift early.
|
|
19
|
+
*/
|
|
20
|
+
export function IntlClientProvider({
|
|
21
|
+
locale,
|
|
22
|
+
messages,
|
|
23
|
+
enableMissingMessageLogging = false,
|
|
24
|
+
children,
|
|
25
|
+
}: IntlClientProviderProps) {
|
|
26
|
+
return (
|
|
27
|
+
<NextIntlClientProvider
|
|
28
|
+
locale={locale}
|
|
29
|
+
messages={messages}
|
|
30
|
+
onError={
|
|
31
|
+
enableMissingMessageLogging
|
|
32
|
+
? (error) => {
|
|
33
|
+
if (error.code === IntlErrorCode.MISSING_MESSAGE) {
|
|
34
|
+
console.warn('[next-intl] Missing message:', error.message);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
: undefined
|
|
38
|
+
}
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
</NextIntlClientProvider>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
* Vitest Configuration for Next.js Web Application
|
|
3
3
|
*
|
|
4
4
|
* @see https://vitest.dev/config/
|
|
5
|
+
*
|
|
6
|
+
* Note: Vitest 4.x uses Vite 7 internally while @vitejs/plugin-react expects
|
|
7
|
+
* Vite 6 types. This type mismatch is known and doesn't affect functionality.
|
|
5
8
|
*/
|
|
6
9
|
import { defineConfig } from 'vitest/config';
|
|
7
10
|
import react from '@vitejs/plugin-react';
|
|
8
11
|
import path from 'path';
|
|
9
12
|
|
|
10
13
|
export default defineConfig({
|
|
14
|
+
// @ts-expect-error Vitest 4.x/Vite 7 type mismatch with @vitejs/plugin-react
|
|
11
15
|
plugins: [react()],
|
|
12
16
|
test: {
|
|
13
17
|
// Test environment
|