create-pardx-scaffold 0.1.0 → 0.1.2

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 (75) 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/shared-services/email/email.module.ts +0 -2
  12. package/template/apps/api/libs/infra/shared-services/file-storage/bucket-resolver.ts +1 -1
  13. package/template/apps/api/libs/infra/shared-services/file-storage/file-storage.module.ts +1 -1
  14. package/template/apps/api/libs/infra/shared-services/sms/sms.module.ts +0 -2
  15. package/template/apps/api/package.json +15 -15
  16. package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
  17. package/template/apps/api/src/app.module.ts +1 -1
  18. package/template/apps/web/.env.example +6 -4
  19. package/template/apps/web/components/error-boundary.tsx +166 -0
  20. package/template/apps/web/components/index.ts +10 -0
  21. package/template/apps/web/components.json +20 -0
  22. package/template/apps/web/config.ts +115 -0
  23. package/template/apps/web/eslint.config.mjs +4 -0
  24. package/template/apps/web/lib/api/avatar-upload.ts +1 -0
  25. package/template/apps/web/lib/api/contracts/client.ts +51 -30
  26. package/template/apps/web/lib/api/contracts/hooks/index.ts +0 -3
  27. package/template/apps/web/lib/api/contracts/hooks/notification.ts +42 -124
  28. package/template/apps/web/lib/api.ts +24 -1
  29. package/template/apps/web/lib/dynamic-import.tsx +121 -0
  30. package/template/apps/web/lib/logger.ts +113 -0
  31. package/template/apps/web/lib/upload/api.ts +37 -105
  32. package/template/apps/web/lib/upload/batch-uploader.ts +7 -74
  33. package/template/apps/web/lib/upload/uploader.ts +10 -74
  34. package/template/apps/web/locales/zh-CN/assessment.json +5 -0
  35. package/template/apps/web/locales/zh-CN/chat.json +6 -0
  36. package/template/apps/web/locales/zh-CN/common.json +38 -0
  37. package/template/apps/web/locales/zh-CN/creative.json +5 -0
  38. package/template/apps/web/locales/zh-CN/daily-challenge.json +6 -0
  39. package/template/apps/web/locales/zh-CN/errors.json +16 -0
  40. package/template/apps/web/locales/zh-CN/forms.json +18 -0
  41. package/template/apps/web/locales/zh-CN/memory.json +5 -0
  42. package/template/apps/web/locales/zh-CN/navigation.json +12 -0
  43. package/template/apps/web/locales/zh-CN/recommendation.json +5 -0
  44. package/template/apps/web/locales/zh-CN/recruitment.json +5 -0
  45. package/template/apps/web/locales/zh-CN/settings.json +7 -0
  46. package/template/apps/web/locales/zh-CN/subscription.json +6 -0
  47. package/template/apps/web/locales/zh-CN/validation.json +8 -0
  48. package/template/apps/web/package.json +14 -15
  49. package/template/apps/web/postcss.config.mjs +1 -0
  50. package/template/apps/web/proxy.ts +102 -0
  51. package/template/apps/web/public/logo.svg +21 -0
  52. package/template/apps/web/vitest.config.ts +69 -0
  53. package/template/apps/web/vitest.setup.ts +80 -0
  54. package/template/package.json +7 -7
  55. package/template/packages/constants/package.json +3 -1
  56. package/template/packages/constants/tsconfig.build.esm.json +8 -0
  57. package/template/packages/contracts/package.json +2 -2
  58. package/template/packages/contracts/src/schemas/uploader.schema.ts +33 -10
  59. package/template/packages/ui/.storybook/main.ts +28 -0
  60. package/template/packages/ui/.storybook/preview.ts +40 -0
  61. package/template/packages/ui/eslint.config.js +3 -0
  62. package/template/packages/ui/package.json +15 -2
  63. package/template/packages/ui/src/components/button.stories.tsx +171 -0
  64. package/template/packages/ui/src/styles/globals.css +1 -1
  65. package/template/packages/ui/tsconfig.json +1 -1
  66. package/template/packages/utils/package.json +2 -2
  67. package/template/packages/utils/tsconfig.build.esm.json +8 -0
  68. package/template/packages/validators/package.json +1 -1
  69. package/template/pnpm-lock.yaml +2263 -999
  70. package/template/scripts/export-scaffold-for-create.js +65 -0
  71. package/template/apps/api/libs/infra/utils/download.ts +0 -21
  72. package/template/apps/web/lib/api/client.ts +0 -649
  73. package/template/apps/web/lib/audio-buffer-queue.ts +0 -273
  74. package/template/apps/web/lib/upload/folder-utils.ts +0 -295
  75. /package/template/apps/api/libs/{infra/shared-services → domain/services}/ip-info/index.ts +0 -0
@@ -0,0 +1,115 @@
1
+ // API 配置
2
+ // API 基础地址从环境变量读取
3
+ // 在 .env.local 文件中设置以下环境变量:
4
+ // - NEXT_PUBLIC_API_BASE_URL: 登录、上传文件、测评等接口的基础地址
5
+
6
+ /**
7
+ * 获取 API 基础地址(用于登录、上传文件、测评等)
8
+ */
9
+ const getApiBaseUrl = (): string => {
10
+ const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
11
+
12
+ if (!baseUrl) {
13
+ // Use default value during build time or development
14
+ const defaultUrl = 'http://localhost:3100/api';
15
+ if (
16
+ process.env.NODE_ENV === 'development' ||
17
+ process.env.NEXT_PHASE === 'phase-production-build'
18
+ ) {
19
+ return defaultUrl;
20
+ }
21
+ // At runtime in production, use default if not set
22
+ console.warn('NEXT_PUBLIC_API_BASE_URL 未设置,使用默认值', defaultUrl);
23
+ return defaultUrl;
24
+ }
25
+
26
+ return baseUrl;
27
+ };
28
+
29
+ /**
30
+ * 获取品牌名称
31
+ */
32
+ const getBrandName = (): string => {
33
+ const brandName = process.env.NEXT_PUBLIC_BRAND_NAME;
34
+ return brandName || 'Pardx.AI';
35
+ };
36
+
37
+ /**
38
+ * 获取品牌 Logo 路径
39
+ */
40
+ const getBrandLogo = (): string => {
41
+ const brandLogo = process.env.NEXT_PUBLIC_BRAND_LOGO;
42
+ return brandLogo || '/logo.svg';
43
+ };
44
+
45
+ /**
46
+ * 获取品牌完整标题(用于页面标题)
47
+ *
48
+ * 产品定位:多智能体驱动的内容创作与运营平台
49
+ * 核心能力:
50
+ * - AI 内容创作(智能写作、创意生成)
51
+ * - 知识库管理(进化型知识库、智能提取、质量评估)
52
+ * - 智能推荐(向量检索、协同过滤)
53
+ * - 招聘面试(AI 招聘 Agent、简历解析、智能匹配)
54
+ * - 会议管理(实时转写、知识提取、纪要生成)
55
+ * - 多智能体协作(AG-UI/Agno)
56
+ */
57
+ const getBrandTitle = (): string => {
58
+ const brandTitle = process.env.NEXT_PUBLIC_BRAND_TITLE;
59
+ return brandTitle || 'PardxAI - Multi-Agent Content Creation ';
60
+ };
61
+
62
+ /**
63
+ * 获取品牌描述(用于页面描述)
64
+ *
65
+ * 描述应包含:
66
+ * - 产品定位(多智能体平台)
67
+ * - 核心功能(内容创作、知识管理、智能推荐、招聘面试)
68
+ * - 技术优势(AI 驱动、多智能体协作)
69
+ * - 目标用户(企业团队、内容创作者、HR 团队)
70
+ */
71
+ const getBrandDescription = (): string => {
72
+ const brandDescription = process.env.NEXT_PUBLIC_BRAND_DESCRIPTION;
73
+ return (
74
+ brandDescription ||
75
+ 'PardxAI is an AI-powered multi-agent platform for content creation.'
76
+ );
77
+ };
78
+
79
+ export const API_CONFIG = {
80
+ // API 基础地址(用于登录、上传文件、测评等)
81
+ baseUrl: getApiBaseUrl(),
82
+
83
+ // API 端点路径 这些断点是不适用ts-rest-api的,用于登录和校验权限的断点,如果未来需要使用ts-rest-api,则将这些断点迁移到ts-rest-api中
84
+ endpoints: {
85
+ // 登录端点
86
+ login: '/sign/in/mobile/password',
87
+
88
+ // Token 刷新端点
89
+ refreshToken: '/sign/refresh/token',
90
+ },
91
+ };
92
+
93
+ /**
94
+ * 品牌配置
95
+ * 可通过环境变量覆盖:
96
+ * - NEXT_PUBLIC_BRAND_NAME: 品牌名称(默认: "Pardx.AI")
97
+ * - NEXT_PUBLIC_BRAND_LOGO: Logo 路径(默认: "/logo.svg")
98
+ * - NEXT_PUBLIC_BRAND_TITLE: 页面标题(默认: "PardxAI - Multi-Agent Content Creation")
99
+ * - NEXT_PUBLIC_BRAND_DESCRIPTION: 页面描述(默认: "PardxAI is an AI-powered multi-agent platform for content creation.")
100
+ *
101
+ * 产品定位说明:
102
+ * PardxAI 是一个多智能体驱动的内容创作与运营平台,提供以下核心能力:
103
+ * 1. AI 内容创作:智能写作、创意生成、多模态内容创作
104
+ * 2. 知识库管理:进化型知识库系统、智能知识提取、质量评估、版本控制、相似度检测与合并
105
+ * 3. 智能推荐:基于向量检索和协同过滤的知识推荐系统
106
+ * 4. 招聘面试:AI 招聘 Agent、简历解析、JD 分析、人才匹配、智能面试
107
+ * 5. 会议管理:实时转写、知识提取、智能纪要生成
108
+ * 6. 多智能体协作:AG-UI/Agno 集成,支持多智能体协同工作
109
+ */
110
+ export const BRAND_CONFIG = {
111
+ name: getBrandName(),
112
+ logo: getBrandLogo(),
113
+ title: getBrandTitle(),
114
+ description: getBrandDescription(),
115
+ };
@@ -0,0 +1,4 @@
1
+ import { nextJsConfig } from '@repo/config/eslint/next-js';
2
+
3
+ /** @type {import("eslint").Linter.Config} */
4
+ export default nextJsConfig;
@@ -32,6 +32,7 @@ export async function uploadAvatar(
32
32
  body: {
33
33
  signature,
34
34
  filename,
35
+ fsize: file.size,
35
36
  bucket: 'pardx-image',
36
37
  vendor: 'oss',
37
38
  },
@@ -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
+ };