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.
- package/README.md +33 -0
- package/bin/mico.js +15 -2
- package/generators/micro-react/index.js +44 -6
- package/generators/micro-react/meta.json +2 -1
- package/generators/micro-react/templates/.cursor/rules/always-read-docs.mdc +14 -4
- package/generators/micro-react/templates/.cursor/rules/layout-app.mdc +36 -26
- package/generators/micro-react/templates/.cursor/rules/project-overview.mdc +5 -2
- package/generators/micro-react/templates/CLAUDE.md +4 -2
- package/generators/micro-react/templates/_gitignore +3 -1
- package/generators/micro-react/templates/_npmrc +1 -0
- package/generators/micro-react/templates/apps/layout/config/config.dev.ts +7 -3
- package/generators/micro-react/templates/apps/layout/config/config.prod.development.ts +5 -0
- package/generators/micro-react/templates/apps/layout/config/config.prod.testing.ts +5 -0
- package/generators/micro-react/templates/apps/layout/config/config.prod.ts +12 -0
- package/generators/micro-react/templates/apps/layout/config/routes.ts +0 -5
- 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
- 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
- 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
- package/generators/micro-react/templates/apps/layout/mock/api.mock.ts +81 -61
- package/generators/micro-react/templates/apps/layout/mock/menus.ts +89 -144
- package/generators/micro-react/templates/apps/layout/mock/pages.ts +83 -0
- package/generators/micro-react/templates/apps/layout/package.json +1 -0
- package/generators/micro-react/templates/apps/layout/src/app.tsx +13 -8
- package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +118 -43
- package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +31 -4
- package/generators/micro-react/templates/apps/layout/src/common/micro-prefetch.ts +3 -2
- package/generators/micro-react/templates/apps/layout/src/common/portal-data.ts +45 -0
- package/generators/micro-react/templates/apps/layout/src/common/request/config.ts +49 -10
- package/generators/micro-react/templates/apps/layout/src/common/request/interceptors.ts +1 -1
- package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +6 -0
- package/generators/micro-react/templates/apps/layout/src/common/theme.ts +0 -2
- package/generators/micro-react/templates/apps/layout/src/components/AppTabs/index.less +0 -1
- package/generators/micro-react/templates/apps/layout/src/components/AppTabs/index.tsx +4 -4
- package/generators/micro-react/templates/apps/layout/src/components/IconFont/index.tsx +4 -5
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.less +21 -6
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +4 -3
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/micro-app-manager.ts +7 -1
- package/generators/micro-react/templates/apps/layout/src/components/RightContent/AvatarDropdown.tsx +12 -16
- package/generators/micro-react/templates/apps/layout/src/global.less +15 -2
- package/generators/micro-react/templates/apps/layout/src/hooks/useMenu.ts +3 -2
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.less +32 -4
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +20 -10
- package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +75 -38
- package/generators/micro-react/templates/apps/layout/src/pages/404/index.tsx +3 -7
- package/generators/micro-react/templates/apps/layout/src/services/user.ts +7 -3
- package/generators/micro-react/templates/dev.preset.json +1 -1
- package/generators/micro-react/templates/package.json +1 -0
- package/generators/micro-react/templates/scripts/apply-sentry-plugin.ts +45 -0
- package/generators/subapp-react/index.js +206 -6
- package/generators/subapp-react/templates/homepage/.env +2 -1
- package/generators/subapp-react/templates/homepage/config/config.prod.development.ts +1 -0
- package/generators/subapp-react/templates/homepage/config/config.prod.testing.ts +1 -0
- package/generators/subapp-react/templates/homepage/config/config.prod.ts +8 -0
- package/generators/subapp-react/templates/homepage/package.json +1 -0
- package/generators/subapp-react/templates/homepage/src/app.tsx +6 -0
- package/generators/subapp-umd/ignore-list.json +5 -0
- package/generators/subapp-umd/index.js +325 -0
- package/generators/subapp-umd/meta.json +11 -0
- package/generators/subapp-umd/templates/README.md +94 -0
- package/generators/subapp-umd/templates/package.json +35 -0
- package/generators/subapp-umd/templates/public/index.html +34 -0
- package/generators/subapp-umd/templates/src/App.less +15 -0
- package/generators/subapp-umd/templates/src/App.tsx +13 -0
- package/generators/subapp-umd/templates/src/index.ts +2 -0
- package/generators/subapp-umd/templates/tsconfig.json +27 -0
- package/generators/subapp-umd/templates/webpack.config.js +68 -0
- package/lib/utils.js +2 -1
- package/package.json +1 -1
|
@@ -19,12 +19,12 @@
|
|
|
19
19
|
|
|
20
20
|
### 四种路由场景
|
|
21
21
|
|
|
22
|
-
| 场景 | noAuthRouteList | noPermissionRouteList | 示例 |
|
|
23
|
-
| --- | --- | --- | --- |
|
|
24
|
-
| 公开页面 | ✅ | ✅ | 首页、活动页 |
|
|
25
|
-
| 登录后公共页面 | ❌ | ✅ | 个人设置、帮助中心 |
|
|
26
|
-
| 需要权限的页面 | ❌ | ❌ | 后台管理、业务功能 |
|
|
27
|
-
| 登录页等特殊页面 | ✅ | ✅ | /
|
|
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
|
-
│
|
|
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
|
-
│ └── 否 →
|
|
56
|
-
│
|
|
57
|
-
│
|
|
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
|
-
│
|
|
64
|
-
│
|
|
65
|
-
│
|
|
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
|
-
│
|
|
72
|
-
│ ├── 是 →
|
|
73
|
-
│
|
|
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
|
-
|
|
96
|
-
├── 是 →
|
|
114
|
+
Tier 1: 页面在菜单中?
|
|
115
|
+
├── 是 → 沿用菜单权限逻辑:
|
|
116
|
+
│ 菜单项 adminOnly === true?
|
|
117
|
+
│ ├── 是 → 禁止访问
|
|
118
|
+
│ 检查 side_menus 白名单
|
|
119
|
+
│ ├── 匹配 → 允许访问
|
|
120
|
+
│ └── 不匹配 → 显示 403
|
|
97
121
|
│
|
|
98
122
|
▼
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
├──
|
|
102
|
-
|
|
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 (
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
| **动态路由** |
|
|
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. 检查
|
|
316
|
-
5.
|
|
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
|
-
- [
|
|
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
|
-
|
|
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
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
};
|