generator-mico-cli 0.2.21 → 0.2.23

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 (68) hide show
  1. package/README.md +33 -0
  2. package/bin/mico.js +15 -2
  3. package/generators/micro-react/index.js +44 -6
  4. package/generators/micro-react/meta.json +2 -1
  5. package/generators/micro-react/templates/.cursor/rules/always-read-docs.mdc +14 -4
  6. package/generators/micro-react/templates/.cursor/rules/layout-app.mdc +36 -26
  7. package/generators/micro-react/templates/.cursor/rules/project-overview.mdc +5 -2
  8. package/generators/micro-react/templates/CLAUDE.md +4 -2
  9. package/generators/micro-react/templates/_gitignore +3 -1
  10. package/generators/micro-react/templates/_npmrc +1 -0
  11. package/generators/micro-react/templates/apps/layout/config/config.dev.ts +7 -3
  12. package/generators/micro-react/templates/apps/layout/config/config.prod.development.ts +5 -0
  13. package/generators/micro-react/templates/apps/layout/config/config.prod.testing.ts +5 -0
  14. package/generators/micro-react/templates/apps/layout/config/config.prod.ts +12 -0
  15. package/generators/micro-react/templates/apps/layout/config/routes.ts +0 -5
  16. 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 +4 -2
  17. 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 +116 -56
  18. 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
  19. package/generators/micro-react/templates/apps/layout/mock/api.mock.ts +81 -61
  20. package/generators/micro-react/templates/apps/layout/mock/menus.ts +89 -144
  21. package/generators/micro-react/templates/apps/layout/mock/pages.ts +83 -0
  22. package/generators/micro-react/templates/apps/layout/package.json +1 -0
  23. package/generators/micro-react/templates/apps/layout/src/app.tsx +13 -8
  24. package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +118 -43
  25. package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +31 -4
  26. package/generators/micro-react/templates/apps/layout/src/common/micro-prefetch.ts +3 -2
  27. package/generators/micro-react/templates/apps/layout/src/common/portal-data.ts +45 -0
  28. package/generators/micro-react/templates/apps/layout/src/common/request/config.ts +49 -10
  29. package/generators/micro-react/templates/apps/layout/src/common/request/interceptors.ts +1 -1
  30. package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +6 -0
  31. package/generators/micro-react/templates/apps/layout/src/common/theme.ts +0 -2
  32. package/generators/micro-react/templates/apps/layout/src/components/AppTabs/index.less +0 -1
  33. package/generators/micro-react/templates/apps/layout/src/components/AppTabs/index.tsx +4 -4
  34. package/generators/micro-react/templates/apps/layout/src/components/IconFont/index.tsx +4 -5
  35. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.less +21 -6
  36. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +4 -3
  37. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/micro-app-manager.ts +7 -1
  38. package/generators/micro-react/templates/apps/layout/src/components/RightContent/AvatarDropdown.tsx +12 -16
  39. package/generators/micro-react/templates/apps/layout/src/global.less +15 -2
  40. package/generators/micro-react/templates/apps/layout/src/hooks/useMenu.ts +3 -2
  41. package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.less +32 -4
  42. package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +20 -10
  43. package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +75 -38
  44. package/generators/micro-react/templates/apps/layout/src/pages/404/index.tsx +3 -7
  45. package/generators/micro-react/templates/apps/layout/src/services/user.ts +7 -3
  46. package/generators/micro-react/templates/dev.preset.json +1 -1
  47. package/generators/micro-react/templates/package.json +1 -0
  48. package/generators/micro-react/templates/scripts/apply-sentry-plugin.ts +45 -0
  49. package/generators/subapp-react/index.js +206 -6
  50. package/generators/subapp-react/templates/homepage/.env +2 -1
  51. package/generators/subapp-react/templates/homepage/config/config.prod.development.ts +1 -0
  52. package/generators/subapp-react/templates/homepage/config/config.prod.testing.ts +1 -0
  53. package/generators/subapp-react/templates/homepage/config/config.prod.ts +8 -0
  54. package/generators/subapp-react/templates/homepage/package.json +1 -0
  55. package/generators/subapp-react/templates/homepage/src/app.tsx +6 -0
  56. package/generators/subapp-umd/ignore-list.json +5 -0
  57. package/generators/subapp-umd/index.js +325 -0
  58. package/generators/subapp-umd/meta.json +11 -0
  59. package/generators/subapp-umd/templates/README.md +94 -0
  60. package/generators/subapp-umd/templates/package.json +35 -0
  61. package/generators/subapp-umd/templates/public/index.html +34 -0
  62. package/generators/subapp-umd/templates/src/App.less +15 -0
  63. package/generators/subapp-umd/templates/src/App.tsx +13 -0
  64. package/generators/subapp-umd/templates/src/index.ts +2 -0
  65. package/generators/subapp-umd/templates/tsconfig.json +27 -0
  66. package/generators/subapp-umd/templates/webpack.config.js +68 -0
  67. package/lib/utils.js +2 -1
  68. package/package.json +1 -1
@@ -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,38 +40,49 @@
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
- ┌─────────────────────────────────┐
62
- │ 3. 菜单渲染 (menu/index.tsx)
63
- isNoPermissionRoute(pathname)?
64
- ├── 是 → 显示全部菜单
65
- └── 按 side_menus 过滤
66
- └─────────────────────────────────┘
67
+ ┌──────────────────────────────────────────┐
68
+ │ 3. 菜单渲染 (menu/index.tsx)
69
+ 按 side_menus 过滤 │
70
+ 每个菜单项独立检查 isNoPermissionRoute
71
+ ├── 匹配该项始终显示
72
+ │ └── 不匹配 → 按 sideMenus 白名单过滤 │
73
+ └──────────────────────────────────────────┘
67
74
 
68
75
 
69
- ┌─────────────────────────────────┐
70
- │ 4. 子应用加载 (MicroAppLoader)
71
- isNoPermissionRoute(pathname)?
72
- │ ├── 是 → 直接加载
73
- └── 否 → 等待 currentUser
74
- └─────────────────────────────────┘
76
+ ┌──────────────────────────────────────────────┐
77
+ │ 4. 子应用加载 (MicroAppLoader)
78
+ page.accessControlEnabled === false?
79
+ │ ├── 是 → 直接加载(PAGES 数据驱动)
80
+ isNoAuthRoute(pathname)?
81
+ │ ├── 是 → 直接加载(静态配置兜底) │
82
+ │ isNoPermissionRoute(pathname)? │
83
+ │ ├── 是 → 直接加载 │
84
+ │ └── 否 → 等待 currentUser │
85
+ └──────────────────────────────────────────────┘
75
86
  ```
76
87
 
77
88
  ### 权限判断逻辑(详细)
@@ -84,22 +95,39 @@
84
95
  ├── 是 → 允许所有访问(调试模式)
85
96
 
86
97
 
98
+ page.accessControlEnabled === false?
99
+ ├── 是 → 跳过 SSO 认证 + 跳过权限校验
100
+
101
+
87
102
  是否在 noPermissionRouteList 中?
88
- ├── 是 → 允许访问,显示全部菜单
103
+ ├── 是 → 允许访问,菜单按各项独立判断
104
+
105
+
106
+ 是否是非动态路由(不在 PAGES 中)?
107
+ ├── 是 → 交给 Umi 处理(404 等静态路由)
89
108
 
90
109
 
91
110
  是否是超级用户?
92
- ├── 是 → 允许访问所有菜单
111
+ ├── 是 → 允许访问所有页面
93
112
 
94
113
 
95
- 菜单项 adminOnly === true?
96
- ├── 是 → 禁止访问(仅超级管理员可见)
114
+ Tier 1: 页面在菜单中?
115
+ ├── 是 → 沿用菜单权限逻辑:
116
+ │ 菜单项 adminOnly === true?
117
+ │ ├── 是 → 禁止访问
118
+ │ 检查 side_menus 白名单
119
+ │ ├── 匹配 → 允许访问
120
+ │ └── 不匹配 → 显示 403
97
121
 
98
122
 
99
- 检查 side_menus 白名单
100
- ├── 精确匹配:menuPath === side_menus[i]
101
- ├── 前缀匹配:side_menus[i].startsWith(menuPath + '.')
102
- └── 都不匹配 → 禁止访问,显示 403
123
+ Tier 2: 隐藏页面(不在菜单中)
124
+ adminOnly === true?
125
+ ├── 显示 403
126
+ accessControlEnabled === true?
127
+ ├── 是 → 检查 routeKey ∈ side_menus
128
+ │ ├── 匹配 → 允许访问
129
+ │ └── 不匹配 → 显示 403
130
+ └── 否 → 允许访问
103
131
  ```
104
132
 
105
133
  ## 文件清单
@@ -251,18 +279,30 @@ export const NO_PERMISSION_ROUTE_LIST: string[] = ['/403', '/404'];
251
279
  ### Layout 中的权限判断
252
280
 
253
281
  ```tsx
254
- // layouts/index.tsx
282
+ // layouts/index.tsx — 双层权限判断
255
283
  const isForbidden = useMemo(() => {
256
- // 关闭权限控制时,不校验权限
257
284
  if (isAuthDisabled()) return false;
258
- // 免权限校验路由,不检查菜单权限
259
285
  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]);
286
+ if (!currentRoute) return false; // 非动态路由
287
+ if (isSuperuserUser(currentUser?.is_superuser)) return false;
288
+
289
+ // Tier 1: 菜单权限交叉引用
290
+ const inAllMenu = findRouteByPath(allMenuRoutes, location.pathname);
291
+ if (inAllMenu) {
292
+ return !findRouteByPath(allowedMenuRoutes, location.pathname);
293
+ }
294
+
295
+ // Tier 2: 隐藏页面级权限
296
+ if (!hasWindowPages()) return false;
297
+ const page = findPageByPath(getWindowPages(), location.pathname);
298
+ if (!page) return false;
299
+ if (page.adminOnly) return true;
300
+ if (page.accessControlEnabled) {
301
+ const sideMenus = (currentUser?.side_menus || []) as string[];
302
+ return !page.routeKey || !sideMenus.includes(page.routeKey);
303
+ }
304
+ return false;
305
+ }, [...]);
266
306
  ```
267
307
 
268
308
  ### MicroAppLoader 中的认证判断
@@ -271,6 +311,8 @@ const isForbidden = useMemo(() => {
271
311
  // components/MicroAppLoader/index.tsx
272
312
  const isAuthReady =
273
313
  isAuthDisabled() ||
314
+ isPageAuthFree(location.pathname) ||
315
+ isNoAuthRoute(location.pathname) ||
274
316
  isNoPermissionRoute(location.pathname) ||
275
317
  !!initialState?.currentUser;
276
318
 
@@ -280,16 +322,31 @@ if (!isAuthReady) {
280
322
  }
281
323
  ```
282
324
 
325
+ 判断优先级:
326
+ 1. `isAuthDisabled()` — 全局关闭权限,直接放行
327
+ 2. `isPageAuthFree` — **PAGES 数据驱动**,页面 `accessControlEnabled === false` 时跳过认证和权限校验
328
+ 3. `isNoAuthRoute()` — 静态配置兜底,免认证路由(PAGES 未注入时的降级保护)
329
+ 4. `isNoPermissionRoute()` — 免权限路由(如 403/404),无需等待 currentUser
330
+ 5. `!!initialState?.currentUser` — 已登录,有用户信息
331
+
332
+ **注意**:`accessControlEnabled === false` 同时影响三个阶段:
333
+
334
+ - SSO 认证(`app.tsx`):跳过 `ensureSsoSession()` 和 `handleAuthFailureRedirect()`
335
+ - 权限校验(`layouts/index.tsx`):Tier 2 隐藏页面默认放行
336
+ - 子应用加载(`MicroAppLoader`):不等待 currentUser 直接加载
337
+
283
338
  ## 设计决策
284
339
 
285
340
  | 决策点 | 选择 | 理由 |
286
341
  | --- | --- | --- |
342
+ | accessControlEnabled 统一控制 | `false` 同时跳过认证和授权 | PAGES 数据驱动,一个字段即可标记公开页面,无需重复配置 noAuthRouteList |
287
343
  | 认证与授权分离 | 两个独立配置项 | 不同场景需要不同组合,如"需要登录但不需要权限"的个人设置页 |
288
344
  | 权限模型 | 白名单 (`side_menus`) | 后端返回的 `side_menus` 是允许列表,比黑名单更安全 |
289
345
  | 403 处理 | 原地渲染组件 | 保持 URL 不变,用户体验更好,便于分享链接 |
290
346
  | 父级菜单显示 | 前缀匹配 | 子菜单有权限时,父级菜单需要作为容器显示 |
291
347
  | 超级用户 | 跳过所有检查 | 管理员需要完整访问权限 |
292
- | 免权限路由的菜单 | 显示全部 | 用户访问公开页面时应能看到所有导航选项 |
348
+ | 免权限路由的菜单 | 按菜单项独立判断 | 每个菜单项根据自身路由是否匹配 noPermissionRouteList 独立决定显示,避免菜单随当前页面变化 |
349
+ | 免认证路由的子应用 | 直接加载 | 不等待 currentUser,免认证路由本身不需要登录 |
293
350
  | 免权限路由的子应用 | 直接加载 | 不等待 currentUser,避免加载卡住 |
294
351
  | 静态路由不受权限控制 | 默认允许访问 | 见下方"静态路由与动态路由"说明 |
295
352
  | adminOnly 判断 | 读取菜单数据字段 | 替代硬编码菜单名称匹配,由后端数据驱动,支持多语言且无需前端维护 |
@@ -301,7 +358,7 @@ if (!isAuthReady) {
301
358
  | 类型 | 来源 | 示例 |
302
359
  | --- | --- | --- |
303
360
  | **静态路由** | 代码中定义 (`config/routes.ts`) | `/`, `/403`, `/404`, `/user/login` |
304
- | **动态路由** | 后端菜单配置 (`window.__MICO_MENUS__`) | `/queue-management`, `/quality-check` |
361
+ | **动态路由** | 后端页面配置 (`window.__MICO_PAGES__`,降级 `window.__MICO_MENUS__`) | `/queue-management`, `/quality-check` |
305
362
 
306
363
  ### 权限控制范围
307
364
 
@@ -311,9 +368,11 @@ if (!isAuthReady) {
311
368
  权限判断逻辑(isForbidden):
312
369
  1. 检查 isAuthDisabled() → 关闭则全部允许
313
370
  2. 检查 isNoPermissionRoute() → 在免权限列表中则允许
314
- 3. 检查 currentRoute(有权限路由) 找到则允许
315
- 4. 检查 routeInAll(所有动态路由)存在则返回 403
316
- 5. 都不匹配交给 Umi 处理(静态路由走这里)
371
+ 3. 检查 currentRoute(PAGES 中的路由)→ 不存在则非动态路由,交给 Umi
372
+ 4. 检查 isSuperuserUser()超级用户放行
373
+ 5. Tier 1: 在菜单中 检查 allowedMenuRoutes
374
+ 6. Tier 2: 不在菜单(隐藏页面)→ 检查 adminOnly + accessControlEnabled + routeKey
375
+ 7. 都不匹配 → 放行
317
376
  ```
318
377
 
319
378
  ### 设计理由
@@ -351,10 +410,11 @@ if (!isAuthReady) {
351
410
  - `side_menus` 格式为菜单路径,如 `"列队管理.配置队列"`
352
411
  - `miss_permissions` 用于按钮级别权限控制,不影响菜单显示
353
412
  - 403 页面在 Layout 内渲染,不会触发路由跳转
354
- - 调试时可在控制台搜索 `isForbidden check` 查看权限判断日志
413
+ - 调试时可在控制台搜索 `isForbidden (menu check)` 或 `isForbidden (hidden page` 查看权限判断日志
355
414
  - **常见问题**:配置了 `noAuthRouteList` 但页面仍显示 403,需同时配置 `noPermissionRouteList`
356
415
 
357
416
  ## 相关文档
358
417
 
359
- - [微前端模式](./feature-微前端模式.md) - 路由和菜单解析
418
+ - [路由与菜单解耦](./feature-路由与菜单解耦.md) - 路由注册与菜单导航数据源分离、双层权限详细说明
419
+ - [微前端模式](./feature-微前端模式.md) - 微应用加载机制
360
420
  - [日志与常量](./arch-日志与常量.md) - 常量管理
@@ -0,0 +1,179 @@
1
+ # 路由与菜单解耦
2
+
3
+ > 创建时间:2026-02-25
4
+
5
+ ## 功能概述
6
+
7
+ 将动态路由注册与菜单导航的数据源解耦。路由注册消费 `window.__MICO_PAGES__`(页面列表),菜单栏消费 `window.__MICO_MENUS__`(菜单树)。两者独立运作,权限通过菜单交叉引用 + 页面级兜底的双层策略控制。
8
+
9
+ ## 技术方案
10
+
11
+ ### 技术栈
12
+
13
+ - 框架:React 18 + @umijs/max
14
+ - 微前端:qiankun
15
+ - 状态管理:Umi initialState
16
+
17
+ ### 数据源关系
18
+
19
+ ```
20
+ __MICO_PAGES__ (扁平列表,所有页面)
21
+ ├── 菜单中的页面(一定能在 __MICO_MENUS__ 中找到对应菜单项)
22
+ └── 隐藏页面(不在菜单中显示,但路由可访问)
23
+
24
+ __MICO_MENUS__ (菜单树,用于导航)
25
+ └── page 字段 → 引用 __MICO_PAGES__ 中的某个页面
26
+ ```
27
+
28
+ ### 数据流
29
+
30
+ ```
31
+ Before:
32
+ patchClientRoutes ← extractRoutes(MENUS)
33
+ layouts 权限校验 ← extractRoutes(MENUS) + filterMenuItems(MENUS)
34
+ 菜单渲染 ← parseMenuItems(MENUS)
35
+
36
+ After:
37
+ patchClientRoutes ← getDynamicRoutes() → PAGES 优先,降级 MENUS
38
+ layouts 权限校验 ← getDynamicRoutes() + extractRoutes(MENUS) + filterMenuItems(MENUS)
39
+ 菜单渲染 ← parseMenuItems(MENUS) ← 不变
40
+ ```
41
+
42
+ ### 权限控制流程
43
+
44
+ ```
45
+ 用户访问路由
46
+
47
+
48
+ disableAuth / noPermissionRoute / superuser?
49
+ ├── 是 → 放行
50
+
51
+
52
+ page.accessControlEnabled === false?
53
+ ├── 是 → 跳过 SSO 认证 + 跳过权限校验(公开页面)
54
+
55
+
56
+ 非动态路由(不在 PAGES 中)?
57
+ ├── 是 → 交给 Umi 处理(404 等)
58
+
59
+
60
+ ┌─────────────────────────────────────────────┐
61
+ │ Tier 1: 菜单权限交叉引用 │
62
+ │ 页面在 __MICO_MENUS__ 中? │
63
+ │ ├── 是 → 沿用 sideMenus 白名单逻辑 │
64
+ │ │ 在 allowedMenuRoutes 中? │
65
+ │ │ ├── 是 → 放行 │
66
+ │ │ └── 否 → 403 │
67
+ │ └── 否 → 进入 Tier 2 │
68
+ ├─────────────────────────────────────────────┤
69
+ │ Tier 2: 隐藏页面级权限 │
70
+ │ (仅 PAGES 数据可用时生效) │
71
+ │ adminOnly: true → 403 │
72
+ │ accessControlEnabled: true │
73
+ │ → routeKey ∈ sideMenus ? 放行 : 403 │
74
+ │ 其他 → 放行 │
75
+ └─────────────────────────────────────────────┘
76
+ ```
77
+
78
+ ### 降级策略
79
+
80
+ 当 `window.__MICO_PAGES__` 未注入时(如旧版部署),`getDynamicRoutes()` 自动降级到从 `window.__MICO_MENUS__` 提取路由,行为与改动前完全一致。
81
+
82
+ ```
83
+ getDynamicRoutes():
84
+ hasWindowPages() ?
85
+ ├── 是 → extractRoutesFromPages(PAGES) ← 新逻辑
86
+ └── 否 → extractRoutes(MENUS) ← 旧逻辑,向后兼容
87
+ ```
88
+
89
+ ## 文件清单
90
+
91
+ ### 修改文件
92
+
93
+ | 文件路径 | 修改内容 |
94
+ | --- | --- |
95
+ | `src/common/menu/types.ts` | `PageConfig` 新增 `accessControlEnabled`、`routeKey` 字段;新增 `PublicPageItem` 类型(`Omit<PageConfig, ...>`);Window 声明新增 `__MICO_PAGES__` |
96
+ | `src/common/menu/parser.ts` | 新增 `getWindowPages`、`hasWindowPages`、`extractRoutesFromPages`、`getDynamicRoutes`、`findPageByPath` 函数;导出 `isSuperuserUser` |
97
+ | `src/app.tsx` | `patchClientRoutes` 改为调用 `getDynamicRoutes()` |
98
+ | `src/layouts/index.tsx` | 路由匹配改用 `allPageRoutes`;权限判断改为双层逻辑(菜单交叉引用 + 隐藏页面级兜底) |
99
+
100
+ ### 未修改文件
101
+
102
+ | 文件路径 | 说明 |
103
+ | --- | --- |
104
+ | `src/layouts/components/menu/index.tsx` | 菜单渲染逻辑不变,继续消费 `__MICO_MENUS__` |
105
+
106
+ ## API / 组件接口
107
+
108
+ ### PublicPageItem
109
+
110
+ ```typescript
111
+ type PublicPageItem = Omit<
112
+ PageConfig,
113
+ 'workspaceSubdomain' | 'publishedBy' | 'versions' | 'createdAt' | 'updatedAt'
114
+ >;
115
+ ```
116
+
117
+ `PageConfig` 新增字段:
118
+
119
+ | 字段 | 类型 | 说明 |
120
+ | --- | --- | --- |
121
+ | `accessControlEnabled` | `boolean` | 是否开启访问控制。`false` 时跳过 SSO 认证和权限校验(公开页面);`true` 时需要登录且通过 `routeKey` 校验权限 |
122
+ | `routeKey` | `string \| null` | 路由权限标识(用于匹配 sideMenus) |
123
+
124
+ ### 页面数据函数
125
+
126
+ ```typescript
127
+ /** 获取 window.__MICO_PAGES__ */
128
+ function getWindowPages(): PublicPageItem[];
129
+
130
+ /** 判断页面数据是否可用 */
131
+ function hasWindowPages(): boolean;
132
+
133
+ /** 从页面数据提取路由配置 */
134
+ function extractRoutesFromPages(pages: PublicPageItem[]): ParsedRoute[];
135
+
136
+ /** 获取动态路由(PAGES 优先,降级 MENUS) */
137
+ function getDynamicRoutes(): ParsedRoute[];
138
+
139
+ /** 根据路径查找页面配置(精确匹配 + 通配符) */
140
+ function findPageByPath(pages: PublicPageItem[], pathname: string): PublicPageItem | undefined;
141
+ ```
142
+
143
+ ### Window 全局变量
144
+
145
+ ```typescript
146
+ interface Window {
147
+ /** 页面列表 — 动态路由注册的数据源 */
148
+ __MICO_PAGES__?: PublicPageItem[];
149
+ /** 菜单树 — 菜单导航的数据源 */
150
+ __MICO_MENUS__?: MenuItem[];
151
+ }
152
+ ```
153
+
154
+ ## 设计决策
155
+
156
+ | 决策点 | 选择 | 理由 |
157
+ | --- | --- | --- |
158
+ | 路由数据源 | `__MICO_PAGES__` 独立于菜单 | 页面和菜单是不同维度:页面定义"有什么路由",菜单定义"导航怎么组织";解耦后支持隐藏页面 |
159
+ | 权限方案 | 菜单交叉引用 + 页面级兜底 | 菜单中的页面复用现有 sideMenus 白名单逻辑,改动最小;隐藏页面用 adminOnly + accessControlEnabled 兜底 |
160
+ | 降级策略 | `getDynamicRoutes()` 自动降级 | `__MICO_PAGES__` 未注入时回退到旧行为,零配置向后兼容 |
161
+ | PublicPageItem 定义 | `Omit<PageConfig, ...>` 派生 | 保持与 PageConfig 的类型继承关系,避免字段重复定义和类型漂移 |
162
+ | accessControlEnabled 语义 | `false` = 公开页面(跳过认证+授权),`true` = 需要权限检查 | 一个字段统一控制 SSO 认证、权限校验、子应用加载等待,减少配置冗余 |
163
+ | 隐藏页面权限 | 先 adminOnly 再 accessControlEnabled | adminOnly 是硬拦截,accessControlEnabled + routeKey 提供用户级粒度控制 |
164
+
165
+ ## 已知限制与待改进
166
+
167
+ - `routeKey` 当前复用 `sideMenus` 做权限匹配,后续可能独立为专门的权限字段
168
+ - 隐藏页面的权限控制依赖 `routeKey` 存在于 `sideMenus` 中,需要后端在下发用户权限时包含隐藏页面的 `routeKey`
169
+
170
+ ## 注意事项
171
+
172
+ - `__MICO_PAGES__` 只包含用户在后台配置的微应用页面,404/403 等静态页面不在其中
173
+ - `__MICO_MENUS__` 中菜单项的 `page` 字段引用的是 `__MICO_PAGES__` 中的某个页面
174
+ - 调试时可在控制台搜索 `isForbidden (menu check)` 或 `isForbidden (hidden page` 查看权限判断日志
175
+
176
+ ## 相关文档
177
+
178
+ - [微前端模式](./feature-微前端模式.md) - 微应用加载机制
179
+ - [菜单权限控制](./feature-菜单权限控制.md) - sideMenus 白名单权限逻辑
@@ -6,73 +6,93 @@
6
6
 
7
7
  export default {
8
8
  // 获取用户信息
9
- // 'GET /api/user/info': {
9
+ 'GET /api/user/info': {
10
+ "code": 200,
11
+ "data": {
12
+ "id": 381,
13
+ "last_login": "2026-03-06T02:37:10.050735Z",
14
+ "is_superuser": true,
15
+ "username": "本地测试mock用户",
16
+ "first_name": "",
17
+ "last_name": "",
18
+ "email": "本地测试mock用户@micous.com",
19
+ "is_staff": true,
20
+ "is_active": true,
21
+ "type": 1,
22
+ "phone": "",
23
+ "agency_id": 0,
24
+ "avatar": "https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png",
25
+ "user_name": "Easton",
26
+ "permission_tree": [
27
+ ],
28
+ "miss_permissions": [],
29
+ "side_menus": [
30
+ '首页',
31
+ '示例模块.示例页面',
32
+ '微应用示例.子应用页面',
33
+ '外部链接',
34
+ '权限管理',
35
+ ],
36
+ "region_permissions": []
37
+ },
38
+ "msg": "ok"
39
+ },
40
+
41
+ // 获取统计数据
42
+ // 'GET /api/dashboard/stats': {
10
43
  // success: true,
11
44
  // data: {
12
- // id: 1001,
13
- // name: '张三',
14
- // email: 'zhangsan@example.com',
15
- // avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=homepage',
16
- // role: 'admin',
17
- // department: '技术部',
45
+ // totalUsers: 12580,
46
+ // activeUsers: 3420,
47
+ // pendingTasks: 156,
48
+ // completedTasks: 8934,
49
+ // lastUpdated: new Date().toISOString(),
18
50
  // },
19
51
  // },
20
52
 
21
- // 获取统计数据
22
- 'GET /api/dashboard/stats': {
23
- success: true,
24
- data: {
25
- totalUsers: 12580,
26
- activeUsers: 3420,
27
- pendingTasks: 156,
28
- completedTasks: 8934,
29
- lastUpdated: new Date().toISOString(),
30
- },
31
- },
32
-
33
53
  // 获取列表数据
34
- 'GET /api/items': {
35
- success: true,
36
- data: {
37
- list: [
38
- {
39
- id: 1,
40
- title: '待审核内容 A',
41
- status: 'pending',
42
- createdAt: '2025-12-27 10:00:00',
43
- },
44
- {
45
- id: 2,
46
- title: '待审核内容 B',
47
- status: 'pending',
48
- createdAt: '2025-12-27 09:30:00',
49
- },
50
- {
51
- id: 3,
52
- title: '已通过内容 C',
53
- status: 'approved',
54
- createdAt: '2025-12-26 15:00:00',
55
- },
56
- {
57
- id: 4,
58
- title: '已拒绝内容 D',
59
- status: 'rejected',
60
- createdAt: '2025-12-26 14:00:00',
61
- },
62
- ],
63
- total: 4,
64
- page: 1,
65
- pageSize: 10,
66
- },
67
- },
54
+ // 'GET /api/items': {
55
+ // success: true,
56
+ // data: {
57
+ // list: [
58
+ // {
59
+ // id: 1,
60
+ // title: '待审核内容 A',
61
+ // status: 'pending',
62
+ // createdAt: '2025-12-27 10:00:00',
63
+ // },
64
+ // {
65
+ // id: 2,
66
+ // title: '待审核内容 B',
67
+ // status: 'pending',
68
+ // createdAt: '2025-12-27 09:30:00',
69
+ // },
70
+ // {
71
+ // id: 3,
72
+ // title: '已通过内容 C',
73
+ // status: 'approved',
74
+ // createdAt: '2025-12-26 15:00:00',
75
+ // },
76
+ // {
77
+ // id: 4,
78
+ // title: '已拒绝内容 D',
79
+ // status: 'rejected',
80
+ // createdAt: '2025-12-26 14:00:00',
81
+ // },
82
+ // ],
83
+ // total: 4,
84
+ // page: 1,
85
+ // pageSize: 10,
86
+ // },
87
+ // },
68
88
 
69
89
  // POST 请求示例
70
- 'POST /api/items/approve': (req: any, res: any) => {
71
- const { id } = req.body || {};
72
- res.json({
73
- success: true,
74
- message: `Item ${id} approved successfully`,
75
- data: { id, status: 'approved' },
76
- });
77
- },
90
+ // 'POST /api/items/approve': (req: any, res: any) => {
91
+ // const { id } = req.body || {};
92
+ // res.json({
93
+ // success: true,
94
+ // message: `Item ${id} approved successfully`,
95
+ // data: { id, status: 'approved' },
96
+ // });
97
+ // },
78
98
  };