tr]:last:border-b-0",
45 | className
46 | )}
47 | {...props}
48 | />
49 | )
50 | }
51 |
52 | function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
53 | return (
54 |
62 | )
63 | }
64 |
65 | function TableHead({ className, ...props }: React.ComponentProps<"th">) {
66 | return (
67 | [role=checkbox]]:translate-y-[2px]",
71 | className
72 | )}
73 | {...props}
74 | />
75 | )
76 | }
77 |
78 | function TableCell({ className, ...props }: React.ComponentProps<"td">) {
79 | return (
80 | | [role=checkbox]]:translate-y-[2px]",
84 | className
85 | )}
86 | {...props}
87 | />
88 | )
89 | }
90 |
91 | function TableCaption({
92 | className,
93 | ...props
94 | }: React.ComponentProps<"caption">) {
95 | return (
96 |
101 | )
102 | }
103 |
104 | export {
105 | Table,
106 | TableHeader,
107 | TableBody,
108 | TableFooter,
109 | TableHead,
110 | TableRow,
111 | TableCell,
112 | TableCaption,
113 | }
114 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TabsPrimitive from "@radix-ui/react-tabs"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | function Tabs({
7 | className,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
16 | )
17 | }
18 |
19 | function TabsList({
20 | className,
21 | ...props
22 | }: React.ComponentProps) {
23 | return (
24 |
32 | )
33 | }
34 |
35 | function TabsTrigger({
36 | className,
37 | ...props
38 | }: React.ComponentProps) {
39 | return (
40 |
48 | )
49 | }
50 |
51 | function TabsContent({
52 | className,
53 | ...props
54 | }: React.ComponentProps) {
55 | return (
56 |
61 | )
62 | }
63 |
64 | export { Tabs, TabsList, TabsTrigger, TabsContent }
65 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6 | return (
7 |
15 | )
16 | }
17 |
18 | export { Textarea }
19 |
--------------------------------------------------------------------------------
/src/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
3 | import { type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 | import { toggleVariants } from "@/components/ui/toggle"
7 |
8 | const ToggleGroupContext = React.createContext<
9 | VariantProps
10 | >({
11 | size: "default",
12 | variant: "default",
13 | })
14 |
15 | function ToggleGroup({
16 | className,
17 | variant,
18 | size,
19 | children,
20 | ...props
21 | }: React.ComponentProps &
22 | VariantProps) {
23 | return (
24 |
34 |
35 | {children}
36 |
37 |
38 | )
39 | }
40 |
41 | function ToggleGroupItem({
42 | className,
43 | children,
44 | variant,
45 | size,
46 | ...props
47 | }: React.ComponentProps &
48 | VariantProps) {
49 | const context = React.useContext(ToggleGroupContext)
50 |
51 | return (
52 |
66 | {children}
67 |
68 | )
69 | }
70 |
71 | export { ToggleGroup, ToggleGroupItem }
72 |
--------------------------------------------------------------------------------
/src/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TogglePrimitive from "@radix-ui/react-toggle"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const toggleVariants = cva(
10 | "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
11 | {
12 | variants: {
13 | variant: {
14 | default: "bg-transparent",
15 | outline:
16 | "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
17 | },
18 | size: {
19 | default: "h-9 px-2 min-w-9",
20 | sm: "h-8 px-1.5 min-w-8",
21 | lg: "h-10 px-2.5 min-w-10",
22 | },
23 | },
24 | defaultVariants: {
25 | variant: "default",
26 | size: "default",
27 | },
28 | }
29 | )
30 |
31 | function Toggle({
32 | className,
33 | variant,
34 | size,
35 | ...props
36 | }: React.ComponentProps &
37 | VariantProps) {
38 | return (
39 |
44 | )
45 | }
46 |
47 | export { Toggle, toggleVariants }
48 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function TooltipProvider({
9 | delayDuration = 0,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
18 | )
19 | }
20 |
21 | function Tooltip({
22 | ...props
23 | }: React.ComponentProps) {
24 | return (
25 |
26 |
27 |
28 | )
29 | }
30 |
31 | function TooltipTrigger({
32 | ...props
33 | }: React.ComponentProps) {
34 | return
35 | }
36 |
37 | function TooltipContent({
38 | className,
39 | sideOffset = 4,
40 | children,
41 | ...props
42 | }: React.ComponentProps) {
43 | return (
44 |
45 |
54 | {children}
55 |
56 |
57 |
58 | )
59 | }
60 |
61 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
62 |
--------------------------------------------------------------------------------
/src/config/api.config.ts:
--------------------------------------------------------------------------------
1 | // src/config/api.config.ts
2 | // API configuration constants
3 | export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://api.example.com';
4 | export const API_TIMEOUT = 30000; // 30 seconds
5 | export const API_VERSION = 'v1';
6 | export const API_HEADERS = {
7 | 'Content-Type': 'application/json',
8 | 'Accept': 'application/json',
9 | };
10 |
11 | // API endpoints
12 | export const API_ENDPOINTS = {
13 | AUTH: {
14 | LOGIN: '/auth/login',
15 | REGISTER: '/auth/register',
16 | LOGOUT: '/auth/logout',
17 | REFRESH_TOKEN: '/auth/refresh',
18 | FORGOT_PASSWORD: '/auth/forgot-password',
19 | RESET_PASSWORD: '/auth/reset-password',
20 | VERIFY_EMAIL: '/auth/verify-email',
21 | ME: '/auth/me',
22 | },
23 | USERS: {
24 | BASE: '/users',
25 | PROFILE: (id: string) => `/users/${id}`,
26 | },
27 | SETTINGS: {
28 | BASE: '/settings',
29 | },
30 | };
31 |
32 | // Request retry configuration
33 | export const RETRY_CONFIG = {
34 | MAX_RETRIES: 3,
35 | RETRY_DELAY: 1000, // 1 second
36 | };
37 |
38 | // Cache configuration
39 | export const CACHE_CONFIG = {
40 | ENABLED: true,
41 | TTL: 60 * 5, // 5 minutes
42 | };
43 |
44 | // Rate limiting
45 | export const RATE_LIMIT_CONFIG = {
46 | MAX_REQUESTS_PER_MINUTE: 60,
47 | };
--------------------------------------------------------------------------------
/src/config/routes.config.ts:
--------------------------------------------------------------------------------
1 | // src/config/routes.config.ts
2 | import { lazy, ComponentType } from 'react';
3 | import { RouteConfig } from '../types/common.types';
4 |
5 | // 使用加权预加载策略
6 | interface PreloadableComponent {
7 | component: Promise;
8 | preloadWeight: number; // 1-10 数值越高优先级越高
9 | preloadStatus: 'none' | 'pending' | 'loaded';
10 | }
11 |
12 | // 组件缓存
13 | const componentCache: Record = {
14 | // 主要路由组件
15 | 'Dashboard': {
16 | component: import('../pages/Dashboard'),
17 | preloadWeight: 10,
18 | preloadStatus: 'none'
19 | },
20 | 'Login': {
21 | component: import('../pages/auth/Login'),
22 | preloadWeight: 9,
23 | preloadStatus: 'none'
24 | },
25 | 'Register': {
26 | component: import('../pages/auth/Register'),
27 | preloadWeight: 5,
28 | preloadStatus: 'none'
29 | },
30 | 'ForgotPassword': {
31 | component: import('../pages/auth/ForgotPassword'),
32 | preloadWeight: 4,
33 | preloadStatus: 'none'
34 | },
35 | 'Profile': {
36 | component: import('../pages/Profile'),
37 | preloadWeight: 7,
38 | preloadStatus: 'none'
39 | },
40 | 'Settings': {
41 | component: import('../pages/Settings'),
42 | preloadWeight: 6,
43 | preloadStatus: 'none'
44 | },
45 | 'NotFound': {
46 | component: import('../pages/NotFound'),
47 | preloadWeight: 3,
48 | preloadStatus: 'none'
49 | }
50 | };
51 |
52 | // 优化的懒加载函数
53 | function optimizedLazy(key: string): ComponentType {
54 | return lazy(() => {
55 | componentCache[key].preloadStatus = 'pending';
56 | return componentCache[key].component.then(module => {
57 | componentCache[key].preloadStatus = 'loaded';
58 | return module;
59 | });
60 | });
61 | }
62 |
63 | // 懒加载组件
64 | const Dashboard = optimizedLazy('Dashboard');
65 | const Profile = optimizedLazy('Profile');
66 | const Settings = optimizedLazy('Settings');
67 | const NotFound = optimizedLazy('NotFound');
68 |
69 | // 认证页面
70 | const Login = optimizedLazy('Login');
71 | const Register = optimizedLazy('Register');
72 | const ForgotPassword = optimizedLazy('ForgotPassword');
73 |
74 | // 路由守卫函数
75 | const requireAuth = (isAuthenticated: boolean) => isAuthenticated;
76 | const requireNoAuth = (isAuthenticated: boolean) => !isAuthenticated;
77 |
78 | // 智能预加载函数
79 | export const preloadRoute = (route: string) => {
80 | // 已经加载过的路由不再重复加载
81 | const preloadKey = getPreloadKeyFromRoute(route);
82 | if (preloadKey && componentCache[preloadKey] && componentCache[preloadKey].preloadStatus === 'none') {
83 | componentCache[preloadKey].preloadStatus = 'pending';
84 | componentCache[preloadKey].component.then(() => {
85 | componentCache[preloadKey].preloadStatus = 'loaded';
86 | });
87 | }
88 | };
89 |
90 | // 获取路由对应的预加载键
91 | function getPreloadKeyFromRoute(route: string): string | null {
92 | switch (route) {
93 | case '/': return 'Dashboard';
94 | case '/login': return 'Login';
95 | case '/register': return 'Register';
96 | case '/forgot-password': return 'ForgotPassword';
97 | case '/profile': return 'Profile';
98 | case '/settings': return 'Settings';
99 | default: return null;
100 | }
101 | }
102 |
103 | // 初始化时预加载高优先级组件
104 | export const initPreload = () => {
105 | // 按优先级排序
106 | const sortedComponents = Object.entries(componentCache)
107 | .sort((a, b) => b[1].preloadWeight - a[1].preloadWeight)
108 | .slice(0, 2); // 只预加载最高优先级的2个组件
109 |
110 | // 延迟预加载,不阻塞初始渲染
111 | setTimeout(() => {
112 | sortedComponents.forEach(([key, config]) => {
113 | if (config.preloadStatus === 'none') {
114 | config.preloadStatus = 'pending';
115 | config.component.then(() => {
116 | config.preloadStatus = 'loaded';
117 | });
118 | }
119 | });
120 | }, 2000);
121 | };
122 |
123 | export const routes: RouteConfig[] = [
124 | {
125 | path: '/',
126 | element: Dashboard,
127 | guards: [requireAuth],
128 | meta: {
129 | title: 'Dashboard',
130 | auth: true,
131 | layout: 'default'
132 | },
133 | redirectTo: '/login'
134 | },
135 | {
136 | path: '/login',
137 | element: Login,
138 | guards: [requireNoAuth],
139 | meta: {
140 | title: 'Login',
141 | auth: false,
142 | },
143 | redirectTo: '/'
144 | },
145 | {
146 | path: '/register',
147 | element: Register,
148 | guards: [requireNoAuth],
149 | meta: {
150 | title: 'Register',
151 | auth: false,
152 | },
153 | redirectTo: '/'
154 | },
155 | {
156 | path: '/forgot-password',
157 | element: ForgotPassword,
158 | guards: [requireNoAuth],
159 | meta: {
160 | title: 'Forgot Password',
161 | auth: false,
162 | },
163 | redirectTo: '/'
164 | },
165 | {
166 | path: '/profile',
167 | element: Profile,
168 | guards: [requireAuth],
169 | meta: {
170 | title: 'Profile',
171 | auth: true,
172 | layout: 'default'
173 | },
174 | redirectTo: '/login'
175 | },
176 | {
177 | path: '/settings',
178 | element: Settings,
179 | guards: [requireAuth],
180 | meta: {
181 | title: 'Settings',
182 | auth: true,
183 | layout: 'default'
184 | },
185 | redirectTo: '/login'
186 | },
187 | {
188 | path: '*',
189 | element: NotFound,
190 | meta: {
191 | title: 'Not Found',
192 | auth: false,
193 | },
194 | },
195 | ];
196 |
197 | // 获取路由配置
198 | export const getRouteByPath = (path: string): RouteConfig | undefined => {
199 | return findRoute(routes, path);
200 | };
201 |
202 | const findRoute = (routes: RouteConfig[], path: string): RouteConfig | undefined => {
203 | for (const route of routes) {
204 | if (route.path === path) {
205 | return route;
206 | }
207 | if (route.children) {
208 | const childRoute = findRoute(route.children, path);
209 | if (childRoute) {
210 | return childRoute;
211 | }
212 | }
213 | }
214 | return undefined;
215 | };
--------------------------------------------------------------------------------
/src/hooks/use-mobile.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/src/hooks/useApi.ts:
--------------------------------------------------------------------------------
1 | // src/hooks/useApi.ts
2 | import { useState, useCallback } from 'react';
3 | import { ApiService, ApiResponse, RequestConfig } from '@/services/ApiService';
4 |
5 | interface UseApiOptions {
6 | onSuccess?: (data: T) => void;
7 | onError?: (error: Error) => void;
8 | }
9 |
10 | /**
11 | * Hook for making API requests with loading and error states
12 | */
13 | export const useApi = (options?: UseApiOptions) => {
14 | const [isLoading, setIsLoading] = useState(false);
15 | const [error, setError] = useState(null);
16 | const [data, setData] = useState(undefined);
17 |
18 | const get = useCallback(
19 | async (url: string, config?: RequestConfig): Promise => {
20 | setIsLoading(true);
21 | setError(null);
22 |
23 | try {
24 | const response = await ApiService.get(url, config);
25 | setData(response.data);
26 | options?.onSuccess?.(response.data);
27 | return response.data;
28 | } catch (err: any) {
29 | setError(err);
30 | options?.onError?.(err);
31 | throw err;
32 | } finally {
33 | setIsLoading(false);
34 | }
35 | },
36 | [options]
37 | );
38 |
39 | const post = useCallback(
40 | async (url: string, postData?: any, config?: RequestConfig): Promise => {
41 | setIsLoading(true);
42 | setError(null);
43 |
44 | try {
45 | const response = await ApiService.post(url, postData, config);
46 | setData(response.data);
47 | options?.onSuccess?.(response.data);
48 | return response.data;
49 | } catch (err: any) {
50 | setError(err);
51 | options?.onError?.(err);
52 | throw err;
53 | } finally {
54 | setIsLoading(false);
55 | }
56 | },
57 | [options]
58 | );
59 |
60 | const put = useCallback(
61 | async (url: string, putData?: any, config?: RequestConfig): Promise => {
62 | setIsLoading(true);
63 | setError(null);
64 |
65 | try {
66 | const response = await ApiService.put(url, putData, config);
67 | setData(response.data);
68 | options?.onSuccess?.(response.data);
69 | return response.data;
70 | } catch (err: any) {
71 | setError(err);
72 | options?.onError?.(err);
73 | throw err;
74 | } finally {
75 | setIsLoading(false);
76 | }
77 | },
78 | [options]
79 | );
80 |
81 | const del = useCallback(
82 | async (url: string, config?: RequestConfig): Promise => {
83 | setIsLoading(true);
84 | setError(null);
85 |
86 | try {
87 | const response = await ApiService.delete(url, config);
88 | setData(response.data);
89 | options?.onSuccess?.(response.data);
90 | return response.data;
91 | } catch (err: any) {
92 | setError(err);
93 | options?.onError?.(err);
94 | throw err;
95 | } finally {
96 | setIsLoading(false);
97 | }
98 | },
99 | [options]
100 | );
101 |
102 | const cancelRequest = useCallback((cancelId: string) => {
103 | ApiService.cancelRequest(cancelId);
104 | }, []);
105 |
106 | return {
107 | get,
108 | post,
109 | put,
110 | delete: del, // 'delete' is a reserved word in JS, so we use 'del' internally
111 | cancelRequest,
112 | isLoading,
113 | error,
114 | data,
115 | };
116 | };
--------------------------------------------------------------------------------
/src/hooks/useAuth.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { AuthContext } from '../providers/AuthProvider';
3 |
4 | /**
5 | * Custom hook to access the authentication context
6 | * @returns Authentication context values and methods
7 | */
8 | export const useAuth = () => {
9 | const context = useContext(AuthContext);
10 |
11 | if (context === null) {
12 | throw new Error('useAuth must be used within an AuthProvider');
13 | }
14 |
15 | return context;
16 | };
--------------------------------------------------------------------------------
/src/hooks/useCodeSplitting.ts:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, ComponentType, ReactNode } from 'react';
2 |
3 | interface SplitComponentProps {
4 | fallback?: React.ReactNode;
5 | load: () => Promise<{ default: ComponentType }>;
6 | props?: Record;
7 | }
8 |
9 | /**
10 | * 用于异步加载组件的自定义钩子
11 | * @param componentPromise 组件导入的 Promise
12 | * @param options 选项配置
13 | * @returns [组件, 加载状态, 错误]
14 | */
15 | export function useCodeSplitting>(
16 | componentPromise: () => Promise<{ default: T }>,
17 | options: { preload?: boolean; delay?: number } = {}
18 | ): [React.ComponentType | null, boolean, Error | null] {
19 | const [Component, setComponent] = useState(null);
20 | const [loading, setLoading] = useState(true);
21 | const [error, setError] = useState(null);
22 |
23 | useEffect(() => {
24 | let mounted = true;
25 | let timer: NodeJS.Timeout | null = null;
26 |
27 | // 如果指定了延迟,使用 setTimeout 延迟加载
28 | const loadComponent = () => {
29 | setLoading(true);
30 |
31 | componentPromise()
32 | .then(module => {
33 | if (mounted) {
34 | setComponent(() => module.default);
35 | setLoading(false);
36 | }
37 | })
38 | .catch(err => {
39 | if (mounted) {
40 | console.error('加载组件失败:', err);
41 | setError(err);
42 | setLoading(false);
43 | }
44 | });
45 | };
46 |
47 | if (options.delay) {
48 | timer = setTimeout(loadComponent, options.delay);
49 | } else {
50 | loadComponent();
51 | }
52 |
53 | return () => {
54 | mounted = false;
55 | if (timer) clearTimeout(timer);
56 | };
57 | }, [componentPromise, options.delay]);
58 |
59 | return [Component, loading, error];
60 | }
61 |
62 | /**
63 | * 异步加载组件的包装组件
64 | */
65 | export function AsyncComponent({ load, fallback = null, props = {} }: SplitComponentProps): ReactNode {
66 | const [Component, loading, error] = useCodeSplitting(load);
67 |
68 | if (loading) return fallback as ReactNode;
69 | if (error) return React.createElement('div', {}, `加载组件时出错: ${error.message}`);
70 | if (!Component) return null;
71 |
72 | return React.createElement(Component, props);
73 | }
74 |
75 | /**
76 | * 预加载组件
77 | * @param componentPromise 组件导入的 Promise
78 | */
79 | export function preloadComponent(componentPromise: () => Promise<{ default: ComponentType }>) {
80 | // 在后台预加载组件
81 | componentPromise().catch(err => {
82 | console.warn('组件预加载失败:', err);
83 | });
84 | }
--------------------------------------------------------------------------------
/src/hooks/useDebounceThrottle.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef, useCallback } from 'react';
2 |
3 | /**
4 | * 防抖钩子
5 | * @param value 需要防抖的值
6 | * @param delay 延迟时间(毫秒)
7 | * @returns 防抖后的值
8 | */
9 | export function useDebounce(value: T, delay = 500): T {
10 | const [debouncedValue, setDebouncedValue] = useState(value);
11 |
12 | useEffect(() => {
13 | // 设置定时器
14 | const timer = setTimeout(() => {
15 | setDebouncedValue(value);
16 | }, delay);
17 |
18 | // 在下一次 useEffect 执行前清除定时器
19 | return () => {
20 | clearTimeout(timer);
21 | };
22 | }, [value, delay]);
23 |
24 | return debouncedValue;
25 | }
26 |
27 | /**
28 | * 防抖函数钩子
29 | * @param fn 要防抖的函数
30 | * @param delay 延迟时间(毫秒)
31 | * @param deps 依赖数组
32 | * @returns 防抖处理后的函数
33 | */
34 | export function useDebounceFn any>(
35 | fn: T,
36 | delay = 500,
37 | deps: any[] = []
38 | ): T {
39 | const fnRef = useRef(fn);
40 |
41 | // 更新函数引用
42 | useEffect(() => {
43 | fnRef.current = fn;
44 | }, [fn]);
45 |
46 | // eslint-disable-next-line react-hooks/exhaustive-deps
47 | return useCallback(
48 | debounce((...args: Parameters) => {
49 | fnRef.current(...args);
50 | }, delay) as T,
51 | [delay, ...deps]
52 | );
53 | }
54 |
55 | /**
56 | * 节流钩子
57 | * @param value 需要节流的值
58 | * @param delay 延迟时间(毫秒)
59 | * @returns 节流后的值
60 | */
61 | export function useThrottle(value: T, delay = 500): T {
62 | const [throttledValue, setThrottledValue] = useState(value);
63 | const lastUpdated = useRef(Date.now());
64 |
65 | useEffect(() => {
66 | const now = Date.now();
67 | const elapsed = now - lastUpdated.current;
68 |
69 | if (elapsed >= delay) {
70 | lastUpdated.current = now;
71 | setThrottledValue(value);
72 | } else {
73 | const timerId = setTimeout(() => {
74 | lastUpdated.current = Date.now();
75 | setThrottledValue(value);
76 | }, delay - elapsed);
77 |
78 | return () => clearTimeout(timerId);
79 | }
80 | }, [value, delay]);
81 |
82 | return throttledValue;
83 | }
84 |
85 | /**
86 | * 节流函数钩子
87 | * @param fn 要节流的函数
88 | * @param delay 延迟时间(毫秒)
89 | * @param deps 依赖数组
90 | * @returns 节流处理后的函数
91 | */
92 | export function useThrottleFn any>(
93 | fn: T,
94 | delay = 500,
95 | deps: any[] = []
96 | ): T {
97 | const fnRef = useRef(fn);
98 |
99 | // 更新函数引用
100 | useEffect(() => {
101 | fnRef.current = fn;
102 | }, [fn]);
103 |
104 | // eslint-disable-next-line react-hooks/exhaustive-deps
105 | return useCallback(
106 | throttle((...args: Parameters) => {
107 | fnRef.current(...args);
108 | }, delay) as T,
109 | [delay, ...deps]
110 | );
111 | }
112 |
113 | // 防抖函数
114 | function debounce any>(
115 | fn: T,
116 | delay: number
117 | ): (...args: Parameters) => void {
118 | let timer: ReturnType | null = null;
119 |
120 | return function(this: any, ...args: Parameters) {
121 | if (timer) clearTimeout(timer);
122 |
123 | timer = setTimeout(() => {
124 | fn.apply(this, args);
125 | }, delay);
126 | };
127 | }
128 |
129 | // 节流函数
130 | function throttle any>(
131 | fn: T,
132 | delay: number
133 | ): (...args: Parameters) => void {
134 | let lastCalled = 0;
135 |
136 | return function(this: any, ...args: Parameters) {
137 | const now = Date.now();
138 |
139 | if (now - lastCalled >= delay) {
140 | lastCalled = now;
141 | fn.apply(this, args);
142 | }
143 | };
144 | }
--------------------------------------------------------------------------------
/src/hooks/useIntersectionObserver.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, RefObject } from 'react';
2 |
3 | interface IntersectionObserverOptions extends IntersectionObserverInit {
4 | freezeOnceVisible?: boolean;
5 | }
6 |
7 | /**
8 | * 使用 IntersectionObserver 监听元素是否进入视口
9 | * @param elementRef 要监听的元素引用
10 | * @param options IntersectionObserver 配置选项
11 | * @returns IntersectionObserverEntry 或 undefined
12 | */
13 | export function useIntersectionObserver(
14 | elementRef: RefObject,
15 | {
16 | threshold = 0,
17 | root = null,
18 | rootMargin = '0%',
19 | freezeOnceVisible = false,
20 | }: IntersectionObserverOptions = {},
21 | ) {
22 | const [entry, setEntry] = useState();
23 |
24 | const frozen = entry?.isIntersecting && freezeOnceVisible;
25 |
26 | useEffect(() => {
27 | const element = elementRef?.current;
28 | const hasIOSupport = !!window.IntersectionObserver;
29 |
30 | if (!hasIOSupport || frozen || !element) return;
31 |
32 | const observer = new IntersectionObserver(
33 | ([entry]) => {
34 | setEntry(entry);
35 | },
36 | { threshold, root, rootMargin }
37 | );
38 |
39 | observer.observe(element);
40 |
41 | return () => {
42 | observer.disconnect();
43 | };
44 | }, [elementRef, threshold, root, rootMargin, frozen]);
45 |
46 | return entry;
47 | }
--------------------------------------------------------------------------------
/src/hooks/useLocalStorage.ts:
--------------------------------------------------------------------------------
1 | // src/hooks/useLocalStorage.ts
2 | import { useState, useEffect } from 'react';
3 | import { StorageService } from '@/services/StorageService';
4 |
5 | /**
6 | * Hook for using localStorage with state management
7 | * @param key The key to store the value under
8 | * @param initialValue The initial value if none exists in storage
9 | */
10 | export function useLocalStorage(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {
11 | const [storedValue, setStoredValue] = useState(() => {
12 | try {
13 | // Get from local storage by key
14 | const item = StorageService.get(key);
15 | // If not found, return initialValue
16 | return item !== null ? item : initialValue;
17 | } catch (error) {
18 | console.error(`Error reading localStorage key "${key}":`, error);
19 | return initialValue;
20 | }
21 | });
22 |
23 | // Return a wrapped version of useState's setter function that persists the new value to localStorage
24 | const setValue = (value: T | ((val: T) => T)) => {
25 | try {
26 | // Allow value to be a function so we have same API as useState
27 | const valueToStore = value instanceof Function ? value(storedValue) : value;
28 | // Save state
29 | setStoredValue(valueToStore);
30 | // Save to local storage
31 | StorageService.set(key, valueToStore);
32 | } catch (error) {
33 | console.error(`Error setting localStorage key "${key}":`, error);
34 | }
35 | };
36 |
37 | // Listen for changes to this localStorage key from other tabs/windows
38 | useEffect(() => {
39 | function handleStorageChange(e: StorageEvent) {
40 | if (e.key === key && e.newValue !== null) {
41 | try {
42 | setStoredValue(JSON.parse(e.newValue));
43 | } catch (error) {
44 | console.error(`Error parsing localStorage key "${key}" in storage event:`, error);
45 | }
46 | }
47 | }
48 |
49 | // Add event listener
50 | window.addEventListener('storage', handleStorageChange);
51 |
52 | // Clean up
53 | return () => {
54 | window.removeEventListener('storage', handleStorageChange);
55 | };
56 | }, [key]);
57 |
58 | return [storedValue, setValue];
59 | }
--------------------------------------------------------------------------------
/src/hooks/useNetworkStatus.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | interface NetworkStatus {
4 | isOnline: boolean;
5 | isSlowConnection: boolean;
6 | connectionType: string;
7 | effectiveType: string;
8 | downlink: number;
9 | rtt: number;
10 | }
11 |
12 | /**
13 | * 网络状态监控钩子
14 | * @returns 网络状态信息
15 | */
16 | export function useNetworkStatus(): NetworkStatus {
17 | const [networkStatus, setNetworkStatus] = useState({
18 | isOnline: navigator.onLine,
19 | isSlowConnection: false,
20 | connectionType: 'unknown',
21 | effectiveType: 'unknown',
22 | downlink: 0,
23 | rtt: 0,
24 | });
25 |
26 | useEffect(() => {
27 | const updateNetworkStatus = () => {
28 | const connection = (navigator as any).connection ||
29 | (navigator as any).mozConnection ||
30 | (navigator as any).webkitConnection;
31 |
32 | const status: NetworkStatus = {
33 | isOnline: navigator.onLine,
34 | isSlowConnection: false,
35 | connectionType: 'unknown',
36 | effectiveType: 'unknown',
37 | downlink: 0,
38 | rtt: 0,
39 | };
40 |
41 | if (connection) {
42 | status.connectionType = connection.type || 'unknown';
43 | status.effectiveType = connection.effectiveType || 'unknown';
44 | status.downlink = connection.downlink || 0;
45 | status.rtt = connection.rtt || 0;
46 |
47 | // 判断是否为慢速连接
48 | status.isSlowConnection =
49 | connection.effectiveType === 'slow-2g' ||
50 | connection.effectiveType === '2g' ||
51 | (connection.downlink && connection.downlink < 1.5);
52 | }
53 |
54 | setNetworkStatus(status);
55 | };
56 |
57 | // 初始化
58 | updateNetworkStatus();
59 |
60 | // 监听网络状态变化
61 | const handleOnline = () => updateNetworkStatus();
62 | const handleOffline = () => updateNetworkStatus();
63 |
64 | window.addEventListener('online', handleOnline);
65 | window.addEventListener('offline', handleOffline);
66 |
67 | // 监听连接变化(如果支持)
68 | const connection = (navigator as any).connection;
69 | if (connection) {
70 | connection.addEventListener('change', updateNetworkStatus);
71 | }
72 |
73 | return () => {
74 | window.removeEventListener('online', handleOnline);
75 | window.removeEventListener('offline', handleOffline);
76 |
77 | if (connection) {
78 | connection.removeEventListener('change', updateNetworkStatus);
79 | }
80 | };
81 | }, []);
82 |
83 | return networkStatus;
84 | }
85 |
86 | /**
87 | * 网络质量评估钩子
88 | */
89 | export function useNetworkQuality() {
90 | const networkStatus = useNetworkStatus();
91 |
92 | const getQualityScore = (): 'excellent' | 'good' | 'fair' | 'poor' | 'offline' => {
93 | if (!networkStatus.isOnline) return 'offline';
94 |
95 | const { effectiveType, downlink, rtt } = networkStatus;
96 |
97 | // 基于有效连接类型
98 | if (effectiveType === '4g' && downlink > 10 && rtt < 100) return 'excellent';
99 | if (effectiveType === '4g' && downlink > 5) return 'good';
100 | if (effectiveType === '3g' || (downlink > 1.5 && rtt < 300)) return 'fair';
101 |
102 | return 'poor';
103 | };
104 |
105 | const getRecommendations = () => {
106 | const quality = getQualityScore();
107 |
108 | switch (quality) {
109 | case 'offline':
110 | return {
111 | message: '网络连接已断开',
112 | suggestions: ['检查网络连接', '尝试刷新页面'],
113 | shouldReduceQuality: true,
114 | shouldDisableAutoRefresh: true,
115 | };
116 | case 'poor':
117 | return {
118 | message: '网络连接较慢',
119 | suggestions: ['减少图片质量', '延迟非关键请求'],
120 | shouldReduceQuality: true,
121 | shouldDisableAutoRefresh: false,
122 | };
123 | case 'fair':
124 | return {
125 | message: '网络连接一般',
126 | suggestions: ['优化图片加载'],
127 | shouldReduceQuality: false,
128 | shouldDisableAutoRefresh: false,
129 | };
130 | default:
131 | return {
132 | message: '网络连接良好',
133 | suggestions: [],
134 | shouldReduceQuality: false,
135 | shouldDisableAutoRefresh: false,
136 | };
137 | }
138 | };
139 |
140 | return {
141 | ...networkStatus,
142 | quality: getQualityScore(),
143 | recommendations: getRecommendations(),
144 | };
145 | }
146 |
--------------------------------------------------------------------------------
/src/hooks/useTheme.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { ThemeContext } from '../providers/ThemeProvider';
3 |
4 | /**
5 | * Custom hook to access the theme context
6 | * @returns Theme context values and methods
7 | */
8 | export const useTheme = () => {
9 | const context = useContext(ThemeContext);
10 |
11 | if (context === null) {
12 | throw new Error('useTheme must be used within a ThemeProvider');
13 | }
14 |
15 | return context;
16 | };
--------------------------------------------------------------------------------
/src/hooks/useTranslation.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { I18nContext } from '../providers/I18nProvider';
3 | import { useTranslation as useReactI18nTranslation } from 'react-i18next';
4 |
5 | /**
6 | * Custom hook to access the internationalization context
7 | * @returns I18n context values and methods
8 | */
9 | export const useTranslation = () => {
10 | const context = useContext(I18nContext);
11 |
12 | if (context === null) {
13 | throw new Error('useTranslation must be used within an I18nProvider');
14 | }
15 |
16 | // Also initialize the react-i18next hook to make sure translations are loaded
17 | useReactI18nTranslation();
18 |
19 | return context;
20 | };
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @tailwind base;
3 | @plugin "tailwindcss-animate";
4 | @custom-variant dark (&:is(.dark *));
5 | @tailwind components;
6 | @tailwind utilities;
7 |
8 | :root {
9 | font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
10 | line-height: 1.5;
11 | font-weight: 400;
12 | color-scheme: light dark;
13 | font-synthesis: none;
14 | text-rendering: optimizeLegibility;
15 | -webkit-font-smoothing: antialiased;
16 | -moz-osx-font-smoothing: grayscale;
17 | --background: oklch(1 0 0);
18 | --foreground: oklch(0.141 0.005 285.823);
19 | --card: oklch(1 0 0);
20 | --card-foreground: oklch(0.141 0.005 285.823);
21 | --popover: oklch(1 0 0);
22 | --popover-foreground: oklch(0.141 0.005 285.823);
23 | --primary: oklch(0.21 0.006 285.885);
24 | --primary-foreground: oklch(0.985 0 0);
25 | --secondary: oklch(0.967 0.001 286.375);
26 | --secondary-foreground: oklch(0.21 0.006 285.885);
27 | --muted: oklch(0.967 0.001 286.375);
28 | --muted-foreground: oklch(0.552 0.016 285.938);
29 | --accent: oklch(0.967 0.001 286.375);
30 | --accent-foreground: oklch(0.21 0.006 285.885);
31 | --destructive: oklch(0.577 0.245 27.325);
32 | --destructive-foreground: oklch(0.577 0.245 27.325);
33 | --border: oklch(0.92 0.004 286.32);
34 | --input: oklch(0.92 0.004 286.32);
35 | --ring: oklch(0.871 0.006 286.286);
36 | --chart-1: oklch(0.646 0.222 41.116);
37 | --chart-2: oklch(0.6 0.118 184.704);
38 | --chart-3: oklch(0.398 0.07 227.392);
39 | --chart-4: oklch(0.828 0.189 84.429);
40 | --chart-5: oklch(0.769 0.188 70.08);
41 | --radius: 0.625rem;
42 | --sidebar: oklch(0.985 0 0);
43 | --sidebar-foreground: oklch(0.141 0.005 285.823);
44 | --sidebar-primary: oklch(0.21 0.006 285.885);
45 | --sidebar-primary-foreground: oklch(0.985 0 0);
46 | --sidebar-accent: oklch(0.967 0.001 286.375);
47 | --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
48 | --sidebar-border: oklch(0.92 0.004 286.32);
49 | --sidebar-ring: oklch(0.871 0.006 286.286);
50 | }
51 |
52 |
53 | @media (prefers-color-scheme: light) {
54 | :root {
55 | color: #213547;
56 | background-color: #ffffff;
57 | }
58 |
59 | a:hover {
60 | color: #747bff;
61 | }
62 |
63 | button {
64 | background-color: #f9f9f9;
65 | }
66 | }
67 |
68 | .dark {
69 | --background: oklch(0.141 0.005 285.823);
70 | --foreground: oklch(0.985 0 0);
71 | --card: oklch(0.141 0.005 285.823);
72 | --card-foreground: oklch(0.985 0 0);
73 | --popover: oklch(0.141 0.005 285.823);
74 | --popover-foreground: oklch(0.985 0 0);
75 | --primary: oklch(0.985 0 0);
76 | --primary-foreground: oklch(0.21 0.006 285.885);
77 | --secondary: oklch(0.274 0.006 286.033);
78 | --secondary-foreground: oklch(0.985 0 0);
79 | --muted: oklch(0.274 0.006 286.033);
80 | --muted-foreground: oklch(0.705 0.015 286.067);
81 | --accent: oklch(0.274 0.006 286.033);
82 | --accent-foreground: oklch(0.985 0 0);
83 | --destructive: oklch(0.396 0.141 25.723);
84 | --destructive-foreground: oklch(0.637 0.237 25.331);
85 | --border: oklch(0.274 0.006 286.033);
86 | --input: oklch(0.274 0.006 286.033);
87 | --ring: oklch(0.442 0.017 285.786);
88 | --chart-1: oklch(0.488 0.243 264.376);
89 | --chart-2: oklch(0.696 0.17 162.48);
90 | --chart-3: oklch(0.769 0.188 70.08);
91 | --chart-4: oklch(0.627 0.265 303.9);
92 | --chart-5: oklch(0.645 0.246 16.439);
93 | --sidebar: oklch(0.21 0.006 285.885);
94 | --sidebar-foreground: oklch(0.985 0 0);
95 | --sidebar-primary: oklch(0.488 0.243 264.376);
96 | --sidebar-primary-foreground: oklch(0.985 0 0);
97 | --sidebar-accent: oklch(0.274 0.006 286.033);
98 | --sidebar-accent-foreground: oklch(0.985 0 0);
99 | --sidebar-border: oklch(0.274 0.006 286.033);
100 | --sidebar-ring: oklch(0.442 0.017 285.786);
101 | }
102 |
103 | @theme inline {
104 | --color-background: var(--background);
105 | --color-foreground: var(--foreground);
106 | --color-card: var(--card);
107 | --color-card-foreground: var(--card-foreground);
108 | --color-popover: var(--popover);
109 | --color-popover-foreground: var(--popover-foreground);
110 | --color-primary: var(--primary);
111 | --color-primary-foreground: var(--primary-foreground);
112 | --color-secondary: var(--secondary);
113 | --color-secondary-foreground: var(--secondary-foreground);
114 | --color-muted: var(--muted);
115 | --color-muted-foreground: var(--muted-foreground);
116 | --color-accent: var(--accent);
117 | --color-accent-foreground: var(--accent-foreground);
118 | --color-destructive: var(--destructive);
119 | --color-destructive-foreground: var(--destructive-foreground);
120 | --color-border: var(--border);
121 | --color-input: var(--input);
122 | --color-ring: var(--ring);
123 | --color-chart-1: var(--chart-1);
124 | --color-chart-2: var(--chart-2);
125 | --color-chart-3: var(--chart-3);
126 | --color-chart-4: var(--chart-4);
127 | --color-chart-5: var(--chart-5);
128 | --radius-sm: calc(var(--radius) - 4px);
129 | --radius-md: calc(var(--radius) - 2px);
130 | --radius-lg: var(--radius);
131 | --radius-xl: calc(var(--radius) + 4px);
132 | --color-sidebar: var(--sidebar);
133 | --color-sidebar-foreground: var(--sidebar-foreground);
134 | --color-sidebar-primary: var(--sidebar-primary);
135 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
136 | --color-sidebar-accent: var(--sidebar-accent);
137 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
138 | --color-sidebar-border: var(--sidebar-border);
139 | --color-sidebar-ring: var(--sidebar-ring);
140 | --animate-accordion-down: accordion-down 0.2s ease-out;
141 | --animate-accordion-up: accordion-up 0.2s ease-out;
142 |
143 | @keyframes accordion-down {
144 | from {
145 | height: 0;
146 | }
147 |
148 | to {
149 | height: var(--radix-accordion-content-height);
150 | }
151 | }
152 |
153 | @keyframes accordion-up {
154 | from {
155 | height: var(--radix-accordion-content-height);
156 | }
157 |
158 | to {
159 | height: 0;
160 | }
161 | }
162 | }
163 |
164 | @layer base {
165 | * {
166 | @apply border-border outline-ring/50;
167 | }
168 |
169 | body {
170 | @apply bg-background text-foreground;
171 | }
172 | }
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx } from 'clsx';
2 | import { twMerge } from 'tailwind-merge';
3 | import { type ClassValue } from 'clsx';
4 |
5 | /**
6 | * Combines multiple class names using clsx and ensures proper
7 | * handling of tailwind classes with twMerge
8 | */
9 | export function cn(...inputs: ClassValue[]): string {
10 | return twMerge(clsx(inputs));
11 | }
12 |
13 | /**
14 | * Format a date according to the specified format
15 | */
16 | export function formatDate(
17 | date: Date | string,
18 | options: Intl.DateTimeFormatOptions = {
19 | year: 'numeric',
20 | month: 'long',
21 | day: 'numeric'
22 | }
23 | ): string {
24 | return new Date(date).toLocaleDateString(undefined, options);
25 | }
26 |
27 | /**
28 | * Truncate a string if it exceeds the specified length
29 | */
30 | export function truncateString(str: string, maxLength: number): string {
31 | if (str.length <= maxLength) return str;
32 | return str.slice(0, maxLength) + '...';
33 | }
34 |
35 | /**
36 | * Generate a random string of the specified length
37 | */
38 | export function generateRandomString(length: number): string {
39 | const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
40 | let result = '';
41 |
42 | for (let i = 0; i < length; i++) {
43 | result += characters.charAt(Math.floor(Math.random() * characters.length));
44 | }
45 |
46 | return result;
47 | }
48 |
49 | /**
50 | * Delay execution for the specified number of milliseconds
51 | */
52 | export function delay(ms: number): Promise {
53 | return new Promise(resolve => setTimeout(resolve, ms));
54 | }
55 |
56 | /**
57 | * Check if an element is in viewport
58 | */
59 | export function isElementInViewport(element: HTMLElement): boolean {
60 | const rect = element.getBoundingClientRect();
61 |
62 | return (
63 | rect.top >= 0 &&
64 | rect.left >= 0 &&
65 | rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
66 | rect.right <= (window.innerWidth || document.documentElement.clientWidth)
67 | );
68 | }
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode, lazy, Suspense } from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import './index.css';
4 | import WebFont from 'webfontloader';
5 |
6 | // 使用 lazy 加载主应用
7 | const App = lazy(() => import('./App'));
8 |
9 | // 添加加载中组件
10 | const LoadingFallback = () => (
11 |
14 | );
15 |
16 | // 性能监控
17 | function reportWebVitals() {
18 | if ('performance' in window && 'getEntriesByType' in performance) {
19 | // FCP 指标
20 | const navigationEntries = performance.getEntriesByType('navigation');
21 | if (navigationEntries.length > 0) {
22 | const navEntry = navigationEntries[0] as PerformanceNavigationTiming;
23 | console.log('FCP:', navEntry.domContentLoadedEventEnd - navEntry.startTime);
24 | }
25 |
26 | // LCP 监控
27 | let lcpReported = false;
28 | const lcpObserver = new PerformanceObserver((entryList) => {
29 | const entries = entryList.getEntries();
30 | const lastEntry = entries[entries.length - 1];
31 | if (!lcpReported) {
32 | lcpReported = true;
33 | console.log('LCP:', lastEntry.startTime);
34 | }
35 | });
36 |
37 | lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
38 |
39 | // CLS 监控
40 | let clsValue = 0;
41 | let clsReported = false;
42 | const clsObserver = new PerformanceObserver((entryList) => {
43 | for (const entry of entryList.getEntries()) {
44 | if (!clsReported) {
45 | const clsEntry = entry as any;
46 | clsValue += clsEntry.value;
47 | console.log('CLS update:', clsValue);
48 | }
49 | }
50 | });
51 |
52 | clsObserver.observe({ type: 'layout-shift', buffered: true });
53 |
54 | // 5秒后报告最终CLS
55 | setTimeout(() => {
56 | if (!clsReported) {
57 | clsReported = true;
58 | console.log('Final CLS:', clsValue);
59 | }
60 | }, 5000);
61 | }
62 | }
63 |
64 | // 使用预加载策略优化字体加载
65 | WebFont.load({
66 | google: {
67 | families: ['Inter:300,400,500,600,700&display=swap', 'Roboto Mono:400,500&display=swap']
68 | },
69 | // 添加加载完成回调
70 | active: () => {
71 | console.log('Fonts loaded successfully');
72 | },
73 | inactive: () => {
74 | console.warn('Failed to load fonts');
75 | }
76 | });
77 |
78 | // 注册Service Worker
79 | if ('serviceWorker' in navigator && import.meta.env.PROD) {
80 | window.addEventListener('load', () => {
81 | navigator.serviceWorker.register('/sw.js')
82 | .then(registration => {
83 | console.log('ServiceWorker registration successful with scope: ', registration.scope);
84 | })
85 | .catch(error => {
86 | console.error('ServiceWorker registration failed: ', error);
87 | });
88 | });
89 | }
90 |
91 | // 初始化应用
92 | const container = document.getElementById('root');
93 | if (container) {
94 | const root = createRoot(container);
95 | root.render(
96 |
97 | }>
98 |
99 |
100 |
101 | );
102 |
103 | // 检测并报告网站性能指标
104 | if (import.meta.env.PROD) {
105 | reportWebVitals();
106 | }
107 | }
--------------------------------------------------------------------------------
/src/pages/NotFound.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useNavigate, Link } from 'react-router-dom';
3 | import { useTranslation } from '../hooks/useTranslation';
4 | import { Button } from '../components/ui/button';
5 |
6 | const NotFound: React.FC = () => {
7 | const navigate = useNavigate();
8 | const { t } = useTranslation();
9 |
10 | return (
11 |
12 |
13 | 404
14 |
15 | {t('notFound.title', 'Page Not Found')}
16 |
17 |
18 | {t('notFound.description', "Sorry, the page you are looking for doesn't exist or has been moved.")}
19 |
20 |
21 |
24 |
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default NotFound;
--------------------------------------------------------------------------------
/src/pages/auth/ForgotPassword.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { useAuth } from '../../hooks/useAuth';
4 | import { useTranslation } from '../../hooks/useTranslation';
5 | import { z } from 'zod';
6 | import { useForm } from 'react-hook-form';
7 | import { zodResolver } from '@hookform/resolvers/zod';
8 |
9 | import { Button } from '../../components/ui/button';
10 | import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../../components/ui/card';
11 | import { Input } from '../../components/ui/input';
12 | import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../../components/ui/form';
13 | import { AlertCircle, ArrowLeft, CheckCircle2, Loader2 } from 'lucide-react';
14 | import { Alert, AlertDescription, AlertTitle } from '../../components/ui/alert';
15 |
16 | const forgotPasswordSchema = z.object({
17 | email: z.string().email({
18 | message: "Please enter a valid email address",
19 | }),
20 | });
21 |
22 | type ForgotPasswordValues = z.infer;
23 |
24 | const ForgotPassword: React.FC = () => {
25 | const { resetPassword, isLoading, error } = useAuth();
26 | const { t } = useTranslation();
27 | const [isSubmitted, setIsSubmitted] = useState(false);
28 | const [submittedEmail, setSubmittedEmail] = useState('');
29 |
30 | const form = useForm({
31 | resolver: zodResolver(forgotPasswordSchema),
32 | defaultValues: {
33 | email: '',
34 | },
35 | });
36 |
37 | const onSubmit = async (values: ForgotPasswordValues) => {
38 | await resetPassword(values.email);
39 | setSubmittedEmail(values.email);
40 | setIsSubmitted(true);
41 | };
42 |
43 | return (
44 |
45 |
46 |
47 | ReactUltra
48 |
49 | {t('auth.forgotPasswordSubtitle', 'Password reset instructions')}
50 |
51 |
52 |
53 |
54 |
55 |
56 | {isSubmitted
57 | ? t('auth.resetLinkSent', 'Reset link sent!')
58 | : t('auth.forgotPasswordTitle', 'Forgot your password?')}
59 |
60 |
61 | {isSubmitted
62 | ? t('auth.checkEmailForInstructions', 'Please check your email for password reset instructions.')
63 | : t('auth.forgotPasswordDescription', "Enter your email and we'll send you a link to reset your password")}
64 |
65 |
66 |
67 | {error && (
68 |
69 |
70 | {error}
71 |
72 | )}
73 |
74 | {isSubmitted ? (
75 |
76 |
77 | Email sent
78 |
79 | We've sent reset instructions to {submittedEmail}
80 |
81 |
82 | ) : (
83 |
112 |
113 | )}
114 |
115 |
116 |
117 |
121 |
122 | {t('auth.backToLogin', 'Back to Login')}
123 |
124 |
125 |
126 |
127 |
128 |
129 | );
130 | };
131 |
132 | export default ForgotPassword;
--------------------------------------------------------------------------------
/src/providers/AppProvider.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, Suspense, lazy } from 'react';
2 | import { ThemeProvider } from './ThemeProvider';
3 | import { I18nProvider } from './I18nProvider';
4 | import { AuthProvider } from './AuthProvider';
5 | import { RouterProvider, AppRoutes } from './RouterProvider';
6 | import { ErrorBoundary } from '../components/common/ErrorBoundary';
7 |
8 | // 延迟加载不是立即需要的组件
9 | const LazyToaster = lazy(() => import('../components/ui/sonner').then(module => ({ default: module.Toaster })));
10 |
11 | // 优化的加载反馈组件
12 | const LoadingFallback = () => (
13 |
19 | );
20 |
21 | // 错误反馈组件
22 | const ErrorFallbackComponent = ({ error, resetError }: { error: Error; resetError: () => void }) => (
23 |
24 |
25 | 应用出错了
26 | {error.message}
27 |
33 |
34 |
35 | );
36 |
37 | interface AppProviderProps {
38 | children: ReactNode;
39 | }
40 |
41 | /**
42 | * AppProvider 是应用程序的根提供者,整合所有上下文提供者
43 | * 并确保提供者之间的依赖关系正确排序
44 | */
45 | export const AppProvider: React.FC = ({ children }) => {
46 | return (
47 | window.location.reload()} />}>
48 |
49 |
50 |
51 |
52 | {/* 主应用内容 */}
53 | }>
54 |
55 | {children}
56 | {/* Toast通知组件 */}
57 |
58 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | );
73 | };
--------------------------------------------------------------------------------
/src/providers/RouterProvider.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, useEffect, Suspense, lazy, useState } from 'react';
2 | import { BrowserRouter, Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom';
3 | import { useAuth } from '../hooks/useAuth';
4 | import { routes, preloadRoute, initPreload } from '../config/routes.config';
5 |
6 | // 延迟加载主布局组件
7 | const Layout = lazy(() => import('../components/layout/Layout'));
8 |
9 | // 美观的加载反馈
10 | const LoadingFallback = () => (
11 |
17 | );
18 |
19 | // 页面过渡动画组件
20 | const PageTransition = ({ children }: { children: ReactNode }) => {
21 | return (
22 |
23 | {children}
24 |
25 | );
26 | };
27 |
28 | interface RouterProviderProps {
29 | children: ReactNode;
30 | }
31 |
32 | // AppRoutes 组件包含所有路由逻辑
33 | const AppRoutes = () => {
34 | const { isAuthenticated, user } = useAuth();
35 | const location = useLocation();
36 | const navigate = useNavigate();
37 | const [prevPathname, setPrevPathname] = useState('/');
38 |
39 | // 记录导航性能
40 | useEffect(() => {
41 | // 记录页面导航性能
42 | const trackNavigation = async () => {
43 | if (location.pathname !== prevPathname) {
44 | const navStart = performance.now();
45 |
46 | // 记录导航完成时间
47 | window.requestAnimationFrame(() => {
48 | const navEnd = performance.now();
49 | const navTime = navEnd - navStart;
50 |
51 | if (import.meta.env.DEV) {
52 | console.log(`导航到 ${location.pathname} 耗时: ${navTime.toFixed(2)}ms`);
53 | }
54 |
55 | // 在生产环境,可以将这些数据发送到分析服务
56 | setPrevPathname(location.pathname);
57 | });
58 | }
59 | };
60 |
61 | trackNavigation();
62 | }, [location.pathname, prevPathname]);
63 |
64 | // 路由变化监听,处理认证和预加载
65 | useEffect(() => {
66 | // 保存认证状态下的上一个路径
67 | if (isAuthenticated && location.pathname !== '/login') {
68 | localStorage.setItem('lastPath', location.pathname);
69 | }
70 |
71 | // 为所有内部链接添加预加载
72 | const setupPreload = () => {
73 | const links = document.querySelectorAll('a[href^="/"]');
74 |
75 | links.forEach(link => {
76 | const href = link.getAttribute('href');
77 |
78 | // 优化:使用事件委托来减少事件监听器数量
79 | link.addEventListener('mouseenter', () => {
80 | if (href) preloadRoute(href);
81 | });
82 |
83 | // 触摸设备上的预加载
84 | link.addEventListener('touchstart', () => {
85 | if (href) preloadRoute(href);
86 | });
87 | });
88 |
89 | return () => {
90 | links.forEach(link => {
91 | link.removeEventListener('mouseenter', () => {});
92 | link.removeEventListener('touchstart', () => {});
93 | });
94 | };
95 | };
96 |
97 | // 延迟设置预加载,避免阻塞初始渲染
98 | const timer = setTimeout(setupPreload, 1000);
99 |
100 | // 在初始加载时触发预加载
101 | if (prevPathname === '/') {
102 | initPreload();
103 | }
104 |
105 | return () => clearTimeout(timer);
106 | }, [location.pathname, isAuthenticated, prevPathname]);
107 |
108 | return (
109 | }>
110 |
111 | {routes.map((route) => {
112 | const RouteElement = route.element;
113 | let routeElement;
114 |
115 | // 检查路由守卫
116 | const guardsPassed = route.guards ?
117 | route.guards.every(guard => guard(isAuthenticated)) : true;
118 |
119 | if (guardsPassed) {
120 | if (route.meta.layout === 'default') {
121 | routeElement = (
122 | }>
123 |
124 |
125 |
126 |
127 |
128 |
129 | );
130 | } else {
131 | routeElement = (
132 | }>
133 |
134 |
135 |
136 |
137 | );
138 | }
139 | } else if (route.redirectTo) {
140 | // 如果需要认证但没有认证,记住当前尝试访问的URL
141 | if (route.meta.auth && !isAuthenticated) {
142 | // 使用会话存储,这样即使刷新也能保留
143 | sessionStorage.setItem('redirectAfterLogin', location.pathname);
144 | }
145 | routeElement = ;
146 | }
147 |
148 | return (
149 |
154 | );
155 | })}
156 |
157 |
158 | );
159 | };
160 |
161 | /**
162 | * RouterProvider 处理应用程序的路由配置
163 | * 它包含了认证和非认证路由的路由守卫
164 | */
165 | export const RouterProvider: React.FC = ({ children }) => {
166 | return (
167 |
168 | {children}
169 |
170 | );
171 | };
172 |
173 | // 导出 AppRoutes 供其他组件使用
174 | export { AppRoutes };
--------------------------------------------------------------------------------
/src/providers/StoreProvider.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 | // This is primarily a wrapper for potential future state management setup
3 | // For now, it serves as a wrapper for Zustand stores
4 |
5 | interface StoreProviderProps {
6 | children: ReactNode;
7 | }
8 |
9 | export const StoreProvider: React.FC = ({ children }) => {
10 | // Zustand doesn't require a provider wrapper like Context or Redux
11 | // This component exists for architectural consistency and potential future extensions
12 | return <>{children}>;
13 | };
--------------------------------------------------------------------------------
/src/providers/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useEffect, useState, ReactNode } from 'react';
2 | import { ThemeProvider as NextThemeProvider } from 'next-themes';
3 |
4 | type ThemeType = 'dark' | 'light' | 'system';
5 |
6 | interface ThemeContextType {
7 | theme: ThemeType;
8 | setTheme: (theme: ThemeType) => void;
9 | toggleTheme: () => void;
10 | isDarkMode: boolean;
11 | }
12 |
13 | export const ThemeContext = createContext(null);
14 |
15 | interface ThemeProviderProps {
16 | children: ReactNode;
17 | }
18 |
19 | export const ThemeProvider: React.FC = ({ children }) => {
20 | const [theme, setTheme] = useState('system');
21 | const [isDarkMode, setIsDarkMode] = useState(false);
22 | const [nextTheme, setNextTheme] = useState('system');
23 | const [themeValue, setThemeValue] = useState('system');
24 |
25 | // Initialize theme based on system preference or saved preferences
26 | useEffect(() => {
27 | const savedTheme = localStorage.getItem('theme');
28 | if (savedTheme) {
29 | const themeValue = savedTheme as ThemeType;
30 | setTheme(themeValue);
31 | setNextTheme(themeValue);
32 | }
33 | }, []);
34 |
35 | // Update dark mode state based on media query and theme
36 | useEffect(() => {
37 | const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
38 |
39 | const handleChange = () => {
40 | if (theme === 'system') {
41 | setIsDarkMode(mediaQuery.matches);
42 | } else {
43 | setIsDarkMode(theme === 'dark');
44 | }
45 | };
46 |
47 | handleChange(); // Set initial value
48 | mediaQuery.addEventListener('change', handleChange);
49 |
50 | return () => {
51 | mediaQuery.removeEventListener('change', handleChange);
52 | };
53 | }, [theme]);
54 |
55 | // Toggle between light and dark themes
56 | const toggleTheme = () => {
57 | const newTheme = theme === 'dark' ? 'light' : 'dark';
58 | setTheme(newTheme);
59 | setNextTheme(newTheme);
60 | localStorage.setItem('theme', newTheme);
61 | };
62 |
63 | // Custom theme setter with localStorage persistence
64 | const handleSetTheme = (newTheme: ThemeType) => {
65 | setTheme(newTheme);
66 | setNextTheme(newTheme);
67 | localStorage.setItem('theme', newTheme);
68 | };
69 |
70 | const value = {
71 | theme,
72 | setTheme: handleSetTheme,
73 | toggleTheme,
74 | isDarkMode,
75 | };
76 |
77 | return (
78 |
79 |
80 | {children}
81 |
82 |
83 | );
84 | };
--------------------------------------------------------------------------------
/src/services/ApiService.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, CancelTokenSource } from 'axios';
2 | import { API_BASE_URL, API_TIMEOUT, API_HEADERS } from '@/config/api.config';
3 | import { StorageService } from './StorageService';
4 | import { LoggerService } from './LoggerService';
5 |
6 | export interface ApiResponse {
7 | data: T;
8 | status: number;
9 | message?: string;
10 | }
11 |
12 | export interface RequestConfig extends AxiosRequestConfig {
13 | cancelId?: string;
14 | }
15 |
16 | class ApiServiceClass {
17 | private client: AxiosInstance;
18 | private cancelTokens: Map;
19 |
20 | constructor() {
21 | this.client = axios.create({
22 | baseURL: API_BASE_URL,
23 | timeout: API_TIMEOUT,
24 | headers: API_HEADERS,
25 | });
26 | this.cancelTokens = new Map();
27 |
28 | this.setupInterceptors();
29 | }
30 |
31 | private setupInterceptors(): void {
32 | // Request interceptor
33 | this.client.interceptors.request.use(
34 | (config) => {
35 | // Add auth token if available
36 | const token = StorageService.get('authToken');
37 | if (token && config.headers) {
38 | config.headers.Authorization = `Bearer ${token}`;
39 | }
40 | return config;
41 | },
42 | (error) => {
43 | LoggerService.error('API request interceptor error', error);
44 | return Promise.reject(error);
45 | }
46 | );
47 |
48 | // Response interceptor
49 | this.client.interceptors.response.use(
50 | (response) => {
51 | return response;
52 | },
53 | (error) => {
54 | if (error.response?.status === 401) {
55 | // Handle unauthorized (token expired, etc.)
56 | StorageService.remove('authToken');
57 | // You could trigger a logout action or redirect here
58 | }
59 |
60 | LoggerService.error('API response error', {
61 | url: error.config?.url,
62 | status: error.response?.status,
63 | error: error.message,
64 | });
65 |
66 | return Promise.reject(error);
67 | }
68 | );
69 | }
70 |
71 | /**
72 | * Set auth token for API requests
73 | */
74 | public setAuthToken(token: string | null): void {
75 | if (token) {
76 | this.client.defaults.headers.common.Authorization = `Bearer ${token}`;
77 | } else {
78 | delete this.client.defaults.headers.common.Authorization;
79 | }
80 | }
81 |
82 | /**
83 | * Make GET request
84 | */
85 | public async get(url: string, config?: RequestConfig): Promise> {
86 | const { cancelId, ...axiosConfig } = config || {};
87 |
88 | if (cancelId) {
89 | this.setupCancelToken(cancelId, axiosConfig);
90 | }
91 |
92 | try {
93 | const response: AxiosResponse = await this.client.get(url, axiosConfig);
94 | return {
95 | data: response.data,
96 | status: response.status,
97 | };
98 | } catch (error) {
99 | this.handleRequestError(error);
100 | throw error;
101 | }
102 | }
103 |
104 | /**
105 | * Make POST request
106 | */
107 | public async post(url: string, data?: any, config?: RequestConfig): Promise> {
108 | const { cancelId, ...axiosConfig } = config || {};
109 |
110 | if (cancelId) {
111 | this.setupCancelToken(cancelId, axiosConfig);
112 | }
113 |
114 | try {
115 | const response: AxiosResponse = await this.client.post(url, data, axiosConfig);
116 | return {
117 | data: response.data,
118 | status: response.status,
119 | };
120 | } catch (error) {
121 | this.handleRequestError(error);
122 | throw error;
123 | }
124 | }
125 |
126 | /**
127 | * Make PUT request
128 | */
129 | public async put(url: string, data?: any, config?: RequestConfig): Promise> {
130 | const { cancelId, ...axiosConfig } = config || {};
131 |
132 | if (cancelId) {
133 | this.setupCancelToken(cancelId, axiosConfig);
134 | }
135 |
136 | try {
137 | const response: AxiosResponse = await this.client.put(url, data, axiosConfig);
138 | return {
139 | data: response.data,
140 | status: response.status,
141 | };
142 | } catch (error) {
143 | this.handleRequestError(error);
144 | throw error;
145 | }
146 | }
147 |
148 | /**
149 | * Make DELETE request
150 | */
151 | public async delete(url: string, config?: RequestConfig): Promise> {
152 | const { cancelId, ...axiosConfig } = config || {};
153 |
154 | if (cancelId) {
155 | this.setupCancelToken(cancelId, axiosConfig);
156 | }
157 |
158 | try {
159 | const response: AxiosResponse = await this.client.delete(url, axiosConfig);
160 | return {
161 | data: response.data,
162 | status: response.status,
163 | };
164 | } catch (error) {
165 | this.handleRequestError(error);
166 | throw error;
167 | }
168 | }
169 |
170 | /**
171 | * Cancel a request by ID
172 | */
173 | public cancelRequest(cancelId: string): void {
174 | const source = this.cancelTokens.get(cancelId);
175 | if (source) {
176 | source.cancel(`Request canceled: ${cancelId}`);
177 | this.cancelTokens.delete(cancelId);
178 | }
179 | }
180 |
181 | /**
182 | * Set up cancel token for a request
183 | */
184 | private setupCancelToken(cancelId: string, config: AxiosRequestConfig): void {
185 | // Cancel any existing request with the same ID
186 | this.cancelRequest(cancelId);
187 |
188 | const source = axios.CancelToken.source();
189 | this.cancelTokens.set(cancelId, source);
190 | config.cancelToken = source.token;
191 | }
192 |
193 | /**
194 | * Handle axios request errors
195 | */
196 | private handleRequestError(error: any): void {
197 | if (axios.isCancel(error)) {
198 | LoggerService.info('Request canceled', error.message);
199 | } else {
200 | LoggerService.error('API request failed', error);
201 | }
202 | }
203 | }
204 |
205 | // Create singleton instance
206 | export const ApiService = new ApiServiceClass();
--------------------------------------------------------------------------------
/src/services/LoggerService.ts:
--------------------------------------------------------------------------------
1 | // Log levels
2 | enum LogLevel {
3 | DEBUG = 'DEBUG',
4 | INFO = 'INFO',
5 | WARN = 'WARN',
6 | ERROR = 'ERROR',
7 | }
8 |
9 | class LoggerServiceClass {
10 | private readonly isProduction = process.env.NODE_ENV === 'production';
11 | private readonly enableRemoteLogging = false; // Set this to true to enable remote logging
12 |
13 | /**
14 | * Debug level log
15 | */
16 | public debug(message: string, data?: any): void {
17 | this.log(LogLevel.DEBUG, message, data);
18 | }
19 |
20 | /**
21 | * Info level log
22 | */
23 | public info(message: string, data?: any): void {
24 | this.log(LogLevel.INFO, message, data);
25 | }
26 |
27 | /**
28 | * Warning level log
29 | */
30 | public warn(message: string, data?: any): void {
31 | this.log(LogLevel.WARN, message, data);
32 | }
33 |
34 | /**
35 | * Error level log
36 | */
37 | public error(message: string, error?: any): void {
38 | this.log(LogLevel.ERROR, message, error);
39 |
40 | // In production, you might want to send errors to a monitoring service like Sentry
41 | if (this.isProduction && this.enableRemoteLogging) {
42 | this.sendToRemoteLogging(message, error);
43 | }
44 | }
45 |
46 | /**
47 | * Internal log method
48 | */
49 | private log(level: LogLevel, message: string, data?: any): void {
50 | const timestamp = new Date().toISOString();
51 | const formattedMessage = `[${timestamp}] [${level}]: ${message}`;
52 |
53 | // Skip debug logs in production unless explicitly enabled
54 | if (this.isProduction && level === LogLevel.DEBUG) {
55 | return;
56 | }
57 |
58 | switch (level) {
59 | case LogLevel.DEBUG:
60 | console.debug(formattedMessage, data || '');
61 | break;
62 | case LogLevel.INFO:
63 | console.info(formattedMessage, data || '');
64 | break;
65 | case LogLevel.WARN:
66 | console.warn(formattedMessage, data || '');
67 | break;
68 | case LogLevel.ERROR:
69 | console.error(formattedMessage, data || '');
70 | break;
71 | default:
72 | console.log(formattedMessage, data || '');
73 | }
74 | }
75 |
76 | /**
77 | * Send logs to remote logging service
78 | * This is a placeholder for integration with services like Sentry, LogRocket, etc.
79 | */
80 | private sendToRemoteLogging(message: string, data?: any): void {
81 | // Implementation would depend on the actual service used
82 | // Example for Sentry:
83 | // Sentry.captureException(data, {
84 | // extra: {
85 | // message,
86 | // },
87 | // });
88 | }
89 | }
90 |
91 | // Create singleton instance
92 | export const LoggerService = new LoggerServiceClass();
--------------------------------------------------------------------------------
/src/services/StorageService.ts:
--------------------------------------------------------------------------------
1 | class StorageServiceClass {
2 | /**
3 | * Get item from storage
4 | */
5 | public get(key: string): T | null {
6 | try {
7 | const item = localStorage.getItem(key);
8 | return item ? JSON.parse(item) : null;
9 | } catch (error) {
10 | console.error(`Error getting item ${key} from localStorage:`, error);
11 | return null;
12 | }
13 | }
14 |
15 | /**
16 | * Set item in storage
17 | */
18 | public set(key: string, value: T): void {
19 | try {
20 | localStorage.setItem(key, JSON.stringify(value));
21 | } catch (error) {
22 | console.error(`Error setting item ${key} in localStorage:`, error);
23 | }
24 | }
25 |
26 | /**
27 | * Remove item from storage
28 | */
29 | public remove(key: string): void {
30 | try {
31 | localStorage.removeItem(key);
32 | } catch (error) {
33 | console.error(`Error removing item ${key} from localStorage:`, error);
34 | }
35 | }
36 |
37 | /**
38 | * Clear all items in storage
39 | */
40 | public clear(): void {
41 | try {
42 | localStorage.clear();
43 | } catch (error) {
44 | console.error('Error clearing localStorage:', error);
45 | }
46 | }
47 |
48 | /**
49 | * Check if storage has item
50 | */
51 | public has(key: string): boolean {
52 | try {
53 | return localStorage.getItem(key) !== null;
54 | } catch (error) {
55 | console.error(`Error checking if localStorage has item ${key}:`, error);
56 | return false;
57 | }
58 | }
59 |
60 | /**
61 | * Get all keys in storage
62 | */
63 | public keys(): string[] {
64 | try {
65 | return Object.keys(localStorage);
66 | } catch (error) {
67 | console.error('Error getting localStorage keys:', error);
68 | return [];
69 | }
70 | }
71 | }
72 |
73 | // Create singleton instance
74 | export const StorageService = new StorageServiceClass();
--------------------------------------------------------------------------------
/src/stores/i18nStore.ts:
--------------------------------------------------------------------------------
1 | // src/stores/i18nStore.ts
2 | import { create } from 'zustand';
3 | import { devtools, persist } from 'zustand/middleware';
4 | import { ApiService } from '@/services/ApiService';
5 | import { StorageService } from '@/services/StorageService';
6 | import { LoggerService } from '@/services/LoggerService';
7 | import { Language } from '@/types/i18n.types';
8 |
9 | interface I18nState {
10 | language: string;
11 | languages: Language[];
12 | translations: Record;
13 | loading: boolean;
14 | error: string | null;
15 | setLanguage: (language: string) => void;
16 | loadTranslations: (language: string) => Promise;
17 | t: (key: string, options?: Record) => string;
18 | }
19 |
20 | export const useI18nStore = create()(
21 | devtools(
22 | persist(
23 | (set, get) => ({
24 | language: 'en',
25 | languages: [
26 | { code: 'en', name: 'English' },
27 | { code: 'es', name: 'Español' },
28 | { code: 'fr', name: 'Français' },
29 | { code: 'de', name: 'Deutsch' },
30 | { code: 'zh', name: '中文' },
31 | ],
32 | translations: {},
33 | loading: false,
34 | error: null,
35 |
36 | setLanguage: (language: string) => {
37 | set({ language });
38 | StorageService.set('language', language);
39 | get().loadTranslations(language);
40 | },
41 |
42 | loadTranslations: async (language: string) => {
43 | set({ loading: true, error: null });
44 |
45 | try {
46 | // In a real app, this would load from your API or static files
47 | const response = await ApiService.get>(
48 | `/translations/${language}.json`
49 | );
50 |
51 | set({
52 | translations: response.data,
53 | loading: false,
54 | error: null,
55 | });
56 |
57 | LoggerService.info(`Loaded translations for language: ${language}`);
58 | } catch (error: any) {
59 | LoggerService.error(`Failed to load translations for language: ${language}`, error);
60 |
61 | // Fallback to embedded basic translations
62 | const basicTranslations: Record> = {
63 | en: {
64 | 'common.welcome': 'Welcome',
65 | 'common.login': 'Login',
66 | 'common.logout': 'Logout',
67 | 'common.register': 'Register',
68 | 'common.save': 'Save',
69 | 'common.cancel': 'Cancel',
70 | 'common.error': 'An error occurred',
71 | },
72 | es: {
73 | 'common.welcome': 'Bienvenido',
74 | 'common.login': 'Iniciar sesión',
75 | 'common.logout': 'Cerrar sesión',
76 | 'common.register': 'Registrarse',
77 | 'common.save': 'Guardar',
78 | 'common.cancel': 'Cancelar',
79 | 'common.error': 'Ocurrió un error',
80 | },
81 | };
82 |
83 | set({
84 | translations: basicTranslations[language] || basicTranslations['en'],
85 | loading: false,
86 | error: 'Failed to load translations from server, using fallback',
87 | });
88 | }
89 | },
90 |
91 | t: (key: string, options?: Record): string => {
92 | const translations = get().translations;
93 | const value = translations[key] || key;
94 |
95 | if (options) {
96 | return Object.entries(options).reduce((acc, [k, v]) => {
97 | return acc.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v));
98 | }, value);
99 | }
100 |
101 | return value;
102 | },
103 | }),
104 | {
105 | name: 'i18n-storage',
106 | partialize: (state) => ({ language: state.language }),
107 | }
108 | )
109 | )
110 | );
--------------------------------------------------------------------------------
/src/stores/notificationStore.ts:
--------------------------------------------------------------------------------
1 | // src/stores/notificationStore.ts
2 | import { create } from 'zustand';
3 | import { devtools } from 'zustand/middleware';
4 | import { v4 as uuidv4 } from 'uuid';
5 |
6 | export type NotificationType = 'success' | 'error' | 'warning' | 'info';
7 |
8 | export interface Notification {
9 | id: string;
10 | title: string;
11 | message: string;
12 | type: NotificationType;
13 | read: boolean;
14 | createdAt: Date;
15 | autoClose?: boolean;
16 | duration?: number;
17 | }
18 |
19 | interface NotificationState {
20 | notifications: Notification[];
21 | addNotification: (notification: Omit) => string;
22 | removeNotification: (id: string) => void;
23 | clearNotifications: () => void;
24 | markAsRead: (id: string) => void;
25 | markAllAsRead: () => void;
26 | }
27 |
28 | export const useNotificationStore = create()(
29 | devtools((set, get) => ({
30 | notifications: [],
31 |
32 | addNotification: (notification) => {
33 | const id = uuidv4();
34 | const fullNotification: Notification = {
35 | id,
36 | read: false,
37 | createdAt: new Date(),
38 | ...notification,
39 | };
40 |
41 | set((state) => ({
42 | notifications: [fullNotification, ...state.notifications],
43 | }));
44 |
45 | // Auto close notification if specified
46 | if (notification.autoClose !== false) {
47 | const duration = notification.duration || 5000; // Default 5 seconds
48 | setTimeout(() => {
49 | get().removeNotification(id);
50 | }, duration);
51 | }
52 |
53 | return id;
54 | },
55 |
56 | removeNotification: (id) => {
57 | set((state) => ({
58 | notifications: state.notifications.filter((n) => n.id !== id),
59 | }));
60 | },
61 |
62 | clearNotifications: () => {
63 | set({ notifications: [] });
64 | },
65 |
66 | markAsRead: (id) => {
67 | set((state) => ({
68 | notifications: state.notifications.map((n) =>
69 | n.id === id ? { ...n, read: true } : n
70 | ),
71 | }));
72 | },
73 |
74 | markAllAsRead: () => {
75 | set((state) => ({
76 | notifications: state.notifications.map((n) => ({ ...n, read: true })),
77 | }));
78 | },
79 | }))
80 | );
--------------------------------------------------------------------------------
/src/stores/themeStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 | import { devtools, persist } from 'zustand/middleware';
3 | import { ThemeType } from '@/types/theme.types';
4 | import { StorageService } from '@/services/StorageService';
5 |
6 | interface ThemeState {
7 | currentTheme: ThemeType;
8 | systemTheme: ThemeType;
9 | setTheme: (theme: ThemeType) => void;
10 | toggleTheme: () => void;
11 | detectSystemTheme: () => ThemeType;
12 | }
13 |
14 | export const useThemeStore = create()(
15 | devtools(
16 | persist(
17 | (set, get) => ({
18 | currentTheme: 'system',
19 | systemTheme: 'light',
20 |
21 | setTheme: (theme: ThemeType) => {
22 | set({ currentTheme: theme });
23 | StorageService.set('theme', theme);
24 |
25 | // Apply theme to document
26 | if (theme === 'system') {
27 | const systemTheme = get().detectSystemTheme();
28 | document.documentElement.classList.toggle('dark', systemTheme === 'dark');
29 | } else {
30 | document.documentElement.classList.toggle('dark', theme === 'dark');
31 | }
32 | },
33 |
34 | toggleTheme: () => {
35 | const { currentTheme, detectSystemTheme } = get();
36 |
37 | if (currentTheme === 'light') {
38 | get().setTheme('dark');
39 | } else if (currentTheme === 'dark') {
40 | get().setTheme('light');
41 | } else {
42 | // If system, toggle based on system preference
43 | const systemTheme = detectSystemTheme();
44 | get().setTheme(systemTheme === 'dark' ? 'light' : 'dark');
45 | }
46 | },
47 |
48 | detectSystemTheme: (): ThemeType => {
49 | const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
50 | const systemTheme = isDark ? 'dark' : 'light';
51 | set({ systemTheme });
52 | return systemTheme;
53 | }
54 | }),
55 | {
56 | name: 'theme-storage',
57 | }
58 | )
59 | )
60 | );
--------------------------------------------------------------------------------
/src/types/auth.types.ts:
--------------------------------------------------------------------------------
1 | // src/types/auth.types.ts
2 | export enum UserRole {
3 | ADMIN = 'ADMIN',
4 | EDITOR = 'EDITOR',
5 | VIEWER = 'VIEWER',
6 | }
7 |
8 | export type Permission =
9 | | 'CREATE_CONTENT'
10 | | 'EDIT_CONTENT'
11 | | 'DELETE_CONTENT'
12 | | 'MANAGE_USERS'
13 | | 'VIEW_ANALYTICS'
14 | | 'VIEW_CONTENT';
15 |
16 | export interface UserPreferences {
17 | theme?: 'light' | 'dark' | 'system';
18 | language?: string;
19 | notifications?: boolean;
20 | emailNotifications?: boolean;
21 | }
22 |
23 | export interface User {
24 | id: string;
25 | username: string;
26 | email: string;
27 | firstName?: string;
28 | lastName?: string;
29 | avatar?: string;
30 | role: UserRole;
31 | permissions: Permission[];
32 | preferences: UserPreferences;
33 | createdAt?: string;
34 | updatedAt?: string;
35 | }
36 |
37 | export interface LoginCredentials {
38 | email: string;
39 | password: string;
40 | rememberMe?: boolean;
41 | }
42 |
43 | export interface RegisterData {
44 | username: string;
45 | email: string;
46 | password: string;
47 | firstName?: string;
48 | lastName?: string;
49 | confirmPassword?: string;
50 | }
51 |
52 | export interface ResetPasswordData {
53 | email: string;
54 | token: string;
55 | password: string;
56 | confirmPassword: string;
57 | }
58 |
59 | export interface VerifyEmailData {
60 | email: string;
61 | token: string;
62 | }
63 |
64 | export interface AuthState {
65 | user: User | null;
66 | isAuthenticated: boolean;
67 | isLoading: boolean;
68 | error: string | null;
69 | }
--------------------------------------------------------------------------------
/src/types/common.types.ts:
--------------------------------------------------------------------------------
1 | // src/types/common.types.ts
2 | import { ReactNode, LazyExoticComponent, ComponentType } from 'react';
3 |
4 | export type RouteGuard = (isAuthenticated: boolean) => boolean;
5 |
6 | export interface RouteMeta {
7 | title?: string;
8 | description?: string;
9 | auth?: boolean;
10 | requiredPermissions?: string[];
11 | layout?: string;
12 | }
13 |
14 | export interface RouteConfig {
15 | path: string;
16 | element: ComponentType | LazyExoticComponent;
17 | guards?: RouteGuard[];
18 | meta: RouteMeta;
19 | children?: RouteConfig[];
20 | redirectTo?: string;
21 | }
22 |
23 | export interface BreadcrumbItem {
24 | label: string;
25 | path?: string;
26 | icon?: ReactNode;
27 | }
28 |
29 | export interface MenuItem {
30 | key: string;
31 | label: string;
32 | icon?: ReactNode;
33 | path?: string;
34 | children?: MenuItem[];
35 | permissions?: string[];
36 | }
37 |
38 | export interface TableColumn {
39 | key: string;
40 | title: string;
41 | dataIndex: string;
42 | render?: (text: any, record: any) => ReactNode;
43 | sorter?: boolean | ((a: any, b: any) => number);
44 | filters?: { text: string; value: string }[];
45 | onFilter?: (value: string, record: any) => boolean;
46 | width?: number | string;
47 | }
48 |
49 | export interface TablePagination {
50 | current: number;
51 | pageSize: number;
52 | total: number;
53 | onChange: (page: number, pageSize: number) => void;
54 | }
55 |
56 | export interface ChartOptions {
57 | title?: string;
58 | xAxis?: {
59 | type: string;
60 | name?: string;
61 | };
62 | yAxis?: {
63 | type: string;
64 | name?: string;
65 | };
66 | series?: any[];
67 | tooltip?: any;
68 | legend?: any;
69 | }
70 |
71 | export interface UploadOptions {
72 | accept?: string;
73 | multiple?: boolean;
74 | maxSize?: number;
75 | maxCount?: number;
76 | }
77 |
78 | export interface UploadedFile {
79 | uid: string;
80 | name: string;
81 | size: number;
82 | type: string;
83 | url?: string;
84 | status: 'uploading' | 'done' | 'error';
85 | percent?: number;
86 | error?: any;
87 | }
88 |
89 | export interface FormConfig {
90 | fields: FormField[];
91 | layout?: 'horizontal' | 'vertical' | 'inline';
92 | labelWidth?: number | string;
93 | submitText?: string;
94 | cancelText?: string;
95 | resetText?: string;
96 | }
97 |
98 | export interface FormField {
99 | name: string;
100 | label?: string;
101 | type: 'text' | 'textarea' | 'number' | 'select' | 'checkbox' | 'radio' | 'date' | 'file' | 'custom';
102 | placeholder?: string;
103 | defaultValue?: any;
104 | options?: { label: string; value: any }[];
105 | rules?: FormRule[];
106 | disabled?: boolean;
107 | hidden?: boolean;
108 | span?: number;
109 | component?: ReactNode;
110 | }
111 |
112 | export interface FormRule {
113 | required?: boolean;
114 | min?: number;
115 | max?: number;
116 | pattern?: RegExp;
117 | message?: string;
118 | validator?: (value: any, formValues: any) => Promise | void;
119 | }
--------------------------------------------------------------------------------
/src/types/i18n.types.ts:
--------------------------------------------------------------------------------
1 | // src/types/i18n.types.ts
2 | export interface Language {
3 | code: string;
4 | name: string;
5 | flag?: string;
6 | rtl?: boolean;
7 | }
8 |
9 | export interface TranslationEntry {
10 | key: string;
11 | defaultValue: string;
12 | description?: string;
13 | }
14 |
15 | export interface TranslationNamespace {
16 | name: string;
17 | translations: TranslationEntry[];
18 | }
--------------------------------------------------------------------------------
/src/types/theme.types.ts:
--------------------------------------------------------------------------------
1 | // src/types/theme.types.ts
2 | export type ThemeType = 'light' | 'dark' | 'system';
3 |
4 | export interface ThemeConfig {
5 | primaryColor: string;
6 | secondaryColor: string;
7 | backgroundColor: string;
8 | textColor: string;
9 | borderRadius: string;
10 | fontFamily: string;
11 | }
12 |
13 | export interface ThemePreset {
14 | name: string;
15 | type: ThemeType;
16 | config: ThemeConfig;
17 | }
--------------------------------------------------------------------------------
/src/types/vite-plugins.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'vite-plugin-prerender' {
2 | export interface PrerenderOption {
3 | routes: string[];
4 | staticDir?: string;
5 | postProcess?: (renderedRoute: any) => any;
6 | minify?: boolean;
7 | renderer?: any;
8 | puppeteerOptions?: any;
9 | server?: any;
10 | entryPath?: string;
11 | }
12 |
13 | const prerender: (options?: PrerenderOption) => any;
14 | export default prerender;
15 | }
16 |
17 | declare module 'vite-plugin-html' {
18 | export interface HtmlPluginOption {
19 | minify?: boolean;
20 | inject?: {
21 | data?: Record;
22 | tags?: Array;
23 | };
24 | template?: string;
25 | entry?: string;
26 | viteNext?: boolean;
27 | }
28 |
29 | export function createHtmlPlugin(options?: HtmlPluginOption): any;
30 | }
31 |
32 | declare module 'vite-imagetools' {
33 | export interface ImagetoolsOptions {
34 | defaultDirectives?: URLSearchParams;
35 | resolveFrom?: 'root' | 'source';
36 | removeMetadata?: boolean;
37 | extendOutputFormats?: Record;
38 | extendTransforms?: Record;
39 | logLevel?: 'info' | 'warn' | 'error' | 'silent';
40 | }
41 |
42 | export function imagetools(options?: ImagetoolsOptions): any;
43 | }
--------------------------------------------------------------------------------
/src/utils/performance.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | }
9 |
--------------------------------------------------------------------------------
/template_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "This is a shadcn-ui template. It includes a basic project structure with directories such as src/hooks, src/lib, and src/components. The src/components directory contains all the shadcn-ui components, with no additional installation commands required. If it's not necessary do not modify files outside the src directory. If it's not necessary do not modify src/index.css.",
3 | "required_fields": [],
4 | "required_files": [
5 | "README.md",
6 | "src/App.tsx",
7 | "src/index.css",
8 | "src/App.css",
9 | "src/main.tsx"
10 | ],
11 | "lang": "typescript",
12 | "framework": "react",
13 | "style": "shadcn/ui",
14 | "scene": "Personal Demonstration Template"
15 | }
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 | "target": "ES2020",
5 | "useDefineForClassFields": true,
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "isolatedModules": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "noUncheckedSideEffectImports": true,
24 |
25 | "baseUrl": ".",
26 | "paths": {
27 | "@/*": ["./src/*"]
28 | }
29 | },
30 | "include": ["src"]
31 | }
32 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
4 | "compilerOptions": {
5 | "baseUrl": ".",
6 | "paths": {
7 | "@/*": ["./src/*"]
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4 | "target": "ES2022",
5 | "lib": ["ES2023"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "noUncheckedSideEffectImports": true
22 | },
23 | "include": ["vite.config.ts"]
24 | }
25 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 | import tailwindcss from '@tailwindcss/vite';
4 | import path from 'path';
5 | import { VitePWA } from 'vite-plugin-pwa';
6 | import tsconfigPaths from 'vite-tsconfig-paths';
7 | import { imagetools } from 'vite-imagetools';
8 |
9 | // https://vitejs.dev/config/
10 | export default defineConfig({
11 | plugins: [
12 | react({
13 | // 配置 babel 以确保正确处理 JSX
14 | babel: {
15 | babelrc: false,
16 | configFile: false,
17 | // 使用项目中已安装的插件
18 | plugins: []
19 | },
20 | }),
21 | tailwindcss(),
22 | tsconfigPaths(),
23 | // 图片优化插件
24 | imagetools({
25 | defaultDirectives: (url) => {
26 | // 为不同尺寸生成响应式图片
27 | if (url.searchParams.has('responsive')) {
28 | return new URLSearchParams({
29 | format: 'webp;jpg',
30 | w: '480;768;1024;1280;1920',
31 | quality: '75'
32 | });
33 | }
34 | // 默认优化
35 | return new URLSearchParams({
36 | format: 'webp',
37 | quality: '75'
38 | });
39 | }
40 | }),
41 | VitePWA({
42 | registerType: 'autoUpdate',
43 | includeAssets: ['favicon.svg', 'robots.txt', 'apple-touch-icon.png'],
44 | manifest: {
45 | name: 'ReactUltra',
46 | short_name: 'ReactUltra',
47 | description: 'Enterprise-grade React Application Template',
48 | theme_color: '#ffffff',
49 | icons: [
50 | {
51 | src: 'pwa-192x192.png',
52 | sizes: '192x192',
53 | type: 'image/png'
54 | },
55 | {
56 | src: 'pwa-512x512.png',
57 | sizes: '512x512',
58 | type: 'image/png',
59 | purpose: 'any maskable'
60 | }
61 | ]
62 | },
63 | workbox: {
64 | runtimeCaching: [
65 | {
66 | urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
67 | handler: 'CacheFirst',
68 | options: {
69 | cacheName: 'google-fonts-cache',
70 | expiration: {
71 | maxEntries: 10,
72 | maxAgeSeconds: 60 * 60 * 24 * 365 // 1 年
73 | },
74 | cacheableResponse: {
75 | statuses: [0, 200]
76 | }
77 | }
78 | },
79 | // 添加图片缓存策略
80 | {
81 | urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp)$/,
82 | handler: 'CacheFirst',
83 | options: {
84 | cacheName: 'images-cache',
85 | expiration: {
86 | maxEntries: 50,
87 | maxAgeSeconds: 60 * 60 * 24 * 30 // 30 天
88 | }
89 | }
90 | }
91 | ]
92 | }
93 | })
94 | ],
95 | resolve: {
96 | alias: {
97 | '@': path.resolve(__dirname, './src'),
98 | },
99 | },
100 | server: {
101 | port: 3000,
102 | open: true,
103 | cors: true,
104 | hmr: {
105 | overlay: true
106 | }
107 | },
108 | build: {
109 | reportCompressedSize: true,
110 | chunkSizeWarningLimit: 1000,
111 | // 减小构建体积,移除调试信息
112 | minify: 'terser',
113 | terserOptions: {
114 | compress: {
115 | drop_console: true,
116 | drop_debugger: true
117 | }
118 | },
119 | // 构建性能优化
120 | rollupOptions: {
121 | output: {
122 | manualChunks: (id) => {
123 | // 更精细的代码分割策略
124 | if (id.includes('node_modules')) {
125 | if (id.includes('react') || id.includes('react-dom')) {
126 | return 'vendor-react';
127 | }
128 | if (id.includes('react-router')) {
129 | return 'vendor-router';
130 | }
131 | if (id.includes('@radix-ui')) {
132 | return 'vendor-radix';
133 | }
134 | if (id.includes('antd')) {
135 | return 'vendor-antd';
136 | }
137 | if (id.includes('echarts')) {
138 | return 'vendor-echarts';
139 | }
140 | if (id.includes('zustand')) {
141 | return 'vendor-zustand';
142 | }
143 | if (id.includes('i18next')) {
144 | return 'vendor-i18n';
145 | }
146 | // 其余的第三方库打包到一起
147 | return 'vendor-others';
148 | }
149 | // 按功能模块拆分业务代码
150 | if (id.includes('/src/pages/auth/')) {
151 | return 'auth';
152 | }
153 | if (id.includes('/src/components/ui/')) {
154 | return 'ui';
155 | }
156 | }
157 | }
158 | },
159 | // 开启 source map 用于生产环境调试
160 | sourcemap: true,
161 | // CSS 代码分割
162 | cssCodeSplit: true
163 | },
164 | // 优化预构建依赖
165 | optimizeDeps: {
166 | include: [
167 | 'react',
168 | 'react-dom',
169 | 'react-router-dom',
170 | 'zustand',
171 | '@radix-ui/react-dialog',
172 | 'tailwind-merge'
173 | ],
174 | exclude: ['@vite/client', '@vite/env']
175 | }
176 | });
177 |
--------------------------------------------------------------------------------
|