generator-mico-cli 0.2.2 → 0.2.5

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 (20) hide show
  1. package/generators/micro-react/templates/apps/layout/docs/feature-/345/276/256/345/211/215/347/253/257/346/250/241/345/274/217.md +90 -8
  2. package/generators/micro-react/templates/apps/layout/src/app.tsx +5 -69
  3. package/generators/micro-react/templates/apps/layout/src/common/micro/index.ts +34 -0
  4. package/generators/micro-react/templates/apps/layout/src/common/micro-prefetch.ts +108 -0
  5. package/generators/micro-react/templates/apps/layout/src/common/request/index.ts +2 -2
  6. package/generators/micro-react/templates/apps/layout/src/common/route-guard.ts +345 -0
  7. package/generators/micro-react/templates/apps/layout/src/common/theme.ts +1 -1
  8. package/generators/micro-react/templates/apps/layout/src/common/upload/oss.ts +1 -1
  9. package/generators/micro-react/templates/apps/layout/src/common/upload/types.ts +1 -1
  10. package/generators/micro-react/templates/apps/layout/src/common/uploadFiles.ts +1 -1
  11. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +59 -185
  12. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/micro-app-manager.ts +511 -0
  13. package/generators/micro-react/templates/apps/layout/src/constants/index.ts +63 -4
  14. package/generators/micro-react/templates/apps/layout/src/hooks/useRoutePermissionRefresh.ts +26 -23
  15. package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +55 -42
  16. package/generators/micro-react/templates/apps/layout/src/pages/403/index.tsx +6 -1
  17. package/generators/subapp-react/templates/homepage/src/app.tsx +4 -24
  18. package/generators/subapp-react/templates/homepage/src/global.less +4 -4
  19. package/package.json +1 -1
  20. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/container-manager.ts +0 -334
@@ -0,0 +1,511 @@
1
+ /**
2
+ * 微应用管理器 v10
3
+ *
4
+ * 核心设计:
5
+ * 1. 稳定容器:容器在 body 中创建,不受 React 生命周期影响
6
+ * 2. 容器移动:激活时移到目标元素内,停用时移回 body 隐藏
7
+ * 3. 实例缓存:每个 entry 只 loadMicroApp 一次,复用实例
8
+ * 4. 自动路由守卫:由 route-guard.ts 自动检测用户意图,无需手动标记
9
+ *
10
+ * v10 改进(相比 v9):
11
+ * - 移除手动 setUserIntent 机制,改为自动检测用户交互
12
+ * - 业务代码无需感知路由守卫,可使用任意常规路由跳转方式
13
+ * - 路由守卫逻辑抽离到独立模块 route-guard.ts
14
+ */
15
+
16
+ import type { MicroApp } from 'qiankun';
17
+ import { loadMicroApp } from 'qiankun';
18
+ import { microAppLogger } from '@/common/logger';
19
+ // 导入路由守卫(会自动初始化)
20
+ import { refreshUserIntent, setUserIntent } from '@/common/route-guard';
21
+
22
+ // 导入低优先级预加载函数
23
+ import { markAppAsPrefetched, prefetchMicroAppsLowPriority } from '@/common/micro-prefetch';
24
+
25
+ // ============================================================================
26
+ // 类型定义
27
+ // ============================================================================
28
+
29
+ export interface MicroAppConfig {
30
+ name: string;
31
+ entry: string;
32
+ /** 目标挂载位置(用于移动容器) */
33
+ target: HTMLElement;
34
+ props: Record<string, unknown>;
35
+ }
36
+
37
+ export interface MicroAppState {
38
+ loading: boolean;
39
+ error: string | null;
40
+ mounted: boolean;
41
+ }
42
+
43
+ type StateChangeCallback = (state: MicroAppState) => void;
44
+
45
+ type ManagerState = 'idle' | 'loading' | 'mounted';
46
+
47
+ interface AppInstance {
48
+ microApp: MicroApp;
49
+ container: HTMLElement;
50
+ entry: string;
51
+ /** qiankun 实例名称(可能与缓存 key 不同,用于避免重复加载问题) */
52
+ qiankunName: string;
53
+ }
54
+
55
+ // ============================================================================
56
+ // 常量
57
+ // ============================================================================
58
+
59
+ const LOAD_TIMEOUT = 30000;
60
+ const MOUNT_TIMEOUT = 30000;
61
+ const UNMOUNT_TIMEOUT = 10000;
62
+
63
+ const CSS_CLASS = {
64
+ base: 'micro-app-stable-container',
65
+ hidden: 'micro-app-stable-container--hidden',
66
+ active: 'micro-app-stable-container--active',
67
+ } as const;
68
+
69
+ // ============================================================================
70
+ // 工具函数
71
+ // ============================================================================
72
+
73
+ function withTimeout<T>(
74
+ promise: Promise<T>,
75
+ ms: number,
76
+ message: string,
77
+ ): Promise<T> {
78
+ return Promise.race([
79
+ promise,
80
+ new Promise<T>((_, reject) => {
81
+ setTimeout(() => reject(new Error(message)), ms);
82
+ }),
83
+ ]);
84
+ }
85
+
86
+ function createStableContainer(appName: string): HTMLElement {
87
+ const container = document.createElement('div');
88
+ container.id = `micro-app-stable-${appName}`;
89
+ container.className = `${CSS_CLASS.base} ${CSS_CLASS.hidden}`;
90
+ container.style.cssText = 'display: none; position: absolute; left: -9999px;';
91
+ document.body.appendChild(container);
92
+ return container;
93
+ }
94
+
95
+ function activateContainer(container: HTMLElement, target: HTMLElement): void {
96
+ target.appendChild(container);
97
+ container.classList.remove(CSS_CLASS.hidden);
98
+ container.classList.add(CSS_CLASS.active);
99
+ container.style.cssText = 'display: block; width: 100%; height: 100%;';
100
+ }
101
+
102
+ function deactivateContainer(container: HTMLElement): void {
103
+ document.body.appendChild(container);
104
+ container.classList.remove(CSS_CLASS.active);
105
+ container.classList.add(CSS_CLASS.hidden);
106
+ container.style.cssText = 'display: none; position: absolute; left: -9999px;';
107
+ }
108
+
109
+ // ============================================================================
110
+ // MicroAppManager 类
111
+ // ============================================================================
112
+
113
+ class MicroAppManager {
114
+ private static instance: MicroAppManager;
115
+
116
+ /** 当前状态 */
117
+ private state: ManagerState = 'idle';
118
+
119
+ /** 已加载的应用实例缓存 */
120
+ private appCache = new Map<string, AppInstance>();
121
+
122
+ /** 当前激活的应用名称 */
123
+ private currentAppName: string | null = null;
124
+
125
+ /** 待处理的请求 */
126
+ private pendingRequest: MicroAppConfig | null = null;
127
+
128
+ /** 状态变化回调 */
129
+ private stateCallback: StateChangeCallback | null = null;
130
+
131
+ /** 操作序列号(用于检测过期操作) */
132
+ private operationSeq = 0;
133
+
134
+ private constructor() {
135
+ // 路由守卫已在 route-guard.ts 中自动初始化
136
+ microAppLogger.log('MicroAppManager initialized');
137
+ }
138
+
139
+ static getInstance(): MicroAppManager {
140
+ if (!MicroAppManager.instance) {
141
+ MicroAppManager.instance = new MicroAppManager();
142
+ }
143
+ return MicroAppManager.instance;
144
+ }
145
+
146
+ // ==========================================================================
147
+ // 公开 API
148
+ // ==========================================================================
149
+
150
+ setStateCallback(callback: StateChangeCallback | null): void {
151
+ console.log('🔍[路由调试] setStateCallback', { hasCallback: !!callback });
152
+ this.stateCallback = callback;
153
+ }
154
+
155
+ /**
156
+ * 切换到指定的微应用
157
+ */
158
+ switchTo(config: MicroAppConfig): void {
159
+ microAppLogger.log('switchTo:', config.name, {
160
+ current: this.currentAppName,
161
+ state: this.state,
162
+ });
163
+
164
+ // 设置/刷新路由意图,确保在整个加载过程中拦截子应用的非预期路由操作
165
+ // 使用当前 URL pathname 作为意图路径
166
+ setUserIntent(window.location.pathname);
167
+
168
+ // 如果是当前已激活的应用,只更新 props
169
+ if (this.state === 'mounted' && this.currentAppName === config.name) {
170
+ microAppLogger.log('Already mounted, updating props only');
171
+ const cached = this.appCache.get(config.name);
172
+ if (cached && cached.microApp.getStatus() === 'MOUNTED') {
173
+ cached.microApp.update?.(config.props);
174
+ if (cached.container.parentElement !== config.target) {
175
+ activateContainer(cached.container, config.target);
176
+ }
177
+ // 通知组件当前状态(重要:组件可能在 cleanup 后重新挂载,需要同步状态)
178
+ this.updateState({ loading: false, error: null, mounted: true });
179
+ }
180
+ return;
181
+ }
182
+
183
+ // 保存请求
184
+ this.pendingRequest = config;
185
+
186
+ // 根据状态处理
187
+ if (this.state === 'idle') {
188
+ this.processRequest();
189
+ } else if (this.state === 'mounted') {
190
+ this.updateState({ loading: true, error: null, mounted: false });
191
+ this.deactivateCurrentAndProcess();
192
+ } else {
193
+ // loading 状态,等待完成后处理
194
+ microAppLogger.log('Busy, request queued');
195
+ this.updateState({ loading: true, error: null, mounted: false });
196
+ }
197
+ }
198
+
199
+ updateProps(props: Record<string, unknown>): void {
200
+ if (this.currentAppName && this.state === 'mounted') {
201
+ const cached = this.appCache.get(this.currentAppName);
202
+ if (cached && cached.microApp.getStatus() === 'MOUNTED') {
203
+ cached.microApp.update?.(props);
204
+ }
205
+ }
206
+ }
207
+
208
+ cancel(): void {
209
+ microAppLogger.log('cancel');
210
+ this.pendingRequest = null;
211
+ }
212
+
213
+ async clearCache(): Promise<void> {
214
+ microAppLogger.log('Clearing cache');
215
+ for (const [name, instance] of this.appCache) {
216
+ try {
217
+ await this.safeUnmount(instance.microApp);
218
+ instance.container.remove();
219
+ } catch (err) {
220
+ microAppLogger.warn('Clear cache error for', name, err);
221
+ }
222
+ }
223
+ this.appCache.clear();
224
+ this.currentAppName = null;
225
+ this.state = 'idle';
226
+ }
227
+
228
+ getDebugInfo(): object {
229
+ return {
230
+ state: this.state,
231
+ currentAppName: this.currentAppName,
232
+ pendingRequest: this.pendingRequest?.name ?? null,
233
+ cacheSize: this.appCache.size,
234
+ cachedApps: Array.from(this.appCache.entries()).map(([name, inst]) => ({
235
+ name,
236
+ qiankunName: inst.qiankunName,
237
+ entry: inst.entry,
238
+ status: inst.microApp.getStatus(),
239
+ })),
240
+ };
241
+ }
242
+
243
+ // ==========================================================================
244
+ // 私有方法
245
+ // ==========================================================================
246
+
247
+ private updateState(state: MicroAppState): void {
248
+ console.log('🔍[路由调试] updateState', { state, hasCallback: !!this.stateCallback });
249
+ this.stateCallback?.(state);
250
+ }
251
+
252
+ private shouldAbort(currentName: string, mySeq: number): boolean {
253
+ if (mySeq !== this.operationSeq) {
254
+ return true;
255
+ }
256
+ if (this.pendingRequest && this.pendingRequest.name !== currentName) {
257
+ return true;
258
+ }
259
+ return false;
260
+ }
261
+
262
+ private async processRequest(): Promise<void> {
263
+ const request = this.pendingRequest;
264
+ this.pendingRequest = null;
265
+
266
+ if (!request) {
267
+ microAppLogger.log('No pending request');
268
+ return;
269
+ }
270
+
271
+ const mySeq = ++this.operationSeq;
272
+ console.log('🔍[路由调试] processRequest 开始', { name: request.name, seq: mySeq });
273
+
274
+ // 刷新意图,确保在整个加载过程中意图保持有效
275
+ refreshUserIntent();
276
+
277
+ this.state = 'loading';
278
+ this.updateState({ loading: true, error: null, mounted: false });
279
+
280
+ try {
281
+
282
+ if (this.shouldAbort(request.name, mySeq)) {
283
+ console.log('🔍[路由调试] ⚠️ Aborted before load', { name: request.name, mySeq, operationSeq: this.operationSeq });
284
+ this.state = 'idle';
285
+ if (this.pendingRequest) this.processRequest();
286
+ return;
287
+ }
288
+
289
+ let appInstance = this.appCache.get(request.name);
290
+
291
+ if (appInstance) {
292
+ // 复用已有实例
293
+ console.log('🔍[路由调试] 复用缓存实例', { name: request.name, status: appInstance.microApp.getStatus() });
294
+
295
+ // 刷新意图
296
+ refreshUserIntent();
297
+
298
+ if (this.shouldAbort(request.name, mySeq)) {
299
+ console.log('🔍[路由调试] ⚠️ Aborted (cached) before activate', { name: request.name });
300
+ this.state = 'idle';
301
+ if (this.pendingRequest) this.processRequest();
302
+ return;
303
+ }
304
+
305
+ activateContainer(appInstance.container, request.target);
306
+
307
+ const status = appInstance.microApp.getStatus();
308
+ console.log('🔍[路由调试] 缓存实例状态', { name: request.name, qiankunName: appInstance.qiankunName, status });
309
+
310
+ // 根据状态决定如何挂载
311
+ if (status === 'BOOTSTRAPPING') {
312
+ // 实例在 loadPromise 后被缓存,但还未完成 mount
313
+ // 需要等待 mountPromise 完成
314
+ console.log('🔍[路由调试] 等待缓存实例 mountPromise...', { name: request.name });
315
+ await withTimeout(appInstance.microApp.mountPromise, MOUNT_TIMEOUT, '子应用挂载超时');
316
+ console.log('🔍[路由调试] 缓存实例 mountPromise 完成', { name: request.name, status: appInstance.microApp.getStatus() });
317
+ // 重要:mount 完成后立即更新 props,确保子应用使用最新的 routePath
318
+ // qiankun 的 mount() 使用的是创建实例时的原始 props,可能包含过期的 routePath
319
+ appInstance.microApp.update?.(request.props);
320
+ console.log('🔍[路由调试] 缓存实例 props 已更新(BOOTSTRAPPING 后)', { name: request.name });
321
+ } else if (status === 'NOT_MOUNTED') {
322
+ // 实例之前被 unmount 过,需要重新 mount
323
+ console.log('🔍[路由调试] 开始 mount 缓存实例', { name: request.name });
324
+ await withTimeout(appInstance.microApp.mount(), MOUNT_TIMEOUT, '子应用挂载超时');
325
+ console.log('🔍[路由调试] 缓存实例 mount 完成', { name: request.name, status: appInstance.microApp.getStatus() });
326
+ // 重要:mount 完成后立即更新 props,确保子应用使用最新的 routePath
327
+ // qiankun 的 mount() 使用的是创建实例时的原始 props,可能包含过期的 routePath
328
+ appInstance.microApp.update?.(request.props);
329
+ console.log('🔍[路由调试] 缓存实例 props 已更新(NOT_MOUNTED 后)', { name: request.name });
330
+ }
331
+ // 如果 status === 'MOUNTED',则无需操作(已在 switchTo 入口处处理)
332
+
333
+ // 刷新意图,保护 mount 成功后的短暂窗口期
334
+ refreshUserIntent();
335
+
336
+ if (this.shouldAbort(request.name, mySeq)) {
337
+ console.log('🔍[路由调试] ⚠️ Aborted (cached) after mount', { name: request.name });
338
+ await this.safeUnmount(appInstance.microApp);
339
+ deactivateContainer(appInstance.container);
340
+ this.state = 'idle';
341
+ if (this.pendingRequest) this.processRequest();
342
+ return;
343
+ }
344
+
345
+ this.currentAppName = request.name;
346
+ this.state = 'mounted';
347
+ // 不立即清除意图,让它自然过期(5秒)
348
+ // 这样可以防止被 abort 的子应用在后台执行时修改路由
349
+ console.log('🔍[路由调试] ✅ 缓存实例挂载成功', { name: request.name, qiankunName: appInstance.qiankunName, status: appInstance.microApp.getStatus() });
350
+ this.updateState({ loading: false, error: null, mounted: true });
351
+
352
+ // 挂载成功后,标记当前应用已加载,并触发其他应用的低优先级预加载
353
+ markAppAsPrefetched(request.entry);
354
+ prefetchMicroAppsLowPriority(request.entry);
355
+
356
+ } else {
357
+ // 创建新实例
358
+ // 使用唯一的 qiankun 实例名称,避免 qiankun 内部状态冲突
359
+ // 当应用被 abort 后重新加载时,qiankun 可能仍保留旧的内部状态
360
+ // 使用时间戳后缀确保每次 loadMicroApp 都是全新的实例
361
+ const qiankunName = `${request.name}__${Date.now()}`;
362
+ console.log('🔍[路由调试] 创建新实例', { name: request.name, qiankunName });
363
+
364
+ if (this.shouldAbort(request.name, mySeq)) {
365
+ this.state = 'idle';
366
+ if (this.pendingRequest) this.processRequest();
367
+ return;
368
+ }
369
+
370
+ const container = createStableContainer(qiankunName);
371
+ activateContainer(container, request.target);
372
+
373
+ console.log('🔍[路由调试] 调用 loadMicroApp', { name: request.name, qiankunName, entry: request.entry });
374
+ const microApp = loadMicroApp(
375
+ {
376
+ name: qiankunName,
377
+ entry: request.entry,
378
+ container,
379
+ props: request.props,
380
+ },
381
+ {
382
+ sandbox: { strictStyleIsolation: false, experimentalStyleIsolation: true },
383
+ },
384
+ );
385
+
386
+ console.log('🔍[路由调试] 等待 loadPromise...', { name: request.name });
387
+ await withTimeout(microApp.loadPromise, LOAD_TIMEOUT, '子应用资源加载超时');
388
+ console.log('🔍[路由调试] loadPromise 完成', { name: request.name, status: microApp.getStatus() });
389
+
390
+ // 刷新意图,继续保护后续操作
391
+ refreshUserIntent();
392
+
393
+ // loadPromise 完成后立即缓存实例
394
+ // 即使后续被 abort,下次也能复用此实例,避免创建多个实例导致重复请求
395
+ appInstance = { microApp, container, entry: request.entry, qiankunName };
396
+ this.appCache.set(request.name, appInstance);
397
+ console.log('🔍[路由调试] 实例已缓存(loadPromise 后)', { name: request.name, qiankunName });
398
+
399
+ if (this.shouldAbort(request.name, mySeq)) {
400
+ console.log('🔍[路由调试] ⚠️ Aborted after loadPromise', { name: request.name, mySeq, operationSeq: this.operationSeq });
401
+ // 不销毁实例,只是 deactivate 容器,实例保留在缓存中供下次复用
402
+ deactivateContainer(container);
403
+ this.state = 'idle';
404
+ if (this.pendingRequest) this.processRequest();
405
+ return;
406
+ }
407
+
408
+ console.log('🔍[路由调试] 等待 mountPromise...', { name: request.name, qiankunName, status: microApp.getStatus() });
409
+ await withTimeout(microApp.mountPromise, MOUNT_TIMEOUT, '子应用挂载超时');
410
+ console.log('🔍[路由调试] mountPromise 完成', { name: request.name, qiankunName, status: microApp.getStatus() });
411
+
412
+ // 重要:mount 完成后调用 update,触发子应用的路由同步
413
+ // 由于子应用 mount 生命周期不再调用 syncRoute,需要通过 update 来同步路由
414
+ microApp.update?.(request.props);
415
+ console.log('🔍[路由调试] 新实例 props 已更新', { name: request.name });
416
+
417
+ // 刷新意图,保护 mount 成功后的短暂窗口期
418
+ refreshUserIntent();
419
+
420
+ if (this.shouldAbort(request.name, mySeq)) {
421
+ console.log('🔍[路由调试] ⚠️ Aborted after mountPromise', { name: request.name, mySeq, operationSeq: this.operationSeq });
422
+ // 实例已在 loadPromise 后缓存,这里只需 unmount 和 deactivate
423
+ await this.safeUnmount(microApp);
424
+ deactivateContainer(container);
425
+ this.state = 'idle';
426
+ if (this.pendingRequest) this.processRequest();
427
+ return;
428
+ }
429
+
430
+ // 实例已在 loadPromise 后缓存,这里不需要重复缓存
431
+
432
+ this.currentAppName = request.name;
433
+ this.state = 'mounted';
434
+ // 不立即清除意图,让它自然过期(5秒)
435
+ // 这样可以防止被 abort 的子应用在后台执行时修改路由
436
+ console.log('🔍[路由调试] ✅ 新实例挂载成功', { name: request.name, qiankunName, status: microApp.getStatus() });
437
+ this.updateState({ loading: false, error: null, mounted: true });
438
+
439
+ // 挂载成功后,标记当前应用已加载,并触发其他应用的低优先级预加载
440
+ markAppAsPrefetched(request.entry);
441
+ prefetchMicroAppsLowPriority(request.entry);
442
+ }
443
+
444
+ if (this.pendingRequest) {
445
+ this.deactivateCurrentAndProcess();
446
+ }
447
+
448
+ } catch (err) {
449
+ microAppLogger.error('Error:', err);
450
+
451
+ if (mySeq !== this.operationSeq) return;
452
+
453
+ this.state = 'idle';
454
+ this.currentAppName = null;
455
+
456
+ if (this.pendingRequest) {
457
+ this.processRequest();
458
+ } else {
459
+ const message = err instanceof Error ? err.message : 'Unknown error';
460
+ this.updateState({
461
+ loading: false,
462
+ error: message.includes('Failed to fetch')
463
+ ? '无法连接到子应用服务,请检查子应用是否已启动'
464
+ : message,
465
+ mounted: false,
466
+ });
467
+ }
468
+ }
469
+ }
470
+
471
+ private async deactivateCurrentAndProcess(): Promise<void> {
472
+ if (!this.currentAppName) {
473
+ this.state = 'idle';
474
+ this.processRequest();
475
+ return;
476
+ }
477
+
478
+ const appInstance = this.appCache.get(this.currentAppName);
479
+ microAppLogger.log('Deactivating:', this.currentAppName);
480
+
481
+ if (appInstance) {
482
+ try {
483
+ await this.safeUnmount(appInstance.microApp);
484
+ deactivateContainer(appInstance.container);
485
+ } catch (err) {
486
+ microAppLogger.warn('Deactivate error:', err);
487
+ }
488
+ }
489
+
490
+ this.currentAppName = null;
491
+ this.state = 'idle';
492
+ this.processRequest();
493
+ }
494
+
495
+ private async safeUnmount(microApp: MicroApp): Promise<void> {
496
+ try {
497
+ if (microApp.getStatus() === 'MOUNTED') {
498
+ await withTimeout(microApp.unmount(), UNMOUNT_TIMEOUT, 'Unmount timeout');
499
+ }
500
+ } catch (err) {
501
+ microAppLogger.warn('safeUnmount error:', err);
502
+ }
503
+ }
504
+ }
505
+
506
+ export const microAppManager = MicroAppManager.getInstance();
507
+
508
+ // 暴露到 window 供调试
509
+ if (typeof window !== 'undefined') {
510
+ (window as unknown as Record<string, unknown>).__MICRO_APP_MANAGER__ = microAppManager;
511
+ }
@@ -1,15 +1,74 @@
1
- export const DEFAULT_NAME = 'Umi Max';
1
+ /**
2
+ * 应用常量定义
3
+ */
2
4
 
3
5
  /**
4
- * 无需认证的路由路径
6
+ * 路由路径常量
5
7
  */
6
- export const NO_AUTH_ROUTES = {
8
+ export const ROUTES = {
9
+ /** 登录页 */
7
10
  LOGIN: '/user/login',
11
+ /** 注册页 */
12
+ REGISTER: '/user/register',
13
+ /** 注册结果页 */
14
+ REGISTER_RESULT: '/user/register-result',
15
+ /** 403 无权限页 */
8
16
  FORBIDDEN: '/403',
17
+ /** 404 未找到页 */
9
18
  NOT_FOUND: '/404',
10
19
  } as const;
11
20
 
12
21
  /**
13
22
  * 无需认证的路由列表
14
23
  */
15
- export const NO_AUTH_ROUTE_LIST = Object.values(NO_AUTH_ROUTES);
24
+ export const NO_AUTH_ROUTE_LIST: string[] = [
25
+ ROUTES.LOGIN,
26
+ ROUTES.REGISTER,
27
+ ROUTES.REGISTER_RESULT,
28
+ ROUTES.FORBIDDEN,
29
+ ROUTES.NOT_FOUND,
30
+ ];
31
+
32
+ /**
33
+ * 主题相关常量
34
+ */
35
+ export const THEME = {
36
+ /** localStorage 存储键 */
37
+ STORAGE_KEY: 'audit-center-theme',
38
+ /** 默认主题 */
39
+ DEFAULT: 'light' as const,
40
+ /** 可选主题值 */
41
+ VALUES: ['light', 'dark'] as const,
42
+ } as const;
43
+
44
+ /**
45
+ * 时区相关常量
46
+ */
47
+ export const TIMEZONE = {
48
+ /** localStorage 存储键(IANA 时区,如 Asia/Shanghai) */
49
+ STORAGE_KEY: 'audit-center-timezone',
50
+ /** localStorage 存储键(用于展示的地区/名称,可选) */
51
+ REGION_STORAGE_KEY: 'audit-center-timezone-region',
52
+ } as const;
53
+
54
+ /**
55
+ * 在线状态相关常量
56
+ */
57
+ export const PRESENCE = {
58
+ /** localStorage 存储键 */
59
+ STORAGE_KEY: 'audit-center-presence-status',
60
+ } as const;
61
+
62
+ /**
63
+ * 存储键常量
64
+ */
65
+ export const STORAGE_KEYS = {
66
+ APP_INFO: 'appInfo',
67
+ IS_SUPERUSER: 'is_superuser',
68
+ GROUPS: 'groups',
69
+ } as const;
70
+
71
+ /**
72
+ * OSS 上传目录分类
73
+ */
74
+ export type TDirCategory = number;
@@ -1,6 +1,5 @@
1
1
  import { useLocation, useModel } from '@umijs/max';
2
- import debounce from 'lodash-es/debounce';
3
- import { useEffect, useMemo, useRef, useState } from 'react';
2
+ import { useEffect, useRef, useState } from 'react';
4
3
  import { layoutLogger } from '@/common/logger';
5
4
  import { NO_AUTH_ROUTE_LIST } from '@/constants';
6
5
 
@@ -21,49 +20,53 @@ export function useRoutePermissionRefresh() {
21
20
 
22
21
  const isFirstRender = useRef(true);
23
22
  const prevPathRef = useRef(location.pathname);
24
- // 使用 ref 持久化 refresh,避免 debounce 实例重建
25
23
  const refreshRef = useRef(refresh);
24
+ const timerRef = useRef<ReturnType<typeof setTimeout>>();
26
25
  refreshRef.current = refresh;
27
26
 
28
- const debouncedRefresh = useMemo(
29
- () =>
30
- debounce(async (pathname: string) => {
31
- layoutLogger.log('Route changed, refreshing user info:', pathname);
32
- try {
33
- await refreshRef.current?.();
34
- } finally {
35
- setIsRefreshing(false);
36
- }
37
- }, 300),
38
- [],
39
- );
40
-
41
27
  useEffect(() => {
42
- // 跳过首次渲染(getInitialState 已经获取过用户信息)
28
+ // 跳过首次渲染
43
29
  if (isFirstRender.current) {
44
30
  isFirstRender.current = false;
45
31
  return;
46
32
  }
47
33
 
48
- // 只有路径真正变化时才刷新(避免 search/hash 变化触发)
34
+ // 路径未变化
49
35
  if (prevPathRef.current === location.pathname) {
50
36
  return;
51
37
  }
38
+
52
39
  prevPathRef.current = location.pathname;
53
40
 
54
- // 免认证路由不需要刷新用户信息
41
+ // 免认证路由不需要刷新
55
42
  if (NO_AUTH_ROUTE_LIST.includes(location.pathname)) {
56
43
  return;
57
44
  }
58
45
 
59
- // 立即设置 loading 状态,阻止页面渲染和业务请求
46
+ // 清除之前的定时器
47
+ if (timerRef.current) {
48
+ clearTimeout(timerRef.current);
49
+ }
50
+
60
51
  setIsRefreshing(true);
61
- debouncedRefresh(location.pathname);
52
+ layoutLogger.log('Route changed, scheduling user info refresh:', location.pathname);
53
+
54
+ // 防抖 300ms
55
+ timerRef.current = setTimeout(async () => {
56
+ try {
57
+ await refreshRef.current?.();
58
+ layoutLogger.log('User info refreshed');
59
+ } finally {
60
+ setIsRefreshing(false);
61
+ }
62
+ }, 300);
62
63
 
63
64
  return () => {
64
- debouncedRefresh.cancel();
65
+ if (timerRef.current) {
66
+ clearTimeout(timerRef.current);
67
+ }
65
68
  };
66
- }, [location.pathname, debouncedRefresh]);
69
+ }, [location.pathname]);
67
70
 
68
71
  return { isRefreshing };
69
72
  }