generator-mico-cli 0.2.21 → 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 (41) hide show
  1. package/generators/micro-react/templates/.cursor/rules/always-read-docs.mdc +14 -4
  2. package/generators/micro-react/templates/.cursor/rules/layout-app.mdc +36 -26
  3. package/generators/micro-react/templates/.cursor/rules/project-overview.mdc +5 -2
  4. package/generators/micro-react/templates/CLAUDE.md +4 -2
  5. package/generators/micro-react/templates/apps/layout/config/config.dev.ts +7 -3
  6. package/generators/micro-react/templates/apps/layout/config/routes.ts +0 -5
  7. 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 +5 -2
  8. 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 +107 -48
  9. 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
  10. package/generators/micro-react/templates/apps/layout/mock/menus.ts +89 -144
  11. package/generators/micro-react/templates/apps/layout/mock/pages.ts +83 -0
  12. package/generators/micro-react/templates/apps/layout/src/app.tsx +10 -8
  13. package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +118 -43
  14. package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +31 -4
  15. package/generators/micro-react/templates/apps/layout/src/common/micro-prefetch.ts +3 -2
  16. package/generators/micro-react/templates/apps/layout/src/common/portal-data.ts +45 -0
  17. package/generators/micro-react/templates/apps/layout/src/common/request/config.ts +49 -10
  18. package/generators/micro-react/templates/apps/layout/src/common/request/interceptors.ts +1 -1
  19. package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +6 -0
  20. package/generators/micro-react/templates/apps/layout/src/common/theme.ts +0 -2
  21. package/generators/micro-react/templates/apps/layout/src/components/AppTabs/index.less +0 -1
  22. package/generators/micro-react/templates/apps/layout/src/components/AppTabs/index.tsx +4 -4
  23. package/generators/micro-react/templates/apps/layout/src/components/IconFont/index.tsx +4 -5
  24. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.less +20 -1
  25. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +4 -3
  26. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/micro-app-manager.ts +7 -1
  27. package/generators/micro-react/templates/apps/layout/src/global.less +15 -2
  28. package/generators/micro-react/templates/apps/layout/src/hooks/useMenu.ts +3 -2
  29. package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.less +30 -3
  30. package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +15 -4
  31. package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +75 -38
  32. package/generators/micro-react/templates/apps/layout/src/pages/404/index.tsx +3 -7
  33. package/generators/micro-react/templates/apps/layout/src/services/user.ts +2 -2
  34. package/generators/micro-react/templates/dev.preset.json +1 -1
  35. package/generators/subapp-react/index.js +160 -2
  36. package/generators/subapp-react/templates/homepage/.env +2 -1
  37. package/generators/subapp-react/templates/homepage/config/config.dev.ts +1 -0
  38. package/generators/subapp-react/templates/homepage/config/config.prod.development.ts +2 -1
  39. package/generators/subapp-react/templates/homepage/config/config.prod.testing.ts +2 -1
  40. package/generators/subapp-react/templates/homepage/config/config.prod.ts +2 -1
  41. package/package.json +1 -1
@@ -20,8 +20,15 @@ docs/
20
20
  ### 2. Layout 应用文档 (`/apps/layout/docs/`)
21
21
  ```
22
22
  apps/layout/docs/
23
- ├── feature-微前端模式.md # qiankun 微前端架构
24
- └── feature-主题色切换.md # 主题系统实现
23
+ ├── feature-微前端模式.md # qiankun 微前端架构
24
+ ├── feature-路由与菜单解耦.md # PAGES/MENUS 数据源分离、双层权限
25
+ ├── feature-菜单权限控制.md # sideMenus 白名单、认证与授权分离
26
+ ├── feature-主题色切换.md # 主题系统实现
27
+ ├── feature-404页面.md # 404 页面
28
+ ├── arch-请求模块.md # HTTP 请求层模块化设计
29
+ ├── arch-日志与常量.md # Logger 工具与常量管理
30
+ ├── common-intl.md # 国际化公共包
31
+ └── utils-timezone.md # 时区工具
25
32
  ```
26
33
 
27
34
  ### 3. 其他应用文档
@@ -33,8 +40,9 @@ apps/layout/docs/
33
40
 
34
41
  ### Step 1: 确认需求涉及的模块
35
42
  - 微前端相关 → 阅读 `apps/layout/docs/feature-微前端模式.md`
43
+ - 路由/菜单/权限相关 → 阅读 `apps/layout/docs/feature-路由与菜单解耦.md` 和 `apps/layout/docs/feature-菜单权限控制.md`
36
44
  - 主题/样式相关 → 阅读 `apps/layout/docs/feature-主题色切换.md`
37
- - 请求/认证相关 → 阅读 `src/common/request/` 和 `src/common/auth/` 源码
45
+ - 请求/认证相关 → 阅读 `apps/layout/docs/arch-请求模块.md` 和 `src/common/auth/` 源码
38
46
  - 菜单相关 → 阅读 `src/common/menu/` 源码
39
47
 
40
48
  ### Step 2: 查找相关文档
@@ -54,9 +62,11 @@ find . -name "*.md" -type f | grep -v node_modules
54
62
  | 需求类型 | 必读文档 |
55
63
  |---------|---------|
56
64
  | 新建子应用 | `apps/layout/docs/feature-微前端模式.md` |
65
+ | 路由/页面配置 | `apps/layout/docs/feature-路由与菜单解耦.md` |
66
+ | 权限控制 | `apps/layout/docs/feature-菜单权限控制.md` |
57
67
  | 主题适配 | `apps/layout/docs/feature-主题色切换.md` |
58
68
  | 提交代码 | `docs/commit-message.md` |
59
- | API 请求 | `src/common/request/index.ts` 源码注释 |
69
+ | API 请求 | `apps/layout/docs/arch-请求模块.md` |
60
70
  | 认证登录 | `src/common/auth/` 目录源码 |
61
71
  | 菜单配置 | `src/common/menu/types.ts` 类型定义 |
62
72
 
@@ -63,24 +63,27 @@ apps/layout/
63
63
  const BasicLayout: React.FC = () => {
64
64
  const location = useLocation();
65
65
 
66
- // 解析路由配置
67
- const routes = useMemo(() => {
68
- const menus = getWindowMenus();
69
- return extractRoutes(menus);
66
+ // 所有页面路由(优先 PAGES,降级 MENUS)— 用于路由匹配和渲染
67
+ const allPageRoutes = useMemo(() => {
68
+ return getDynamicRoutes();
70
69
  }, []);
71
70
 
72
- // 查找当前路由配置
71
+ // 菜单路由(从 MENUS)— 用于权限交叉引用
72
+ const allMenuRoutes = useMemo(() => {
73
+ return extractRoutes(getMenus());
74
+ }, []);
75
+
76
+ // 当前路由配置(从所有页面路由中查找)
73
77
  const currentRoute = useMemo(() => {
74
- return findRouteByPath(routes, location.pathname);
75
- }, [routes, location.pathname]);
78
+ return findRouteByPath(allPageRoutes, location.pathname);
79
+ }, [allPageRoutes, location.pathname]);
76
80
 
77
81
  // 渲染页面内容
78
82
  const renderContent = () => {
79
- // 微应用类型使用 MicroAppLoader
80
83
  if (currentRoute?.loadType === 'microapp' && currentRoute.entry) {
81
- return <MicroAppLoader entry={currentRoute.entry} name={currentRoute.path} />;
84
+ const appName = getAppNameFromEntry(currentRoute.entry);
85
+ return <MicroAppLoader entry={currentRoute.entry} name={appName} />;
82
86
  }
83
- // 内部路由使用 Outlet
84
87
  return <Outlet />;
85
88
  };
86
89
 
@@ -98,28 +101,36 @@ const BasicLayout: React.FC = () => {
98
101
  };
99
102
  ```
100
103
 
101
- ### 2. 菜单系统 (common/menu/)
104
+ ### 2. 全局数据源 (common/portal-data.ts) + 菜单系统 (common/menu/)
102
105
 
103
106
  ```typescript
104
- // 获取菜单数据
105
- const menus = getWindowMenus();
107
+ // 获取页面列表 (window.__MICO_PAGES__)
108
+ const pages = getPages();
109
+
110
+ // 获取菜单树 (window.__MICO_MENUS__)
111
+ const menus = getMenus();
106
112
 
107
- // 提取路由配置
108
- const routes = extractRoutes(menus);
113
+ // 从页面列表提取动态路由(优先数据源)
114
+ const routes = getDynamicRoutes();
109
115
 
110
- // 解析菜单项
116
+ // 从菜单树提取路由(用于权限交叉引用)
117
+ const menuRoutes = extractRoutes(menus);
118
+
119
+ // 解析菜单项(用于侧边栏渲染)
111
120
  const menuItems = parseMenuItems(menus);
112
121
 
113
122
  // 查找当前路由
114
123
  const currentRoute = findRouteByPath(routes, pathname);
124
+
125
+ // 通过 pageId 查找页面(O(1))
126
+ const page = getPageById(pageId);
115
127
  ```
116
128
 
117
129
  ### 3. 动态路由 (app.tsx)
118
130
 
119
131
  ```typescript
120
132
  export function patchClientRoutes({ routes }: { routes: any[] }) {
121
- const menus = getWindowMenus();
122
- const dynamicRoutes = extractRoutes(menus);
133
+ const dynamicRoutes = getDynamicRoutes(); // 从 __MICO_PAGES__ 获取
123
134
 
124
135
  const rootRoute = routes.find((r) => r.path === '/');
125
136
  dynamicRoutes.forEach((route) => {
@@ -128,9 +139,7 @@ export function patchClientRoutes({ routes }: { routes: any[] }) {
128
139
  name: route.name,
129
140
  meta: {
130
141
  loadType: route.loadType,
131
- htmlUrl: route.htmlUrl,
132
- jsUrls: route.jsUrls,
133
- cssUrls: route.cssUrls,
142
+ entry: route.entry,
134
143
  },
135
144
  });
136
145
  });
@@ -265,8 +274,9 @@ const { collapsed } = useModel('global');
265
274
 
266
275
  ## 注意事项
267
276
 
268
- 1. **菜单数据来源**: 通过 `window.__MICO_MENUS__` 注入
269
- 2. **路由动态生成**: 在 `patchClientRoutes` 中根据菜单生成路由
270
- 3. **微应用判断**: 根据 `htmlUrl` 或 `jsUrls` 判断是否为微应用
271
- 4. **主题初始化**: `app.tsx` 中调用 `initTheme()` 避免闪烁
272
- 5. **认证共享**: 子应用使用主应用传递的 `request` 实例
277
+ 1. **数据来源**: 页面列表通过 `window.__MICO_PAGES__` 注入,菜单树通过 `window.__MICO_MENUS__` 注入
278
+ 2. **路由动态生成**: 在 `patchClientRoutes` 中优先从 `__MICO_PAGES__` 生成路由
279
+ 3. **权限交叉引用**: 菜单路由(`__MICO_MENUS__`)用于权限判断,页面路由(`__MICO_PAGES__`)用于渲染
280
+ 4. **微应用判断**: 根据 `htmlUrl` `jsUrls` 判断是否为微应用
281
+ 5. **主题初始化**: `app.tsx` 中调用 `initTheme()` 避免闪烁
282
+ 6. **认证共享**: 子应用使用主应用传递的 `request` 实例
@@ -61,15 +61,16 @@ alwaysApply: true
61
61
  - **关键文件**:
62
62
  - `src/layouts/index.tsx` - 主布局组件
63
63
  - `src/components/MicroAppLoader/` - 微应用加载器
64
- - `src/common/menu/parser.ts` - 菜单解析与路由提取
64
+ - `src/common/menu/parser.ts` - 菜单解析、页面数据提取与路由生成
65
65
  - `src/common/request/index.ts` - 统一请求封装
66
66
  - `src/hooks/useTheme.ts` - 主题管理 Hook
67
67
 
68
68
  ### 2. 微前端架构
69
- - 菜单数据通过 `window.__MICO_MENUS__` 注入
69
+ - 路由注册优先使用 `window.__MICO_PAGES__`(页面列表),无数据时降级到 `window.__MICO_MENUS__`(菜单树)
70
70
  - 根据 `htmlUrl` 或 `jsUrls` 判断是否为微应用
71
71
  - 使用 `loadMicroApp` API 动态挂载子应用
72
72
  - 子应用共享主应用的 `request` 实例
73
+ - 权限通过菜单交叉引用 + 页面级兜底的双层策略控制
73
74
 
74
75
  ### 3. 主题系统
75
76
  - 支持亮色/暗黑主题切换
@@ -123,6 +124,8 @@ Jenkins 根据环境执行 `CICD/` 下的构建脚本:
123
124
  ## 相关文档
124
125
 
125
126
  - [微前端模式](mdc:apps/layout/docs/feature-微前端模式.md)
127
+ - [路由与菜单解耦](mdc:apps/layout/docs/feature-路由与菜单解耦.md)
128
+ - [菜单权限控制](mdc:apps/layout/docs/feature-菜单权限控制.md)
126
129
  - [主题色切换](mdc:apps/layout/docs/feature-主题色切换.md)
127
130
  - [提交规范](mdc:docs/commit-message.md)
128
131
  - [部署说明](mdc:deployDesc.md)
@@ -108,8 +108,8 @@ Logger 工具在 `src/common/logger.ts`,常量定义在 `src/constants/index.t
108
108
  ### 认证模块
109
109
  认证逻辑在 `src/common/auth/`,核心文件 `auth-manager.ts` 管理 Token 存储和用户信息。SSO 登录在 `getInitialState()` 中完成,确保 UI 渲染前认证已就绪。
110
110
 
111
- ### 菜单系统
112
- 菜单解析与类型定义在 `src/common/menu/`。
111
+ ### 菜单与路由系统
112
+ 菜单解析与类型定义在 `src/common/menu/`。路由注册消费 `window.__MICO_PAGES__`(页面列表),菜单栏消费 `window.__MICO_MENUS__`(菜单树),两者解耦运作。权限通过菜单交叉引用 + 页面级兜底的双层策略控制。详见 [路由与菜单解耦](./apps/layout/docs/feature-路由与菜单解耦.md) 和 [菜单权限控制](./apps/layout/docs/feature-菜单权限控制.md)。
113
113
 
114
114
  ### 微前端
115
115
  layout 应用作为 qiankun 主应用,子应用通过 Umi 配置中的 qiankun 配置注册。详见 [微前端模式](./apps/layout/docs/feature-微前端模式.md)。
@@ -121,6 +121,8 @@ layout 应用作为 qiankun 主应用,子应用通过 Umi 配置中的 qiankun
121
121
  | [README.md](./README.md) | 项目入口、技术栈、常用命令 |
122
122
  | [提交规范](./docs/commit-message.md) | Git Commit 详细规范 |
123
123
  | [微前端模式](./apps/layout/docs/feature-微前端模式.md) | qiankun 架构、MicroAppLoader、子应用配置 |
124
+ | [路由与菜单解耦](./apps/layout/docs/feature-路由与菜单解耦.md) | PAGES/MENUS 数据源分离、双层权限控制 |
125
+ | [菜单权限控制](./apps/layout/docs/feature-菜单权限控制.md) | sideMenus 白名单、认证与授权分离 |
124
126
  | [主题色切换](./apps/layout/docs/feature-主题色切换.md) | useTheme Hook、CSS 变量系统 |
125
127
  | [请求模块架构](./apps/layout/docs/arch-请求模块.md) | HTTP 请求层模块化设计 |
126
128
  | [日志与常量](./apps/layout/docs/arch-日志与常量.md) | Logger 工具与常量管理 |
@@ -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
  },
@@ -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',
@@ -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()` 清理子应用
@@ -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 工具与常量管理
@@ -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,22 +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
+ ├── 是 → 允许访问所有页面
93
111
 
94
112
 
95
- 菜单项 adminOnly === true?
96
- ├── 是 → 禁止访问(仅超级管理员可见)
113
+ Tier 1: 页面在菜单中?
114
+ ├── 是 → 沿用菜单权限逻辑:
115
+ │ 菜单项 adminOnly === true?
116
+ │ ├── 是 → 禁止访问
117
+ │ 检查 side_menus 白名单
118
+ │ ├── 匹配 → 允许访问
119
+ │ └── 不匹配 → 显示 403
97
120
 
98
121
 
99
- 检查 side_menus 白名单
100
- ├── 精确匹配:menuPath === side_menus[i]
101
- ├── 前缀匹配:side_menus[i].startsWith(menuPath + '.')
102
- └── 都不匹配 → 禁止访问,显示 403
122
+ Tier 2: 隐藏页面(不在菜单中)
123
+ adminOnly === true?
124
+ ├── 显示 403
125
+ accessControlEnabled === true?
126
+ ├── 是 → 检查 routeKey ∈ side_menus
127
+ │ ├── 匹配 → 允许访问
128
+ │ └── 不匹配 → 显示 403
129
+ └── 否 → 允许访问
103
130
  ```
104
131
 
105
132
  ## 文件清单
@@ -251,18 +278,30 @@ export const NO_PERMISSION_ROUTE_LIST: string[] = ['/403', '/404'];
251
278
  ### Layout 中的权限判断
252
279
 
253
280
  ```tsx
254
- // layouts/index.tsx
281
+ // layouts/index.tsx — 双层权限判断
255
282
  const isForbidden = useMemo(() => {
256
- // 关闭权限控制时,不校验权限
257
283
  if (isAuthDisabled()) return false;
258
- // 免权限校验路由,不检查菜单权限
259
284
  if (isNoPermissionRoute(location.pathname)) return false;
260
- // 如果在有权限的路由中找到了,说明有权限
261
- if (currentRoute) return false;
262
- // 在所有路由中存在但无权限
263
- const routeInAll = findRouteByPath(allRoutes, location.pathname);
264
- return !!routeInAll;
265
- }, [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
+ }, [...]);
266
305
  ```
267
306
 
268
307
  ### MicroAppLoader 中的认证判断
@@ -271,6 +310,8 @@ const isForbidden = useMemo(() => {
271
310
  // components/MicroAppLoader/index.tsx
272
311
  const isAuthReady =
273
312
  isAuthDisabled() ||
313
+ isPageAuthFree(location.pathname) ||
314
+ isNoAuthRoute(location.pathname) ||
274
315
  isNoPermissionRoute(location.pathname) ||
275
316
  !!initialState?.currentUser;
276
317
 
@@ -280,16 +321,31 @@ if (!isAuthReady) {
280
321
  }
281
322
  ```
282
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
+
283
337
  ## 设计决策
284
338
 
285
339
  | 决策点 | 选择 | 理由 |
286
340
  | --- | --- | --- |
341
+ | accessControlEnabled 统一控制 | `false` 同时跳过认证和授权 | PAGES 数据驱动,一个字段即可标记公开页面,无需重复配置 noAuthRouteList |
287
342
  | 认证与授权分离 | 两个独立配置项 | 不同场景需要不同组合,如"需要登录但不需要权限"的个人设置页 |
288
343
  | 权限模型 | 白名单 (`side_menus`) | 后端返回的 `side_menus` 是允许列表,比黑名单更安全 |
289
344
  | 403 处理 | 原地渲染组件 | 保持 URL 不变,用户体验更好,便于分享链接 |
290
345
  | 父级菜单显示 | 前缀匹配 | 子菜单有权限时,父级菜单需要作为容器显示 |
291
346
  | 超级用户 | 跳过所有检查 | 管理员需要完整访问权限 |
292
347
  | 免权限路由的菜单 | 显示全部 | 用户访问公开页面时应能看到所有导航选项 |
348
+ | 免认证路由的子应用 | 直接加载 | 不等待 currentUser,免认证路由本身不需要登录 |
293
349
  | 免权限路由的子应用 | 直接加载 | 不等待 currentUser,避免加载卡住 |
294
350
  | 静态路由不受权限控制 | 默认允许访问 | 见下方"静态路由与动态路由"说明 |
295
351
  | adminOnly 判断 | 读取菜单数据字段 | 替代硬编码菜单名称匹配,由后端数据驱动,支持多语言且无需前端维护 |
@@ -301,7 +357,7 @@ if (!isAuthReady) {
301
357
  | 类型 | 来源 | 示例 |
302
358
  | --- | --- | --- |
303
359
  | **静态路由** | 代码中定义 (`config/routes.ts`) | `/`, `/403`, `/404`, `/user/login` |
304
- | **动态路由** | 后端菜单配置 (`window.__MICO_MENUS__`) | `/queue-management`, `/quality-check` |
360
+ | **动态路由** | 后端页面配置 (`window.__MICO_PAGES__`,降级 `window.__MICO_MENUS__`) | `/queue-management`, `/quality-check` |
305
361
 
306
362
  ### 权限控制范围
307
363
 
@@ -311,9 +367,11 @@ if (!isAuthReady) {
311
367
  权限判断逻辑(isForbidden):
312
368
  1. 检查 isAuthDisabled() → 关闭则全部允许
313
369
  2. 检查 isNoPermissionRoute() → 在免权限列表中则允许
314
- 3. 检查 currentRoute(有权限路由) 找到则允许
315
- 4. 检查 routeInAll(所有动态路由)存在则返回 403
316
- 5. 都不匹配交给 Umi 处理(静态路由走这里)
370
+ 3. 检查 currentRoute(PAGES 中的路由)→ 不存在则非动态路由,交给 Umi
371
+ 4. 检查 isSuperuserUser()超级用户放行
372
+ 5. Tier 1: 在菜单中 检查 allowedMenuRoutes
373
+ 6. Tier 2: 不在菜单(隐藏页面)→ 检查 adminOnly + accessControlEnabled + routeKey
374
+ 7. 都不匹配 → 放行
317
375
  ```
318
376
 
319
377
  ### 设计理由
@@ -351,10 +409,11 @@ if (!isAuthReady) {
351
409
  - `side_menus` 格式为菜单路径,如 `"列队管理.配置队列"`
352
410
  - `miss_permissions` 用于按钮级别权限控制,不影响菜单显示
353
411
  - 403 页面在 Layout 内渲染,不会触发路由跳转
354
- - 调试时可在控制台搜索 `isForbidden check` 查看权限判断日志
412
+ - 调试时可在控制台搜索 `isForbidden (menu check)` 或 `isForbidden (hidden page` 查看权限判断日志
355
413
  - **常见问题**:配置了 `noAuthRouteList` 但页面仍显示 403,需同时配置 `noPermissionRouteList`
356
414
 
357
415
  ## 相关文档
358
416
 
359
- - [微前端模式](./feature-微前端模式.md) - 路由和菜单解析
417
+ - [路由与菜单解耦](./feature-路由与菜单解耦.md) - 路由注册与菜单导航数据源分离、双层权限详细说明
418
+ - [微前端模式](./feature-微前端模式.md) - 微应用加载机制
360
419
  - [日志与常量](./arch-日志与常量.md) - 常量管理