create-pardx-scaffold 0.1.1 → 0.1.3

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.
Files changed (79) hide show
  1. package/package.json +1 -1
  2. package/template/.cursor/worktrees.json +37 -0
  3. package/template/.dockerignore +49 -0
  4. package/template/.mcp.json +26 -0
  5. package/template/.nvmrc +1 -0
  6. package/template/CLAUDE.md +85 -0
  7. package/template/apps/api/libs/domain/services/index.ts +7 -0
  8. package/template/apps/api/libs/{infra/shared-services → domain/services}/ip-info/ip-info.module.ts +2 -0
  9. package/template/apps/api/libs/{infra/shared-services → domain/services}/ip-info/ip-info.service.ts +2 -0
  10. package/template/apps/api/libs/infra/clients/internal/file-storage/dto/file.dto.ts +1 -1
  11. package/template/apps/api/libs/infra/common/ts-rest/response.helper.ts +9 -1
  12. package/template/apps/api/libs/infra/shared-services/email/email.module.ts +0 -2
  13. package/template/apps/api/libs/infra/shared-services/file-storage/bucket-resolver.ts +1 -1
  14. package/template/apps/api/libs/infra/shared-services/file-storage/file-storage.module.ts +1 -1
  15. package/template/apps/api/libs/infra/shared-services/sms/sms.module.ts +0 -2
  16. package/template/apps/api/libs/infra/shared-services/uploader/uploader.service.ts +4 -4
  17. package/template/apps/api/package.json +15 -15
  18. package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
  19. package/template/apps/api/src/app.module.ts +5 -1
  20. package/template/apps/api/src/modules/uploader/uploader.controller.ts +305 -0
  21. package/template/apps/api/src/modules/uploader/uploader.module.ts +17 -0
  22. package/template/apps/web/.env.example +6 -4
  23. package/template/apps/web/components/error-boundary.tsx +166 -0
  24. package/template/apps/web/components/index.ts +10 -0
  25. package/template/apps/web/components.json +20 -0
  26. package/template/apps/web/config.ts +115 -0
  27. package/template/apps/web/eslint.config.mjs +4 -0
  28. package/template/apps/web/lib/api/avatar-upload.ts +1 -0
  29. package/template/apps/web/lib/api/contracts/client.ts +51 -30
  30. package/template/apps/web/lib/api/contracts/hooks/index.ts +0 -3
  31. package/template/apps/web/lib/api/contracts/hooks/notification.ts +42 -124
  32. package/template/apps/web/lib/api.ts +24 -1
  33. package/template/apps/web/lib/dynamic-import.tsx +121 -0
  34. package/template/apps/web/lib/logger.ts +113 -0
  35. package/template/apps/web/lib/upload/api.ts +37 -105
  36. package/template/apps/web/lib/upload/batch-uploader.ts +7 -74
  37. package/template/apps/web/lib/upload/uploader.ts +10 -74
  38. package/template/apps/web/locales/zh-CN/assessment.json +5 -0
  39. package/template/apps/web/locales/zh-CN/chat.json +6 -0
  40. package/template/apps/web/locales/zh-CN/common.json +38 -0
  41. package/template/apps/web/locales/zh-CN/creative.json +5 -0
  42. package/template/apps/web/locales/zh-CN/daily-challenge.json +6 -0
  43. package/template/apps/web/locales/zh-CN/errors.json +16 -0
  44. package/template/apps/web/locales/zh-CN/forms.json +18 -0
  45. package/template/apps/web/locales/zh-CN/memory.json +5 -0
  46. package/template/apps/web/locales/zh-CN/navigation.json +12 -0
  47. package/template/apps/web/locales/zh-CN/recommendation.json +5 -0
  48. package/template/apps/web/locales/zh-CN/recruitment.json +5 -0
  49. package/template/apps/web/locales/zh-CN/settings.json +7 -0
  50. package/template/apps/web/locales/zh-CN/subscription.json +6 -0
  51. package/template/apps/web/locales/zh-CN/validation.json +8 -0
  52. package/template/apps/web/package.json +14 -15
  53. package/template/apps/web/postcss.config.mjs +1 -0
  54. package/template/apps/web/proxy.ts +102 -0
  55. package/template/apps/web/public/logo.svg +21 -0
  56. package/template/apps/web/vitest.config.ts +69 -0
  57. package/template/apps/web/vitest.setup.ts +80 -0
  58. package/template/package.json +7 -7
  59. package/template/packages/constants/package.json +3 -1
  60. package/template/packages/constants/tsconfig.build.esm.json +8 -0
  61. package/template/packages/contracts/package.json +2 -2
  62. package/template/packages/contracts/src/schemas/uploader.schema.ts +33 -10
  63. package/template/packages/ui/.storybook/main.ts +28 -0
  64. package/template/packages/ui/.storybook/preview.ts +40 -0
  65. package/template/packages/ui/eslint.config.js +3 -0
  66. package/template/packages/ui/package.json +15 -2
  67. package/template/packages/ui/src/components/button.stories.tsx +171 -0
  68. package/template/packages/ui/src/styles/globals.css +1 -1
  69. package/template/packages/ui/tsconfig.json +1 -1
  70. package/template/packages/utils/package.json +2 -2
  71. package/template/packages/utils/tsconfig.build.esm.json +8 -0
  72. package/template/packages/validators/package.json +1 -1
  73. package/template/pnpm-lock.yaml +2263 -999
  74. package/template/scripts/export-scaffold-for-create.js +65 -0
  75. package/template/apps/api/libs/infra/utils/download.ts +0 -21
  76. package/template/apps/web/lib/api/client.ts +0 -649
  77. package/template/apps/web/lib/audio-buffer-queue.ts +0 -273
  78. package/template/apps/web/lib/upload/folder-utils.ts +0 -295
  79. /package/template/apps/api/libs/{infra/shared-services → domain/services}/ip-info/index.ts +0 -0
@@ -41,6 +41,54 @@ import {
41
41
 
42
42
  const API_BASE_URL = API_CONFIG.baseUrl;
43
43
 
44
+ // ============================================================================
45
+ // 错误消息去重机制(模块级别缓存)
46
+ // 在短时间内(2秒)相同的错误消息只显示一次
47
+ // ============================================================================
48
+ const errorMessageCache = new Map<string, number>();
49
+ const ERROR_DEDUP_INTERVAL = 2000; // 2秒
50
+ const MAX_CACHE_SIZE = 50; // 最大缓存数量
51
+
52
+ /**
53
+ * 清理过期的错误缓存
54
+ */
55
+ const cleanupErrorCache = () => {
56
+ const now = Date.now();
57
+ for (const [key, timestamp] of errorMessageCache.entries()) {
58
+ if (now - timestamp >= ERROR_DEDUP_INTERVAL) {
59
+ errorMessageCache.delete(key);
60
+ }
61
+ }
62
+ // 如果清理后仍超过限制,删除最旧的项
63
+ if (errorMessageCache.size > MAX_CACHE_SIZE) {
64
+ const oldestKey = errorMessageCache.keys().next().value;
65
+ if (oldestKey) errorMessageCache.delete(oldestKey);
66
+ }
67
+ };
68
+
69
+ /**
70
+ * 检查错误消息是否应该被去重(跳过显示)
71
+ */
72
+ const shouldDeduplicateError = (errorKey: string): boolean => {
73
+ const now = Date.now();
74
+ const lastShown = errorMessageCache.get(errorKey);
75
+
76
+ // 如果在去重时间窗口内已经显示过相同的错误,则跳过
77
+ if (lastShown && now - lastShown < ERROR_DEDUP_INTERVAL) {
78
+ return true;
79
+ }
80
+
81
+ // 记录当前错误消息的显示时间
82
+ errorMessageCache.set(errorKey, now);
83
+
84
+ // 清理过期缓存
85
+ if (errorMessageCache.size > MAX_CACHE_SIZE) {
86
+ cleanupErrorCache();
87
+ }
88
+
89
+ return false;
90
+ };
91
+
44
92
  /**
45
93
  * Base fetch function with standard headers (no auth check)
46
94
  * Used for public endpoints like login, register
@@ -136,13 +184,7 @@ const baseFetch = async (args: ApiFetcherArgs, requireAuth: boolean = true) => {
136
184
  throw new VersionMismatchError(minBuild);
137
185
  }
138
186
 
139
- // 错误消息去重机制:记录最近显示的错误消息和时间戳
140
- // 在短时间内(2秒)相同的错误消息只显示一次
141
- const errorMessageCache = new Map<string, number>();
142
- const ERROR_DEDUP_INTERVAL = 2000; // 2秒
143
- const MAX_CACHE_SIZE = 50; // 最大缓存数量
144
-
145
- // Global error handling
187
+ // Global error handling with deduplication
146
188
  const handleError = (errorMsg: string, statusCode: number) => {
147
189
  if (typeof window === 'undefined') return;
148
190
 
@@ -150,42 +192,21 @@ const baseFetch = async (args: ApiFetcherArgs, requireAuth: boolean = true) => {
150
192
 
151
193
  // 如果已经在登录页面,不需要全局错误处理,由登录组件自己处理
152
194
  if (pathname === '/login' || pathname.endsWith('/login')) {
153
- // 登录页面的错误(如密码错误、401 等)由登录组件自己处理,不显示 toast
154
195
  return;
155
196
  }
156
197
 
157
198
  // 创建错误消息的唯一标识(包含消息内容和状态码)
158
199
  const errorKey = `${statusCode}:${errorMsg}`;
159
- const now = Date.now();
160
- const lastShown = errorMessageCache.get(errorKey);
161
200
 
162
- // 如果在去重时间窗口内已经显示过相同的错误,则跳过
163
- if (lastShown && now - lastShown < ERROR_DEDUP_INTERVAL) {
201
+ // 使用模块级别的去重机制
202
+ if (shouldDeduplicateError(errorKey)) {
164
203
  return;
165
204
  }
166
205
 
167
- // 记录当前错误消息的显示时间
168
- errorMessageCache.set(errorKey, now);
169
-
170
- // 清理过期缓存:达到最大数量时清理所有过期项
171
- if (errorMessageCache.size > MAX_CACHE_SIZE) {
172
- for (const [key, timestamp] of errorMessageCache.entries()) {
173
- if (now - timestamp >= ERROR_DEDUP_INTERVAL) {
174
- errorMessageCache.delete(key);
175
- }
176
- }
177
- // 如果清理后仍超过限制,删除最旧的项
178
- if (errorMessageCache.size > MAX_CACHE_SIZE) {
179
- const oldestKey = errorMessageCache.keys().next().value;
180
- if (oldestKey) errorMessageCache.delete(oldestKey);
181
- }
182
- }
183
-
184
206
  // Handle 401 Unauthorized - clear storage and redirect to login
185
207
  if (statusCode === 401) {
186
208
  clearToken();
187
209
  toast.error(errorMsg || '登录已过期,请重新登录');
188
- // 重定向到登录页(公开页面在上面已经提前返回,不会执行到这里)
189
210
  window.location.href = '/login';
190
211
  } else {
191
212
  // Show error toast for other error codes
@@ -15,9 +15,6 @@ export {
15
15
  settingKeys,
16
16
  useSaveAccount,
17
17
  useUpdateAvatar,
18
- useGetUsage,
19
- useGetBranding,
20
- useSaveBranding,
21
18
  useSendVerifyEmail,
22
19
  useSetPassword,
23
20
  useBindEmail,
@@ -4,62 +4,20 @@
4
4
  * Notification API Hooks
5
5
  * 基于 ts-rest 契约的通知 API Hooks
6
6
  *
7
- * 性能优化:
8
- * - 懒加载模式:支持 enabled 选项,仅在需要时加载数据
9
- * - 减少初始 API 调用和 SSE 数据压力
10
- *
11
- * @example
12
- * ```tsx
13
- * import {
14
- * useNotifications,
15
- * useUnreadNotificationCount,
16
- * useMarkNotificationRead,
17
- * useMarkAllNotificationsRead,
18
- * } from '@/lib/api/contracts/hooks/notification';
19
- *
20
- * function NotificationPanel() {
21
- * const [open, setOpen] = useState(false);
22
- * // 懒加载:仅当面板打开时加载通知列表
23
- * const { data, isLoading } = useNotifications({ limit: 20 }, { enabled: open });
24
- * // 未读数量始终加载,用于显示徽章
25
- * const { data: unreadCount } = useUnreadNotificationCount();
26
- * const markReadMutation = useMarkNotificationRead();
27
- * const markAllReadMutation = useMarkAllNotificationsRead();
28
- *
29
- * return (
30
- * <div>
31
- * <p>未读: {unreadCount?.total}</p>
32
- * {data?.list.map(notification => (
33
- * <div key={notification.id} onClick={() => {
34
- * markReadMutation.mutate({ notificationIds: [notification.id] });
35
- * }}>
36
- * {notification.content.type}
37
- * </div>
38
- * ))}
39
- * <button onClick={() => markAllReadMutation.mutate({})}>
40
- * 全部标为已读
41
- * </button>
42
- * </div>
43
- * );
44
- * }
45
- * ```
7
+ * 注意:此文件是脚手架占位符。
8
+ * 实际项目中需要在 @repo/contracts 中定义 notificationContract,
9
+ * 并在 client.ts 中导出 notificationApi notificationClient。
46
10
  */
47
11
 
48
- import { useQuery, useQueryClient } from '@tanstack/react-query';
49
- import { notificationApi, notificationClient } from '../client';
50
- import type {
51
- MessageNotificationType,
52
- GetNotificationListRequest,
53
- } from '@repo/contracts';
54
-
55
12
  // ============================================================================
56
13
  // Query Keys
57
14
  // ============================================================================
58
15
 
59
16
  export const notificationKeys = {
60
17
  all: ['notifications'] as const,
61
- list: (params?: Partial<GetNotificationListRequest>) =>
18
+ list: (params?: Record<string, unknown>) =>
62
19
  [...notificationKeys.all, 'list', params] as const,
20
+ unreadCount: () => [...notificationKeys.all, 'unreadCount'] as const,
63
21
  };
64
22
 
65
23
  // ============================================================================
@@ -67,7 +25,7 @@ export const notificationKeys = {
67
25
  // ============================================================================
68
26
 
69
27
  export interface NotificationsParams {
70
- type?: MessageNotificationType;
28
+ type?: string;
71
29
  isRead?: boolean;
72
30
  limit?: number;
73
31
  page?: number;
@@ -79,102 +37,62 @@ export interface NotificationsOptions {
79
37
  }
80
38
 
81
39
  // ============================================================================
82
- // Query Hooks
40
+ // Placeholder Hooks
41
+ // 这些 hooks 是占位符,需要在实际项目中实现
83
42
  // ============================================================================
84
43
 
85
44
  /**
86
- * 获取通知列表
87
- *
88
- * 使用 React Query 原生 useQuery 实现 enabled 选项,
89
- * 支持懒加载模式,仅在需要时加载数据
90
- *
91
- * @param params - 查询参数(筛选条件和分页)
92
- * @param options - 可选配置项,支持 enabled 懒加载
93
- *
94
- * @example
95
- * ```tsx
96
- * // 懒加载:仅当面板打开时才加载列表
97
- * const { data, isLoading, refetch } = useNotifications(
98
- * { limit: 20 },
99
- * { enabled: isPanelOpen }
100
- * );
101
- * ```
45
+ * 获取通知列表 (占位符)
46
+ * TODO: 实现 notificationContract 后启用
102
47
  */
103
48
  export function useNotifications(
104
- params?: NotificationsParams,
105
- options?: NotificationsOptions,
49
+ _params?: NotificationsParams,
50
+ _options?: NotificationsOptions,
106
51
  ) {
107
- const queryParams = {
108
- type: params?.type,
109
- isRead: params?.isRead,
110
- limit: params?.limit || 20,
111
- page: params?.page || 1,
52
+ // 占位符实现
53
+ return {
54
+ data: undefined,
55
+ isLoading: false,
56
+ error: null,
57
+ refetch: () => Promise.resolve(),
112
58
  };
113
-
114
- const isEnabled = options?.enabled ?? true;
115
-
116
- // 使用 React Query 原生 useQuery,正确支持 enabled 选项
117
- return useQuery({
118
- queryKey: notificationKeys.list(queryParams),
119
- queryFn: async () => {
120
- const response = await notificationClient.getList({
121
- query: queryParams,
122
- });
123
- return response;
124
- },
125
- staleTime: 30 * 1000, // 30 seconds
126
- enabled: isEnabled,
127
- });
128
59
  }
129
60
 
130
- // ============================================================================
131
- // Mutation Hooks
132
- // ============================================================================
133
-
134
61
  /**
135
- * 标记通知已读
62
+ * 标记通知已读 (占位符)
63
+ * TODO: 实现 notificationContract 后启用
136
64
  */
137
65
  export function useMarkNotificationRead() {
138
- const queryClient = useQueryClient();
139
-
140
- return notificationApi.markRead.useMutation({
141
- onSuccess: () => {
142
- // 刷新通知列表和未读数量
143
- queryClient.invalidateQueries({
144
- queryKey: notificationKeys.all,
145
- });
146
- },
147
- });
66
+ return {
67
+ mutate: (_params: { notificationIds: string[] }) => {},
68
+ mutateAsync: (_params: { notificationIds: string[] }) => Promise.resolve(),
69
+ isLoading: false,
70
+ isPending: false,
71
+ };
148
72
  }
149
73
 
150
74
  /**
151
- * 标记全部通知已读
75
+ * 标记全部通知已读 (占位符)
76
+ * TODO: 实现 notificationContract 后启用
152
77
  */
153
78
  export function useMarkAllNotificationsRead() {
154
- const queryClient = useQueryClient();
155
-
156
- return notificationApi.markAllRead.useMutation({
157
- onSuccess: () => {
158
- // 刷新通知列表和未读数量
159
- queryClient.invalidateQueries({
160
- queryKey: notificationKeys.all,
161
- });
162
- },
163
- });
79
+ return {
80
+ mutate: () => {},
81
+ mutateAsync: () => Promise.resolve(),
82
+ isLoading: false,
83
+ isPending: false,
84
+ };
164
85
  }
165
86
 
166
87
  /**
167
- * 删除通知
88
+ * 删除通知 (占位符)
89
+ * TODO: 实现 notificationContract 后启用
168
90
  */
169
91
  export function useDeleteNotification() {
170
- const queryClient = useQueryClient();
171
-
172
- return notificationApi.delete.useMutation({
173
- onSuccess: () => {
174
- // 刷新通知列表和未读数量
175
- queryClient.invalidateQueries({
176
- queryKey: notificationKeys.all,
177
- });
178
- },
179
- });
92
+ return {
93
+ mutate: (_params: { notificationId: string }) => {},
94
+ mutateAsync: (_params: { notificationId: string }) => Promise.resolve(),
95
+ isLoading: false,
96
+ isPending: false,
97
+ };
180
98
  }
@@ -44,6 +44,9 @@ export interface TokenData {
44
44
  // Token Management
45
45
  // ============================================================================
46
46
 
47
+ // Token 刷新队列:防止并发请求时多次刷新 token
48
+ let refreshPromise: Promise<LoginData> | null = null;
49
+
47
50
  // 获取存储的 token(使用新的存储模块)
48
51
  export function getToken(): string | null {
49
52
  return getAccessToken();
@@ -121,8 +124,10 @@ export async function login(credentials: LoginRequest): Promise<LoginData> {
121
124
  /**
122
125
  * 刷新 Token API (ts-rest)
123
126
  * 使用 signClient.refreshToken
127
+ *
128
+ * 注意:此函数内部使用,外部应使用 ensureValidToken
124
129
  */
125
- export async function refreshToken(): Promise<LoginData> {
130
+ async function doRefreshToken(): Promise<LoginData> {
126
131
  const refresh = getRefreshToken();
127
132
  if (!refresh) {
128
133
  throw new Error('没有可用的 refresh token');
@@ -149,6 +154,24 @@ export async function refreshToken(): Promise<LoginData> {
149
154
  return data;
150
155
  }
151
156
 
157
+ /**
158
+ * 刷新 Token(带队列机制)
159
+ * 防止并发请求时多次刷新 token
160
+ */
161
+ export async function refreshToken(): Promise<LoginData> {
162
+ // 如果已有刷新请求在进行中,等待它完成
163
+ if (refreshPromise) {
164
+ return refreshPromise;
165
+ }
166
+
167
+ // 创建刷新 Promise 并共享
168
+ refreshPromise = doRefreshToken().finally(() => {
169
+ refreshPromise = null;
170
+ });
171
+
172
+ return refreshPromise;
173
+ }
174
+
152
175
  /**
153
176
  * 确保 token 有效(如果过期则自动刷新)
154
177
  */
@@ -0,0 +1,121 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * 动态导入工具
5
+ *
6
+ * 提供代码分割和懒加载的工具函数
7
+ * 用于优化首屏加载性能
8
+ */
9
+ import dynamic from 'next/dynamic';
10
+ import type { ComponentType, ReactNode } from 'react';
11
+
12
+ /**
13
+ * 加载状态组件
14
+ */
15
+ function DefaultLoadingComponent() {
16
+ return (
17
+ <div className="flex items-center justify-center p-4">
18
+ <div className="h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-blue-600" />
19
+ </div>
20
+ );
21
+ }
22
+
23
+ /**
24
+ * 创建懒加载组件
25
+ *
26
+ * @param importFn - 动态导入函数
27
+ * @param options - 配置选项
28
+ * @returns 懒加载的组件
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * // 基本用法
33
+ * const HeavyChart = createLazyComponent(() => import('@/components/chart'));
34
+ *
35
+ * // 带自定义加载状态
36
+ * const Editor = createLazyComponent(
37
+ * () => import('@/components/editor'),
38
+ * { loading: <EditorSkeleton /> }
39
+ * );
40
+ *
41
+ * // 禁用 SSR(用于仅客户端组件)
42
+ * const ClientOnlyMap = createLazyComponent(
43
+ * () => import('@/components/map'),
44
+ * { ssr: false }
45
+ * );
46
+ * ```
47
+ */
48
+ export function createLazyComponent<P extends object>(
49
+ importFn: () => Promise<{ default: ComponentType<P> }>,
50
+ options: {
51
+ loading?: ReactNode;
52
+ ssr?: boolean;
53
+ } = {},
54
+ ) {
55
+ const { loading, ssr = true } = options;
56
+
57
+ return dynamic(importFn, {
58
+ loading: () => (loading ? <>{loading}</> : <DefaultLoadingComponent />),
59
+ ssr,
60
+ });
61
+ }
62
+
63
+ /**
64
+ * 预加载组件
65
+ *
66
+ * 在用户可能需要组件之前预先加载
67
+ * 例如:鼠标悬停在链接上时预加载目标页面的组件
68
+ *
69
+ * @param importFn - 动态导入函数
70
+ *
71
+ * @example
72
+ * ```tsx
73
+ * const loadEditor = () => import('@/components/editor');
74
+ *
75
+ * // 在鼠标悬停时预加载
76
+ * <button onMouseEnter={() => preloadComponent(loadEditor)}>
77
+ * 打开编辑器
78
+ * </button>
79
+ * ```
80
+ */
81
+ export function preloadComponent<T>(
82
+ importFn: () => Promise<T>,
83
+ ): Promise<T> | void {
84
+ if (typeof window !== 'undefined') {
85
+ return importFn();
86
+ }
87
+ }
88
+
89
+ /**
90
+ * 常用的懒加载组件示例
91
+ *
92
+ * 这些组件通常较大,适合懒加载:
93
+ * - 富文本编辑器
94
+ * - 图表库
95
+ * - 地图组件
96
+ * - 文件预览器
97
+ * - 代码编辑器
98
+ */
99
+
100
+ // 示例:懒加载图表组件(如果存在)
101
+ // export const LazyChart = createLazyComponent(
102
+ // () => import('@/components/chart'),
103
+ // { ssr: false }
104
+ // );
105
+
106
+ // 示例:懒加载编辑器组件(如果存在)
107
+ // export const LazyEditor = createLazyComponent(
108
+ // () => import('@/components/editor'),
109
+ // { ssr: false }
110
+ // );
111
+
112
+ // 示例:懒加载文件预览器(如果存在)
113
+ // export const LazyFilePreview = createLazyComponent(
114
+ // () => import('@/components/file-preview'),
115
+ // { ssr: false }
116
+ // );
117
+
118
+ export default {
119
+ createLazyComponent,
120
+ preloadComponent,
121
+ };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Frontend Logger
3
+ *
4
+ * 浏览器兼容的日志库,替代 console.log
5
+ * 支持日志级别控制和生产环境静默
6
+ */
7
+
8
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
9
+
10
+ interface LoggerConfig {
11
+ level: LogLevel;
12
+ prefix?: string;
13
+ enableInProduction?: boolean;
14
+ }
15
+
16
+ const LOG_LEVELS: Record<LogLevel, number> = {
17
+ debug: 0,
18
+ info: 1,
19
+ warn: 2,
20
+ error: 3,
21
+ };
22
+
23
+ class Logger {
24
+ private level: LogLevel;
25
+ private prefix: string;
26
+ private enableInProduction: boolean;
27
+
28
+ constructor(config: Partial<LoggerConfig> = {}) {
29
+ this.level = config.level ?? this.getDefaultLevel();
30
+ this.prefix = config.prefix ?? '[App]';
31
+ this.enableInProduction = config.enableInProduction ?? false;
32
+ }
33
+
34
+ private getDefaultLevel(): LogLevel {
35
+ if (typeof window === 'undefined') {
36
+ return 'info';
37
+ }
38
+ return process.env.NODE_ENV === 'production' ? 'warn' : 'debug';
39
+ }
40
+
41
+ private shouldLog(level: LogLevel): boolean {
42
+ // 在生产环境中,除非明确启用,否则只记录 warn 和 error
43
+ if (
44
+ process.env.NODE_ENV === 'production' &&
45
+ !this.enableInProduction &&
46
+ LOG_LEVELS[level] < LOG_LEVELS.warn
47
+ ) {
48
+ return false;
49
+ }
50
+ return LOG_LEVELS[level] >= LOG_LEVELS[this.level];
51
+ }
52
+
53
+ private formatMessage(level: LogLevel, message: string): string {
54
+ const timestamp = new Date().toISOString();
55
+ return `${timestamp} ${this.prefix} [${level.toUpperCase()}] ${message}`;
56
+ }
57
+
58
+ debug(message: string, ...args: unknown[]): void {
59
+ if (this.shouldLog('debug')) {
60
+ console.debug(this.formatMessage('debug', message), ...args);
61
+ }
62
+ }
63
+
64
+ info(message: string, ...args: unknown[]): void {
65
+ if (this.shouldLog('info')) {
66
+ console.info(this.formatMessage('info', message), ...args);
67
+ }
68
+ }
69
+
70
+ warn(message: string, ...args: unknown[]): void {
71
+ if (this.shouldLog('warn')) {
72
+ console.warn(this.formatMessage('warn', message), ...args);
73
+ }
74
+ }
75
+
76
+ error(message: string, ...args: unknown[]): void {
77
+ if (this.shouldLog('error')) {
78
+ console.error(this.formatMessage('error', message), ...args);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * 设置日志级别
84
+ */
85
+ setLevel(level: LogLevel): void {
86
+ this.level = level;
87
+ }
88
+
89
+ /**
90
+ * 创建带有自定义前缀的子 logger
91
+ */
92
+ child(prefix: string): Logger {
93
+ return new Logger({
94
+ level: this.level,
95
+ prefix: `${this.prefix}:${prefix}`,
96
+ enableInProduction: this.enableInProduction,
97
+ });
98
+ }
99
+ }
100
+
101
+ // 默认 logger 实例
102
+ export const logger = new Logger();
103
+
104
+ // 创建模块专用 logger
105
+ export const createLogger = (prefix: string): Logger => {
106
+ return logger.child(prefix);
107
+ };
108
+
109
+ // 导出类型
110
+ export type { LogLevel, LoggerConfig };
111
+ export { Logger };
112
+
113
+ export default logger;