generator-mico-cli 0.2.20 → 0.2.22

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 (60) hide show
  1. package/README.md +29 -0
  2. package/bin/mico.js +124 -5
  3. package/generators/micro-react/index.js +76 -17
  4. package/generators/micro-react/templates/.cursor/rules/always-read-docs.mdc +14 -4
  5. package/generators/micro-react/templates/.cursor/rules/layout-app.mdc +36 -26
  6. package/generators/micro-react/templates/.cursor/rules/project-overview.mdc +5 -2
  7. package/generators/micro-react/templates/CLAUDE.md +15 -7
  8. package/generators/micro-react/templates/_gitignore +2 -0
  9. package/generators/micro-react/templates/apps/layout/config/config.dev.ts +7 -3
  10. package/generators/micro-react/templates/apps/layout/config/config.ts +21 -0
  11. package/generators/micro-react/templates/apps/layout/config/routes.ts +0 -5
  12. package/generators/micro-react/templates/apps/layout/docs/common-intl.md +8 -6
  13. 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 +65 -37
  14. package/generators/micro-react/templates/apps/layout/docs/feature-/350/217/234/345/215/225/346/235/203/351/231/220/346/216/247/345/210/266.md +112 -48
  15. package/generators/micro-react/templates/apps/layout/docs/feature-/350/267/257/347/224/261/344/270/216/350/217/234/345/215/225/350/247/243/350/200/246.md +179 -0
  16. package/generators/micro-react/templates/apps/layout/docs/utils-timezone.md +4 -2
  17. package/generators/micro-react/templates/apps/layout/mock/menus.ts +89 -139
  18. package/generators/micro-react/templates/apps/layout/mock/pages.ts +83 -0
  19. package/generators/micro-react/templates/apps/layout/package.json +3 -2
  20. package/generators/micro-react/templates/apps/layout/src/app.tsx +10 -8
  21. package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +121 -58
  22. package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +35 -4
  23. package/generators/micro-react/templates/apps/layout/src/common/micro-prefetch.ts +3 -2
  24. package/generators/micro-react/templates/apps/layout/src/common/portal-data.ts +45 -0
  25. package/generators/micro-react/templates/apps/layout/src/common/request/config.ts +49 -10
  26. package/generators/micro-react/templates/apps/layout/src/common/request/interceptors.ts +1 -1
  27. package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +6 -0
  28. package/generators/micro-react/templates/apps/layout/src/common/theme.ts +0 -2
  29. package/generators/micro-react/templates/apps/layout/src/components/AppTabs/index.less +0 -1
  30. package/generators/micro-react/templates/apps/layout/src/components/AppTabs/index.tsx +4 -4
  31. package/generators/micro-react/templates/apps/layout/src/components/IconFont/index.tsx +4 -5
  32. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.less +20 -1
  33. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +4 -3
  34. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/micro-app-manager.ts +7 -1
  35. package/generators/micro-react/templates/apps/layout/src/global.less +15 -3
  36. package/generators/micro-react/templates/apps/layout/src/hooks/useMenu.ts +3 -2
  37. package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.less +30 -3
  38. package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +15 -4
  39. package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +75 -38
  40. package/generators/micro-react/templates/apps/layout/src/pages/404/index.tsx +3 -7
  41. package/generators/micro-react/templates/apps/layout/src/services/user.ts +2 -2
  42. package/generators/micro-react/templates/dev.preset.json +1 -1
  43. package/generators/micro-react/templates/package.json +2 -1
  44. package/generators/subapp-react/index.js +240 -14
  45. package/generators/subapp-react/templates/homepage/.env +2 -1
  46. package/generators/subapp-react/templates/homepage/config/config.dev.ts +9 -1
  47. package/generators/subapp-react/templates/homepage/config/config.prod.development.ts +2 -1
  48. package/generators/subapp-react/templates/homepage/config/config.prod.testing.ts +2 -1
  49. package/generators/subapp-react/templates/homepage/config/config.prod.ts +2 -1
  50. package/generators/subapp-react/templates/homepage/config/config.ts +21 -0
  51. package/generators/subapp-react/templates/homepage/config/routes.ts +1 -1
  52. package/generators/subapp-react/templates/homepage/mock/api.mock.ts +2 -2
  53. package/generators/subapp-react/templates/homepage/package.json +3 -2
  54. package/generators/subapp-react/templates/homepage/src/app.tsx +1 -1
  55. package/generators/subapp-react/templates/homepage/src/common/request.ts +2 -2
  56. package/generators/subapp-react/templates/homepage/src/global.less +2 -1
  57. package/generators/subapp-react/templates/homepage/src/pages/index.less +1 -1
  58. package/generators/subapp-react/templates/homepage/src/pages/index.tsx +27 -27
  59. package/lib/utils.js +200 -2
  60. package/package.json +1 -1
@@ -4,6 +4,7 @@ import { defineConfig } from '@umijs/max';
4
4
 
5
5
 
6
6
  import mockMenus from '../mock/menus';
7
+ import mockPages from '../mock/pages';
7
8
 
8
9
  const config: ReturnType<typeof defineConfig> = {
9
10
  publicPath: '/',
@@ -16,18 +17,21 @@ const config: ReturnType<typeof defineConfig> = {
16
17
  {
17
18
  content: `
18
19
  window.__MICO_MENUS__ = ${JSON.stringify(mockMenus)};
20
+ window.__MICO_PAGES__ = ${JSON.stringify(mockPages)};
19
21
  window.__MICO_CONFIG__ = {
20
22
  appName: '<%= projectName %>',
23
+ title: '测试样例',
24
+ logo: '',
21
25
  apiBaseUrl: '',
22
26
  defaultPath: '',
23
27
  // 免认证路由(跳过 SSO 登录),支持 /* 前缀匹配
24
28
  // noAuthRouteList: ['/*'],
25
29
  // 免权限校验路由(跳过菜单权限检查)
26
- // noPermissionRouteList: ['/task-query/*'],
30
+ noPermissionRouteList: [],
27
31
  // 不显示布局的路由(全屏页面)
28
- // noLayoutRouteList: ['/task-query/*'],
32
+ // noLayoutRouteList: [],
29
33
  // 关闭权限控制(调试用)
30
- disableAuth: true,
34
+ disableAuth: false,
31
35
  };
32
36
  `,
33
37
  },
@@ -118,6 +118,27 @@ const config: ReturnType<typeof defineConfig> = {
118
118
  */
119
119
  tailwindcss: {},
120
120
 
121
+ /**
122
+ * @name 额外 Babel Presets
123
+ * @description 使用 @mico-platform/ui 的 babel preset,实现组件与图标的按需加载
124
+ */
125
+ extraBabelPresets: ['@mico-platform/ui/babel-preset'],
126
+
127
+ /**
128
+ * @name MFSU 配置
129
+ * @description
130
+ * - exclude: theme 子路径解析;ui 尽量与主应用一起编译
131
+ * - shared: React 单例,确保 MFSU 预打包里的组件(如 Layout)与主应用共用同一份 React,否则 useContext 报 null
132
+ * @doc https://umijs.org/docs/guides/mfsu
133
+ */
134
+ mfsu: {
135
+ exclude: ['@mico-platform/theme', '@mico-platform/ui'],
136
+ shared: {
137
+ react: { singleton: true },
138
+ 'react-dom': { singleton: true },
139
+ },
140
+ },
141
+
121
142
  /**
122
143
  * @name qiankun 微前端配置
123
144
  * @description 作为主应用,动态加载子应用
@@ -5,11 +5,6 @@
5
5
  * @note Umi Max 会自动使用 src/layouts/index.tsx 作为全局布局,无需显式配置
6
6
  */
7
7
  export default [
8
- {
9
- path: '/user/login',
10
- component: './User/Login',
11
- name: '登录',
12
- },
13
8
  {
14
9
  path: '/',
15
10
  component: './Home',
@@ -10,6 +10,8 @@
10
10
  - 需要从多语言中台动态获取翻译文案
11
11
  - 希望统一管理国际化逻辑,减少重复代码
12
12
 
13
+ > **注意**:本文档描述的是 `@payment-portal/common-intl` 包的完整使用方式,涵盖了所有使用该包的应用(包括本仓库外的应用如 conversation-v2、session、workorder 等)。当前仓库仅包含 `layout` 和 `basis` 两个应用。
14
+
13
15
  ## 核心特性
14
16
 
15
17
  | 特性 | 说明 |
@@ -311,12 +313,12 @@ console.log(SUPPORTED_LOCALES); // ['zh_CN', 'en', 'ar', 'tr']
311
313
 
312
314
  目前支持以下语言:
313
315
 
314
- | 语言常量 | 值 | 说明 |
315
- | ------------ | ------- | -------- |
316
- | `LANG.ZH_CN` | `zh_CN` | 简体中文 |
317
- | `LANG.EN` | `en` | 英语 |
318
- | `LANG.AR` | `ar` | 阿拉伯语 |
319
- | `LANG.TR` | `tr` | 土耳其语 |
316
+ | 语言常量 | 值 | 说明 |
317
+ | ------------- | ------- | -------- |
318
+ | `LANG.ZH_CN` | `zh_CN` | 简体中文 |
319
+ | `LANG.EN_US` | `en` | 英语 |
320
+ | `LANG.AR_SA` | `ar` | 阿拉伯语 |
321
+ | `LANG.TR_TR` | `tr` | 土耳其语 |
320
322
 
321
323
  ### Q: 如何在代码中获取当前语言?
322
324
 
@@ -18,8 +18,8 @@
18
18
 
19
19
  ### 核心实现
20
20
 
21
- 1. 主应用通过 `window.__MICO_MENUS__` 获取菜单配置
22
- 2. 解析菜单时根据 `htmlUrl` 或 `jsUrls` 判断是否为微应用
21
+ 1. 主应用优先通过 `window.__MICO_PAGES__` 获取页面配置,无数据时降级到 `window.__MICO_MENUS__`(详见 [路由与菜单解耦](./feature-路由与菜单解耦.md))
22
+ 2. 解析页面时根据 `htmlUrl` 或 `jsUrls` 判断是否为微应用
23
23
  3. 路由匹配时,微应用类型使用 `MicroAppLoader` 组件加载
24
24
  4. `MicroAppLoader` 使用 qiankun 的 `loadMicroApp` API 动态挂载子应用
25
25
  5. 组件卸载时自动调用 `unmount()` 清理子应用
@@ -28,16 +28,16 @@
28
28
 
29
29
  ### 核心文件
30
30
 
31
- | 文件路径 | 说明 |
32
- | --------------------------------------------------- | -------------------------------- |
33
- | `src/components/MicroAppLoader/index.tsx` | qiankun 微应用加载器组件 |
34
- | `src/components/MicroAppLoader/container-manager.ts`| 容器生命周期管理、会话 ID 机制 |
35
- | `src/components/MicroAppLoader/index.less` | 加载器样式 |
36
- | `src/common/menu/parser.ts` | 菜单解析,包含加载类型判断 |
37
- | `src/common/menu/types.ts` | 类型定义 |
38
- | `src/layouts/index.tsx` | 主布局,集成微应用渲染 |
39
- | `src/app.tsx` | qiankun 全局错误处理 |
40
- | `config/config.ts` | qiankun master 配置 |
31
+ | 文件路径 | 说明 |
32
+ | --- | --- |
33
+ | `src/components/MicroAppLoader/index.tsx` | qiankun 微应用加载器组件 |
34
+ | `src/components/MicroAppLoader/micro-app-manager.ts` | 微应用管理器(单例),稳定容器 + 实例缓存 + 操作序列号 |
35
+ | `src/components/MicroAppLoader/index.less` | 加载器样式 |
36
+ | `src/common/menu/parser.ts` | 菜单解析,包含加载类型判断 |
37
+ | `src/common/menu/types.ts` | 类型定义 |
38
+ | `src/layouts/index.tsx` | 主布局,集成微应用渲染 |
39
+ | `src/app.tsx` | qiankun 全局错误处理 |
40
+ | `config/config.ts` | qiankun master 配置 |
41
41
 
42
42
  ## API / 组件接口
43
43
 
@@ -334,6 +334,7 @@ const config: ReturnType<typeof defineConfig> = {
334
334
  externals: {
335
335
  react: 'window.React',
336
336
  'react-dom': 'window.ReactDOM',
337
+ 'react-dom/client': 'window.ReactDOMClient',
337
338
  '@mico-platform/ui': 'window.micoUI',
338
339
  },
339
340
  };
@@ -421,6 +422,8 @@ export default function HomePage() {
421
422
 
422
423
  ## 相关文档
423
424
 
425
+ - [路由与菜单解耦](./feature-路由与菜单解耦.md) - 路由注册与菜单导航数据源分离
426
+ - [菜单权限控制](./feature-菜单权限控制.md) - sideMenus 白名单权限逻辑
424
427
  - [主题色切换](./feature-主题色切换.md) - 主题系统实现与子应用适配
425
428
  - [请求模块架构](./arch-请求模块.md) - HTTP 请求层模块化设计
426
429
  - [日志与常量](./arch-日志与常量.md) - Logger 工具与常量管理
@@ -462,52 +465,77 @@ addGlobalUncaughtErrorHandler((event: Event | string) => {
462
465
  });
463
466
  ```
464
467
 
465
- ### container-manager API
468
+ ### MicroAppManager API (micro-app-manager.ts)
466
469
 
467
- 容器管理器提供会话 ID 机制防止并发加载冲突:
470
+ `MicroAppManager` 是单例类,管理微应用的加载、缓存、切换和卸载:
468
471
 
469
472
  ```typescript
470
- /** 开始加载会话,返回唯一会话 ID */
471
- function startLoadSession(appName: string): number;
473
+ interface MicroAppConfig {
474
+ name: string;
475
+ entry: string;
476
+ target: HTMLElement; // 目标挂载位置
477
+ props: Record<string, unknown>;
478
+ }
479
+
480
+ interface MicroAppState {
481
+ loading: boolean;
482
+ error: string | null;
483
+ mounted: boolean;
484
+ }
485
+
486
+ const manager = microAppManager; // 单例,导出即用
472
487
 
473
- /** 检查会话是否仍然有效(被新加载覆盖时返回 false) */
474
- function isLoadSessionValid(appName: string, sessionId: number): boolean;
488
+ /** 切换到指定微应用(已挂载则仅更新 props) */
489
+ manager.switchTo(config: MicroAppConfig): void;
475
490
 
476
- /** 标记加载完成 */
477
- function markLoadComplete(appName: string, sessionId: number): boolean;
491
+ /** 更新当前已挂载微应用的 props */
492
+ manager.updateProps(props: Record<string, unknown>): void;
478
493
 
479
- /** 等待当前卸载操作完成(如果有) */
480
- async function waitForUnmount(appName: string): Promise<void>;
494
+ /** 取消待处理的请求 */
495
+ manager.cancel(): void;
481
496
 
482
- /** 获取当前加载状态 */
483
- function getLoadingStatus(appName: string): 'idle' | 'loading' | 'mounted' | 'unmounting';
497
+ /** 清除所有缓存实例 */
498
+ manager.clearCache(): Promise<void>;
499
+
500
+ /** 获取调试信息 */
501
+ manager.getDebugInfo(): object;
502
+
503
+ /** 设置状态变化回调 */
504
+ manager.setStateCallback(callback: StateChangeCallback | null): void;
484
505
  ```
485
506
 
507
+ ### 核心设计
508
+
509
+ 1. **稳定容器**:容器在 `document.body` 中创建,激活时移到目标元素内,停用时移回 body 隐藏,不受 React 生命周期影响
510
+ 2. **实例缓存**:每个 entry 只 `loadMicroApp` 一次,后续切换复用已有实例(mount/unmount)
511
+ 3. **操作序列号**:通过递增的 `operationSeq` 检测过期操作,替代旧的会话 ID 机制
512
+ 4. **自动路由守卫**:由独立的 `route-guard.ts` 自动检测用户意图,业务代码无需感知
513
+
486
514
  ### 快速切换时序
487
515
 
488
516
  ```
489
517
  场景:A → B → A 快速切换
490
518
 
491
- A1 开始加载 (sessionId=1)
492
-
493
- 切换到 B,A1 cleanup 在 queueMicrotask 中执行
519
+ switchTo(A):operationSeq=1,开始 loadMicroApp
494
520
 
495
- 切换回 A,A2 开始加载 (sessionId=2)
521
+ switchTo(B):operationSeq=2,A pendingRequest 被替换
496
522
 
497
- A2 调用 waitForUnmount(),等待 A1 完全卸载
523
+ A processRequest 在异步步骤中检测 shouldAbort(mySeq=1) true
498
524
 
499
- A1 卸载完成(带 10 秒超时保护)
525
+ A 等待 mountPromise 完成后执行 safeUnmount,容器移回 body
500
526
 
501
- A2 继续加载,每个异步步骤检查 isLoadSessionValid(sessionId)
527
+ B 开始 processRequest,检查缓存 → 无缓存 → loadMicroApp
502
528
 
503
- 加载完成后立即同步最新 Props(locale/timezone 等)
529
+ B 加载完成后同步最新 Props(locale/timezone/routePath 等)
504
530
  ```
505
531
 
506
532
  ### 设计决策
507
533
 
508
534
  | 决策点 | 选择 | 理由 |
509
- |--------|------|------|
510
- | 卸载时机 | `queueMicrotask` | 在当前事件循环末尾执行,比 `requestAnimationFrame` 更快更可靠 |
511
- | 并发控制 | 会话 ID 机制 | 简单高效,无需复杂的锁机制 |
512
- | 超时时间 | 10 | 平衡等待时间与异常检测速度 |
513
- | Props 同步 | 加载后立即同步 | 确保加载期间的变化不丢失 |
535
+ | --- | --- | --- |
536
+ | 容器策略 | 稳定容器(body 中创建,移动挂载) | 不受 React 生命周期影响,避免容器被意外销毁 |
537
+ | 并发控制 | 操作序列号 + pendingRequest 队列 | 简单高效,新请求自动覆盖旧请求 |
538
+ | 实例缓存 | loadPromise 完成后立即缓存 | 即使被 abort 也可复用,避免重复加载 |
539
+ | unmount 安全 | 等待 mountPromise 完成后再 unmount | 避免 single-spa error #32 |
540
+ | 超时保护 | load 30s / mount 30s / unmount 10s | 平衡等待时间与异常检测速度 |
541
+ | Props 同步 | mount 完成后立即 safeUpdate | 确保加载期间的变化不丢失 |
@@ -1,6 +1,6 @@
1
1
  # 菜单权限控制
2
2
 
3
- > 创建时间:2026-01-24 更新时间:2026-01-27
3
+ > 创建时间:2026-01-24 更新时间:2026-02-08
4
4
 
5
5
  ## 功能概述
6
6
 
@@ -19,12 +19,12 @@
19
19
 
20
20
  ### 四种路由场景
21
21
 
22
- | 场景 | noAuthRouteList | noPermissionRouteList | 示例 |
23
- | --- | --- | --- | --- |
24
- | 公开页面 | ✅ | ✅ | 首页、活动页 |
25
- | 登录后公共页面 | ❌ | ✅ | 个人设置、帮助中心 |
26
- | 需要权限的页面 | ❌ | ❌ | 后台管理、业务功能 |
27
- | 登录页等特殊页面 | ✅ | ✅ | /user/login、/403 |
22
+ | 场景 | noAuthRouteList | noPermissionRouteList | accessControlEnabled | 示例 |
23
+ | --- | --- | --- | --- | --- |
24
+ | 公开页面 | ✅ | ✅ | `false` | 首页、活动页 |
25
+ | 登录后公共页面 | ❌ | ✅ | - | 个人设置、帮助中心 |
26
+ | 需要权限的页面 | ❌ | ❌ | `true` | 后台管理、业务功能 |
27
+ | 登录页等特殊页面 | ✅ | ✅ | `false` | /login、/403 |
28
28
 
29
29
  ## 技术方案
30
30
 
@@ -40,22 +40,28 @@
40
40
  用户访问路由
41
41
 
42
42
 
43
- ┌─────────────────────────────────┐
44
- │ 1. SSO 认证检查 (app.tsx)
45
- │ isNoAuthRoute(pathname)?
46
- │ ├── 是 → 跳过 SSO
47
- └── → 执行 ensureSsoSession()
48
- └─────────────────────────────────┘
43
+ ┌──────────────────────────────────────────────┐
44
+ │ 1. SSO 认证检查 (app.tsx)
45
+ │ isNoAuthRoute(pathname)?
46
+ │ ├── 是 → 跳过 SSO
47
+ page.accessControlEnabled === false? │
48
+ │ ├── 是 → 跳过 SSO(PAGES 数据驱动) │
49
+ │ └── 否 → 执行 ensureSsoSession() │
50
+ └──────────────────────────────────────────────┘
49
51
 
50
52
 
51
- ┌─────────────────────────────────┐
52
- │ 2. 权限校验 (layouts/index.tsx)
53
- │ isNoPermissionRoute(pathname)?
54
- │ ├── 是 → 跳过权限校验
55
- │ └── 否 → 检查 side_menus
56
- ├── 有权限 → 正常渲染
57
- └── 无权限显示 403
58
- └─────────────────────────────────┘
53
+ ┌──────────────────────────────────────────┐
54
+ │ 2. 权限校验 (layouts/index.tsx)
55
+ │ isNoPermissionRoute(pathname)?
56
+ │ ├── 是 → 跳过权限校验
57
+ │ └── 否 → 双层权限判断
58
+ Tier 1: 页面在菜单中? │
59
+ ├── 沿用 sideMenus │
60
+ │ │ 白名单逻辑 │
61
+ │ └── 否 → Tier 2: 页面级权限 │
62
+ │ adminOnly / routeKey│
63
+ │ 详见:路由与菜单解耦文档 │
64
+ └──────────────────────────────────────────┘
59
65
 
60
66
 
61
67
  ┌─────────────────────────────────┐
@@ -66,12 +72,16 @@
66
72
  └─────────────────────────────────┘
67
73
 
68
74
 
69
- ┌─────────────────────────────────┐
70
- │ 4. 子应用加载 (MicroAppLoader)
71
- isNoPermissionRoute(pathname)?
72
- │ ├── 是 → 直接加载
73
- └── 否 → 等待 currentUser
74
- └─────────────────────────────────┘
75
+ ┌──────────────────────────────────────────────┐
76
+ │ 4. 子应用加载 (MicroAppLoader)
77
+ page.accessControlEnabled === false?
78
+ │ ├── 是 → 直接加载(PAGES 数据驱动)
79
+ isNoAuthRoute(pathname)?
80
+ │ ├── 是 → 直接加载(静态配置兜底) │
81
+ │ isNoPermissionRoute(pathname)? │
82
+ │ ├── 是 → 直接加载 │
83
+ │ └── 否 → 等待 currentUser │
84
+ └──────────────────────────────────────────────┘
75
85
  ```
76
86
 
77
87
  ### 权限判断逻辑(详细)
@@ -84,18 +94,39 @@
84
94
  ├── 是 → 允许所有访问(调试模式)
85
95
 
86
96
 
97
+ page.accessControlEnabled === false?
98
+ ├── 是 → 跳过 SSO 认证 + 跳过权限校验
99
+
100
+
87
101
  是否在 noPermissionRouteList 中?
88
102
  ├── 是 → 允许访问,显示全部菜单
89
103
 
90
104
 
105
+ 是否是非动态路由(不在 PAGES 中)?
106
+ ├── 是 → 交给 Umi 处理(404 等静态路由)
107
+
108
+
91
109
  是否是超级用户?
92
- ├── 是 → 允许访问所有菜单
110
+ ├── 是 → 允许访问所有页面
111
+
112
+
113
+ Tier 1: 页面在菜单中?
114
+ ├── 是 → 沿用菜单权限逻辑:
115
+ │ 菜单项 adminOnly === true?
116
+ │ ├── 是 → 禁止访问
117
+ │ 检查 side_menus 白名单
118
+ │ ├── 匹配 → 允许访问
119
+ │ └── 不匹配 → 显示 403
93
120
 
94
121
 
95
- 检查 side_menus 白名单
96
- ├── 精确匹配:menuPath === side_menus[i]
97
- ├── 前缀匹配:side_menus[i].startsWith(menuPath + '.')
98
- └── 都不匹配 → 禁止访问,显示 403
122
+ Tier 2: 隐藏页面(不在菜单中)
123
+ adminOnly === true?
124
+ ├── 显示 403
125
+ accessControlEnabled === true?
126
+ ├── 是 → 检查 routeKey ∈ side_menus
127
+ │ ├── 匹配 → 允许访问
128
+ │ └── 不匹配 → 显示 403
129
+ └── 否 → 允许访问
99
130
  ```
100
131
 
101
132
  ## 文件清单
@@ -241,24 +272,36 @@ export const NO_PERMISSION_ROUTE_LIST: string[] = ['/403', '/404'];
241
272
  │ └── 配置队列 ✅ 精确匹配 "列队管理.配置队列"
242
273
  ├── 质量管理 ❌ 不在白名单
243
274
  │ └── 抽样检查 ❌ 不在白名单
244
- └── 权限管理 ❌ 硬编码禁止(非超级用户)
275
+ └── 权限管理 ❌ adminOnly=true,非超级用户不可见
245
276
  ```
246
277
 
247
278
  ### Layout 中的权限判断
248
279
 
249
280
  ```tsx
250
- // layouts/index.tsx
281
+ // layouts/index.tsx — 双层权限判断
251
282
  const isForbidden = useMemo(() => {
252
- // 关闭权限控制时,不校验权限
253
283
  if (isAuthDisabled()) return false;
254
- // 免权限校验路由,不检查菜单权限
255
284
  if (isNoPermissionRoute(location.pathname)) return false;
256
- // 如果在有权限的路由中找到了,说明有权限
257
- if (currentRoute) return false;
258
- // 在所有路由中存在但无权限
259
- const routeInAll = findRouteByPath(allRoutes, location.pathname);
260
- return !!routeInAll;
261
- }, [currentRoute, allRoutes, location.pathname]);
285
+ if (!currentRoute) return false; // 非动态路由
286
+ if (isSuperuserUser(currentUser?.is_superuser)) return false;
287
+
288
+ // Tier 1: 菜单权限交叉引用
289
+ const inAllMenu = findRouteByPath(allMenuRoutes, location.pathname);
290
+ if (inAllMenu) {
291
+ return !findRouteByPath(allowedMenuRoutes, location.pathname);
292
+ }
293
+
294
+ // Tier 2: 隐藏页面级权限
295
+ if (!hasWindowPages()) return false;
296
+ const page = findPageByPath(getWindowPages(), location.pathname);
297
+ if (!page) return false;
298
+ if (page.adminOnly) return true;
299
+ if (page.accessControlEnabled) {
300
+ const sideMenus = (currentUser?.side_menus || []) as string[];
301
+ return !page.routeKey || !sideMenus.includes(page.routeKey);
302
+ }
303
+ return false;
304
+ }, [...]);
262
305
  ```
263
306
 
264
307
  ### MicroAppLoader 中的认证判断
@@ -267,6 +310,8 @@ const isForbidden = useMemo(() => {
267
310
  // components/MicroAppLoader/index.tsx
268
311
  const isAuthReady =
269
312
  isAuthDisabled() ||
313
+ isPageAuthFree(location.pathname) ||
314
+ isNoAuthRoute(location.pathname) ||
270
315
  isNoPermissionRoute(location.pathname) ||
271
316
  !!initialState?.currentUser;
272
317
 
@@ -276,18 +321,34 @@ if (!isAuthReady) {
276
321
  }
277
322
  ```
278
323
 
324
+ 判断优先级:
325
+ 1. `isAuthDisabled()` — 全局关闭权限,直接放行
326
+ 2. `isPageAuthFree` — **PAGES 数据驱动**,页面 `accessControlEnabled === false` 时跳过认证和权限校验
327
+ 3. `isNoAuthRoute()` — 静态配置兜底,免认证路由(PAGES 未注入时的降级保护)
328
+ 4. `isNoPermissionRoute()` — 免权限路由(如 403/404),无需等待 currentUser
329
+ 5. `!!initialState?.currentUser` — 已登录,有用户信息
330
+
331
+ **注意**:`accessControlEnabled === false` 同时影响三个阶段:
332
+
333
+ - SSO 认证(`app.tsx`):跳过 `ensureSsoSession()` 和 `handleAuthFailureRedirect()`
334
+ - 权限校验(`layouts/index.tsx`):Tier 2 隐藏页面默认放行
335
+ - 子应用加载(`MicroAppLoader`):不等待 currentUser 直接加载
336
+
279
337
  ## 设计决策
280
338
 
281
339
  | 决策点 | 选择 | 理由 |
282
340
  | --- | --- | --- |
341
+ | accessControlEnabled 统一控制 | `false` 同时跳过认证和授权 | PAGES 数据驱动,一个字段即可标记公开页面,无需重复配置 noAuthRouteList |
283
342
  | 认证与授权分离 | 两个独立配置项 | 不同场景需要不同组合,如"需要登录但不需要权限"的个人设置页 |
284
343
  | 权限模型 | 白名单 (`side_menus`) | 后端返回的 `side_menus` 是允许列表,比黑名单更安全 |
285
344
  | 403 处理 | 原地渲染组件 | 保持 URL 不变,用户体验更好,便于分享链接 |
286
345
  | 父级菜单显示 | 前缀匹配 | 子菜单有权限时,父级菜单需要作为容器显示 |
287
346
  | 超级用户 | 跳过所有检查 | 管理员需要完整访问权限 |
288
347
  | 免权限路由的菜单 | 显示全部 | 用户访问公开页面时应能看到所有导航选项 |
348
+ | 免认证路由的子应用 | 直接加载 | 不等待 currentUser,免认证路由本身不需要登录 |
289
349
  | 免权限路由的子应用 | 直接加载 | 不等待 currentUser,避免加载卡住 |
290
350
  | 静态路由不受权限控制 | 默认允许访问 | 见下方"静态路由与动态路由"说明 |
351
+ | adminOnly 判断 | 读取菜单数据字段 | 替代硬编码菜单名称匹配,由后端数据驱动,支持多语言且无需前端维护 |
291
352
 
292
353
  ## 静态路由与动态路由
293
354
 
@@ -296,7 +357,7 @@ if (!isAuthReady) {
296
357
  | 类型 | 来源 | 示例 |
297
358
  | --- | --- | --- |
298
359
  | **静态路由** | 代码中定义 (`config/routes.ts`) | `/`, `/403`, `/404`, `/user/login` |
299
- | **动态路由** | 后端菜单配置 (`window.__MICO_MENUS__`) | `/queue-management`, `/quality-check` |
360
+ | **动态路由** | 后端页面配置 (`window.__MICO_PAGES__`,降级 `window.__MICO_MENUS__`) | `/queue-management`, `/quality-check` |
300
361
 
301
362
  ### 权限控制范围
302
363
 
@@ -306,9 +367,11 @@ if (!isAuthReady) {
306
367
  权限判断逻辑(isForbidden):
307
368
  1. 检查 isAuthDisabled() → 关闭则全部允许
308
369
  2. 检查 isNoPermissionRoute() → 在免权限列表中则允许
309
- 3. 检查 currentRoute(有权限路由) 找到则允许
310
- 4. 检查 routeInAll(所有动态路由)存在则返回 403
311
- 5. 都不匹配交给 Umi 处理(静态路由走这里)
370
+ 3. 检查 currentRoute(PAGES 中的路由)→ 不存在则非动态路由,交给 Umi
371
+ 4. 检查 isSuperuserUser()超级用户放行
372
+ 5. Tier 1: 在菜单中 检查 allowedMenuRoutes
373
+ 6. Tier 2: 不在菜单(隐藏页面)→ 检查 adminOnly + accessControlEnabled + routeKey
374
+ 7. 都不匹配 → 放行
312
375
  ```
313
376
 
314
377
  ### 设计理由
@@ -346,10 +409,11 @@ if (!isAuthReady) {
346
409
  - `side_menus` 格式为菜单路径,如 `"列队管理.配置队列"`
347
410
  - `miss_permissions` 用于按钮级别权限控制,不影响菜单显示
348
411
  - 403 页面在 Layout 内渲染,不会触发路由跳转
349
- - 调试时可在控制台搜索 `isForbidden check` 查看权限判断日志
412
+ - 调试时可在控制台搜索 `isForbidden (menu check)` 或 `isForbidden (hidden page` 查看权限判断日志
350
413
  - **常见问题**:配置了 `noAuthRouteList` 但页面仍显示 403,需同时配置 `noPermissionRouteList`
351
414
 
352
415
  ## 相关文档
353
416
 
354
- - [微前端模式](./feature-微前端模式.md) - 路由和菜单解析
417
+ - [路由与菜单解耦](./feature-路由与菜单解耦.md) - 路由注册与菜单导航数据源分离、双层权限详细说明
418
+ - [微前端模式](./feature-微前端模式.md) - 微应用加载机制
355
419
  - [日志与常量](./arch-日志与常量.md) - 常量管理