generator-mico-cli 0.2.28 → 0.2.29
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 +5 -20
- package/bin/mico.js +27 -62
- package/generators/micro-react/index.js +8 -0
- package/generators/micro-react/templates/.cursor/rules/always-read-docs.mdc +3 -0
- package/generators/micro-react/templates/.cursor/rules/project-overview.mdc +1 -0
- package/generators/micro-react/templates/CLAUDE.md +1 -0
- package/generators/micro-react/templates/README.md +1 -1
- package/generators/micro-react/templates/apps/layout/config/config.dev.ts +4 -2
- package/generators/micro-react/templates/apps/layout/config/config.prod.development.ts +2 -0
- package/generators/micro-react/templates/apps/layout/config/config.prod.testing.ts +2 -0
- package/generators/micro-react/templates/apps/layout/config/config.prod.ts +2 -0
- package/generators/micro-react/templates/apps/layout/docs/feature-PermissionFilter/346/214/211/351/222/256/346/235/203/351/231/220.md +116 -0
- 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 +83 -77
- 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 +50 -35
- package/generators/micro-react/templates/apps/layout/docs/feature-/350/267/257/347/224/261/346/235/203/351/231/220/346/227/245/345/277/227.md +162 -0
- package/generators/micro-react/templates/apps/layout/mock/api.mock.ts +23 -31
- package/generators/micro-react/templates/apps/layout/mock/pages.ts +5 -6
- package/generators/micro-react/templates/apps/layout/package.json +2 -1
- package/generators/micro-react/templates/apps/layout/src/app.tsx +30 -2
- package/generators/micro-react/templates/apps/layout/src/common/auth/type.ts +15 -27
- package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +148 -85
- package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +2 -6
- package/generators/micro-react/templates/apps/layout/src/common/portal-data.ts +46 -2
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +5 -1
- package/generators/micro-react/templates/apps/layout/src/components/PermissionFilter/index.tsx +51 -0
- package/generators/micro-react/templates/apps/layout/src/components/RightContent/AvatarDropdown.tsx +10 -1
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +3 -3
- package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +105 -60
- package/generators/micro-react/templates/apps/layout/src/locales/en-US.ts +17 -0
- package/generators/micro-react/templates/apps/layout/src/locales/zh-CN.ts +16 -0
- package/generators/micro-react/templates/apps/layout/src/pages/404/index.tsx +7 -3
- package/generators/micro-react/templates/apps/layout/src/pages/Home/index.less +5 -0
- package/generators/micro-react/templates/apps/layout/src/pages/Home/index.tsx +49 -1
- package/generators/micro-react/templates/apps/layout/src/services/user.ts +28 -21
- package/generators/micro-react/templates/packages/common-intl/README.md +77 -369
- package/generators/micro-react/templates/packages/common-intl/package.json +3 -13
- package/generators/micro-react/templates/packages/common-intl/src/index.ts +3 -6
- package/generators/micro-react/templates/packages/common-intl/src/intl.ts +20 -29
- package/generators/micro-react/templates/packages/common-intl/tsconfig.json +2 -4
- package/generators/subapp-react/index.js +28 -22
- package/generators/subapp-react/templates/homepage/README.md +1 -0
- 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 +1 -0
- package/generators/subapp-react/templates/homepage/docs/feature-PermissionFilter/346/214/211/351/222/256/346/235/203/351/231/220.md +35 -0
- package/generators/subapp-react/templates/homepage/package.json +2 -1
- package/generators/subapp-react/templates/homepage/src/app.tsx +7 -0
- package/generators/subapp-react/templates/homepage/src/common/mainApp.ts +39 -2
- package/generators/subapp-react/templates/homepage/src/components/PermissionFilter/index.tsx +48 -0
- package/generators/subapp-react/templates/homepage/src/pages/index.tsx +35 -1
- package/lib/utils.js +0 -1
- package/package.json +2 -2
- package/generators/micro-react/templates/apps/layout/docs/common-intl.md +0 -372
- package/generators/micro-react/templates/packages/common-intl/src/indexedDBUtils.ts +0 -51
- package/generators/micro-react/templates/packages/common-intl/src/utils.ts +0 -482
- package/generators/micro-react/templates/packages/common-intl/vite.config.ts +0 -25
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# 菜单权限控制
|
|
2
2
|
|
|
3
|
-
> 创建时间:2026-01-24 更新时间:2026-
|
|
3
|
+
> 创建时间:2026-01-24 更新时间:2026-03-27(同步 `getMenuPage`:pageId 优先,path 兜底)
|
|
4
4
|
|
|
5
5
|
## 功能概述
|
|
6
6
|
|
|
7
|
-
基于用户信息中的 `
|
|
7
|
+
基于用户信息中的 **`menu_perms`**(菜单权限 code 数组,与 OpenAPI 8 `GET /user/info/` 一致)实现菜单和路由权限。非超级用户仅当 **`menu_perms` 包含对应 code** 时可见菜单项、可访问受控动态路由(`accessControlEnabled && routeKey` 时要求 `routeKey ∈ menu_perms`)。访问无权限路由时显示 403 页面。
|
|
8
8
|
|
|
9
9
|
**v2 更新**:支持认证(Authentication)与授权(Authorization)分离配置,可独立控制"跳过 SSO 登录"和"跳过菜单权限校验"。
|
|
10
10
|
|
|
@@ -54,22 +54,21 @@
|
|
|
54
54
|
│ 2. 权限校验 (layouts/index.tsx) │
|
|
55
55
|
│ isNoPermissionRoute(pathname)? │
|
|
56
56
|
│ ├── 是 → 跳过权限校验 │
|
|
57
|
-
│ └── 否 →
|
|
58
|
-
│
|
|
59
|
-
│
|
|
60
|
-
│ │
|
|
61
|
-
│ └── 否 → Tier 2: 页面级权限 │
|
|
62
|
-
│ adminOnly / routeKey│
|
|
57
|
+
│ └── 否 → PAGES 页面:adminOnly / │
|
|
58
|
+
│ accessControlEnabled+routeKey│
|
|
59
|
+
│ 与 menu_perms;无 page 时 │
|
|
60
|
+
│ 按菜单过滤结果兜底 │
|
|
63
61
|
│ 详见:路由与菜单解耦文档 │
|
|
64
62
|
└──────────────────────────────────────────┘
|
|
65
63
|
│
|
|
66
64
|
▼
|
|
67
65
|
┌──────────────────────────────────────────┐
|
|
68
66
|
│ 3. 菜单渲染 (menu/index.tsx) │
|
|
69
|
-
│ 按
|
|
67
|
+
│ 按 menu_perms 过滤(菜单项 code │
|
|
68
|
+
│ 与关联页 routeKey / nameKey 一致) │
|
|
70
69
|
│ 每个菜单项独立检查 isNoPermissionRoute│
|
|
71
70
|
│ ├── 匹配 → 该项始终显示 │
|
|
72
|
-
│ └── 不匹配 →
|
|
71
|
+
│ └── 不匹配 → code ∈ menu_perms │
|
|
73
72
|
└──────────────────────────────────────────┘
|
|
74
73
|
│
|
|
75
74
|
▼
|
|
@@ -111,23 +110,20 @@ page.accessControlEnabled === false?
|
|
|
111
110
|
├── 是 → 允许访问所有页面
|
|
112
111
|
│
|
|
113
112
|
▼
|
|
114
|
-
|
|
115
|
-
├── 是 →
|
|
116
|
-
│
|
|
117
|
-
│
|
|
118
|
-
│
|
|
119
|
-
│ ├──
|
|
120
|
-
│ └──
|
|
113
|
+
能关联到 PAGES 页面配置?
|
|
114
|
+
├── 是 → adminOnly === true?
|
|
115
|
+
│ ├── 是 → 403
|
|
116
|
+
│ accessControlEnabled === true?
|
|
117
|
+
│ ├── 是 → routeKey ∈ menu_perms?
|
|
118
|
+
│ │ ├── 是 → 允许
|
|
119
|
+
│ │ └── 否 → 403
|
|
120
|
+
│ └── 否 → 允许
|
|
121
121
|
│
|
|
122
122
|
▼
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
├── 是 → 检查 routeKey ∈ side_menus
|
|
128
|
-
│ ├── 匹配 → 允许访问
|
|
129
|
-
│ └── 不匹配 → 显示 403
|
|
130
|
-
└── 否 → 允许访问
|
|
123
|
+
路径仅在菜单路由中、无 page 元数据
|
|
124
|
+
└── 在 filterMenuItems 后的路由集中?
|
|
125
|
+
├── 是 → 允许
|
|
126
|
+
└── 否 → 403
|
|
131
127
|
```
|
|
132
128
|
|
|
133
129
|
## 文件清单
|
|
@@ -144,7 +140,8 @@ Tier 2: 隐藏页面(不在菜单中)
|
|
|
144
140
|
| --- | --- |
|
|
145
141
|
| `src/common/menu/types.ts` | 新增 `noPermissionRouteList` 类型定义 |
|
|
146
142
|
| `src/constants/index.ts` | 新增 `isNoPermissionRoute`、`getNoPermissionRouteList` 函数 |
|
|
147
|
-
| `src/common/
|
|
143
|
+
| `src/common/portal-data.ts` | `getMenuPage`:pageId 优先,无匹配再用 path 与页面 route |
|
|
144
|
+
| `src/common/menu/parser.ts` | `filterMenuItems`、`getMenuItemPermCode` 等,消费 `getMenuPage` |
|
|
148
145
|
| `src/layouts/index.tsx` | 新增 `isForbidden` 判断,集成 `isNoPermissionRoute` |
|
|
149
146
|
| `src/layouts/components/menu/index.tsx` | 集成菜单过滤,支持免权限校验路由 |
|
|
150
147
|
| `src/components/MicroAppLoader/index.tsx` | 集成 `isNoPermissionRoute`,免权限路由直接加载 |
|
|
@@ -194,33 +191,43 @@ function isAuthDisabled(): boolean;
|
|
|
194
191
|
interface MenuFilterOptions {
|
|
195
192
|
/** 是否是超级用户 */
|
|
196
193
|
isSuperuser?: boolean | number;
|
|
197
|
-
/**
|
|
198
|
-
|
|
194
|
+
/** 用户拥有的菜单权限 code(与页面 routeKey、菜单项一致) */
|
|
195
|
+
menuPerms?: string[];
|
|
199
196
|
}
|
|
200
197
|
```
|
|
201
198
|
|
|
199
|
+
### 菜单项如何关联到页面(与 `routeKey` / `menu_perms`)
|
|
200
|
+
|
|
201
|
+
`page` 类型菜单项的权限 code 来自关联页的 **`routeKey`**(当该页 `accessControlEnabled && routeKey` 时)。关联页由 **`getMenuPage(item)`**(`src/common/portal-data.ts`)解析:
|
|
202
|
+
|
|
203
|
+
1. **`pageId`**:在 `__MICO_PAGES__` 中按 `id` 查找,**命中则使用该页面**(与菜单 `path` 是否一致无关)。
|
|
204
|
+
2. **`path` + 页面 `route`**:上一步无结果时,用 `findPageByPath` 将菜单 `path` 与页面 `route` 匹配(精确 + `/*` 通配)。
|
|
205
|
+
|
|
206
|
+
详见 [路由与菜单解耦](./feature-路由与菜单解耦.md) 文档内「菜单项关联页面(getMenuPage)」小节。
|
|
207
|
+
|
|
202
208
|
### filterMenuItems
|
|
203
209
|
|
|
204
210
|
```typescript
|
|
205
211
|
/**
|
|
206
|
-
*
|
|
212
|
+
* 根据 menu_perms 过滤菜单项
|
|
213
|
+
* - page:关联页由 getMenuPage → routeKey(需校验时);link:nameKey
|
|
207
214
|
*/
|
|
208
|
-
function filterMenuItems(
|
|
209
|
-
items: MenuItem[],
|
|
210
|
-
options?: MenuFilterOptions,
|
|
211
|
-
parentPath?: string,
|
|
212
|
-
): MenuItem[];
|
|
215
|
+
function filterMenuItems(items: MenuItem[], options?: MenuFilterOptions): MenuItem[];
|
|
213
216
|
```
|
|
214
217
|
|
|
215
218
|
### 用户信息相关字段
|
|
216
219
|
|
|
217
220
|
```typescript
|
|
218
221
|
interface IUserInfo {
|
|
219
|
-
is_superuser: boolean
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
is_superuser: boolean;
|
|
223
|
+
menu_perms: string[];
|
|
224
|
+
button_perms: string[];
|
|
225
|
+
app_perms: string[];
|
|
226
|
+
region_perms: string[];
|
|
227
|
+
name: string;
|
|
228
|
+
user_name: string;
|
|
229
|
+
username: string;
|
|
230
|
+
// …见 src/common/auth/type.ts IUserInfo / IUserInfoApiData
|
|
224
231
|
}
|
|
225
232
|
```
|
|
226
233
|
|
|
@@ -264,43 +271,41 @@ export const NO_AUTH_ROUTE_LIST: string[] = [
|
|
|
264
271
|
export const NO_PERMISSION_ROUTE_LIST: string[] = ['/403', '/404'];
|
|
265
272
|
```
|
|
266
273
|
|
|
267
|
-
###
|
|
274
|
+
### 菜单过滤结果(示意)
|
|
268
275
|
|
|
269
276
|
```
|
|
270
|
-
|
|
271
|
-
├──
|
|
272
|
-
├──
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
│ └── 抽样检查 ❌ 不在白名单
|
|
276
|
-
└── 权限管理 ❌ adminOnly=true,非超级用户不可见
|
|
277
|
+
menu_perms 含 code "a"、"b.c"
|
|
278
|
+
├── 页面菜单(routeKey=a) ✅
|
|
279
|
+
├── 页面菜单(routeKey 不在列表) ❌
|
|
280
|
+
├── 分组(仅容器,有可见子项) ✅ 作为父级保留
|
|
281
|
+
└── adminOnly 菜单 ❌ 非超管
|
|
277
282
|
```
|
|
278
283
|
|
|
279
284
|
### Layout 中的权限判断
|
|
280
285
|
|
|
281
286
|
```tsx
|
|
282
|
-
// layouts/index.tsx —
|
|
287
|
+
// layouts/index.tsx — 以 PAGES + menu_perms 为主,无 page 时菜单路由兜底
|
|
283
288
|
const isForbidden = useMemo(() => {
|
|
284
289
|
if (isAuthDisabled()) return false;
|
|
285
290
|
if (isNoPermissionRoute(location.pathname)) return false;
|
|
286
|
-
if (!currentRoute) return false;
|
|
291
|
+
if (!currentRoute) return false;
|
|
287
292
|
if (isSuperuserUser(currentUser?.is_superuser)) return false;
|
|
288
293
|
|
|
289
|
-
|
|
294
|
+
const page =
|
|
295
|
+
currentRoute.pageConfig ??
|
|
296
|
+
(hasPages() ? findPageByPath(getPages(), location.pathname) : undefined);
|
|
297
|
+
|
|
298
|
+
if (page) {
|
|
299
|
+
if (page.adminOnly) return true;
|
|
300
|
+
if (!page.accessControlEnabled) return false;
|
|
301
|
+
const menuPerms = currentUser?.menu_perms || [];
|
|
302
|
+
return !page.routeKey || !menuPerms.includes(page.routeKey);
|
|
303
|
+
}
|
|
304
|
+
|
|
290
305
|
const inAllMenu = findRouteByPath(allMenuRoutes, location.pathname);
|
|
291
306
|
if (inAllMenu) {
|
|
292
307
|
return !findRouteByPath(allowedMenuRoutes, location.pathname);
|
|
293
308
|
}
|
|
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
309
|
return false;
|
|
305
310
|
}, [...]);
|
|
306
311
|
```
|
|
@@ -323,6 +328,7 @@ if (!isAuthReady) {
|
|
|
323
328
|
```
|
|
324
329
|
|
|
325
330
|
判断优先级:
|
|
331
|
+
|
|
326
332
|
1. `isAuthDisabled()` — 全局关闭权限,直接放行
|
|
327
333
|
2. `isPageAuthFree` — **PAGES 数据驱动**,页面 `accessControlEnabled === false` 时跳过认证和权限校验
|
|
328
334
|
3. `isNoAuthRoute()` — 静态配置兜底,免认证路由(PAGES 未注入时的降级保护)
|
|
@@ -341,9 +347,9 @@ if (!isAuthReady) {
|
|
|
341
347
|
| --- | --- | --- |
|
|
342
348
|
| accessControlEnabled 统一控制 | `false` 同时跳过认证和授权 | PAGES 数据驱动,一个字段即可标记公开页面,无需重复配置 noAuthRouteList |
|
|
343
349
|
| 认证与授权分离 | 两个独立配置项 | 不同场景需要不同组合,如"需要登录但不需要权限"的个人设置页 |
|
|
344
|
-
| 权限模型 | 白名单 (`
|
|
350
|
+
| 权限模型 | 白名单 (`menu_perms`) | 后端返回的 code 列表为允许集合;与 `routeKey` / 菜单项一一对应 |
|
|
345
351
|
| 403 处理 | 原地渲染组件 | 保持 URL 不变,用户体验更好,便于分享链接 |
|
|
346
|
-
| 父级菜单显示 |
|
|
352
|
+
| 父级菜单显示 | 子项驱动 | `group` 无独立 code,有可见子项时保留为容器 |
|
|
347
353
|
| 超级用户 | 跳过所有检查 | 管理员需要完整访问权限 |
|
|
348
354
|
| 免权限路由的菜单 | 按菜单项独立判断 | 每个菜单项根据自身路由是否匹配 noPermissionRouteList 独立决定显示,避免菜单随当前页面变化 |
|
|
349
355
|
| 免认证路由的子应用 | 直接加载 | 不等待 currentUser,免认证路由本身不需要登录 |
|
|
@@ -370,9 +376,9 @@ if (!isAuthReady) {
|
|
|
370
376
|
2. 检查 isNoPermissionRoute() → 在免权限列表中则允许
|
|
371
377
|
3. 检查 currentRoute(PAGES 中的路由)→ 不存在则非动态路由,交给 Umi
|
|
372
378
|
4. 检查 isSuperuserUser() → 超级用户放行
|
|
373
|
-
5.
|
|
374
|
-
6.
|
|
375
|
-
7.
|
|
379
|
+
5. 有 PAGES 页面元数据 → adminOnly / accessControlEnabled + routeKey ∈ menu_perms
|
|
380
|
+
6. 无页面元数据但在菜单路由中 → 检查 allowedMenuRoutes(已按 menu_perms 过滤)
|
|
381
|
+
7. 其他 → 放行
|
|
376
382
|
```
|
|
377
383
|
|
|
378
384
|
### 设计理由
|
|
@@ -384,15 +390,15 @@ if (!isAuthReady) {
|
|
|
384
390
|
|
|
385
391
|
### 影响
|
|
386
392
|
|
|
387
|
-
当用户 `is_superuser=false` 且 `
|
|
393
|
+
当用户 `is_superuser=false` 且 `menu_perms=[]` 时:
|
|
388
394
|
|
|
389
|
-
| 路由
|
|
390
|
-
|
|
|
391
|
-
| `/` (Home)
|
|
392
|
-
| `/403`
|
|
393
|
-
| `/404`
|
|
394
|
-
| `/user/login`
|
|
395
|
-
| `/queue-management` | ❌ 显示 403 | 动态路由,无权限
|
|
395
|
+
| 路由 | 结果 | 原因 |
|
|
396
|
+
| ------------------- | ----------- | ---------------------------------- |
|
|
397
|
+
| `/` (Home) | ✅ 正常显示 | 静态路由,不受权限控制 |
|
|
398
|
+
| `/403` | ✅ 正常显示 | 在 `noPermissionRouteList` 中 |
|
|
399
|
+
| `/404` | ✅ 正常显示 | 在 `noPermissionRouteList` 中 |
|
|
400
|
+
| `/user/login` | ✅ 正常显示 | 静态路由 + 在 `noAuthRouteList` 中 |
|
|
401
|
+
| `/queue-management` | ❌ 显示 403 | 动态路由,无权限 |
|
|
396
402
|
|
|
397
403
|
### 如需控制静态路由
|
|
398
404
|
|
|
@@ -406,11 +412,11 @@ if (!isAuthReady) {
|
|
|
406
412
|
|
|
407
413
|
- `noAuthRouteList` 和 `noPermissionRouteList` 是独立的,需要根据场景分别配置
|
|
408
414
|
- 两个列表都支持 `/*` 后缀进行前缀匹配
|
|
409
|
-
- `
|
|
410
|
-
- `
|
|
411
|
-
- `
|
|
415
|
+
- `menu_perms` 为空时,非超级用户没有任何菜单权限(`page`/`link` 项)
|
|
416
|
+
- 菜单项权限 code 与页面 `routeKey` 一致;`page` 类型先通过 **`getMenuPage`** 得到关联页再取 `routeKey`,否则回退 `nameKey`(link)
|
|
417
|
+
- `button_perms` 可用于按钮级权限(layout 侧栏不直接消费)
|
|
412
418
|
- 403 页面在 Layout 内渲染,不会触发路由跳转
|
|
413
|
-
-
|
|
419
|
+
- 调试时可在控制台过滤 `routePermission` 查看结构化权限日志(详见 [路由权限日志](./feature-路由权限日志.md))
|
|
414
420
|
- **常见问题**:配置了 `noAuthRouteList` 但页面仍显示 403,需同时配置 `noPermissionRouteList`
|
|
415
421
|
|
|
416
422
|
## 相关文档
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# 路由与菜单解耦
|
|
2
2
|
|
|
3
|
-
> 创建时间:2026-02-25
|
|
3
|
+
> 创建时间:2026-02-25 更新时间:2026-03-27
|
|
4
4
|
|
|
5
5
|
## 功能概述
|
|
6
6
|
|
|
7
|
-
将动态路由注册与菜单导航的数据源解耦。路由注册消费 `window.__MICO_PAGES__`(页面列表),菜单栏消费 `window.__MICO_MENUS__
|
|
7
|
+
将动态路由注册与菜单导航的数据源解耦。路由注册消费 `window.__MICO_PAGES__`(页面列表),菜单栏消费 `window.__MICO_MENUS__`(菜单树)。两者独立运作,**路由与侧栏权限**统一以用户信息中的 **`menu_perms`(菜单 code 列表)** 为准,与页面 **`routeKey`**、菜单项权限 code **一一对应**;无页面元数据时仍可按「菜单中是否可见」兜底。
|
|
8
8
|
|
|
9
9
|
## 技术方案
|
|
10
10
|
|
|
@@ -22,9 +22,20 @@ __MICO_PAGES__ (扁平列表,所有页面)
|
|
|
22
22
|
└── 隐藏页面(不在菜单中显示,但路由可访问)
|
|
23
23
|
|
|
24
24
|
__MICO_MENUS__ (菜单树,用于导航)
|
|
25
|
-
└── page
|
|
25
|
+
└── `page` 类型项通过 pageId / path 关联到 __MICO_PAGES__(见下方「菜单项关联页面」)
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
### 菜单项关联页面(`getMenuPage`)
|
|
29
|
+
|
|
30
|
+
侧栏过滤、从菜单树提取路由(`extractRoutes`)、`routeKey` / `menu_perms` 等需要「菜单项 → 页面配置」时,统一由 `src/common/portal-data.ts` 的 **`getMenuPage(item)`** 解析:
|
|
31
|
+
|
|
32
|
+
| 顺序 | 规则 | 说明 |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| 1 | **`pageId` → `getPageById`** | 菜单项带 `pageId` 且在 `__MICO_PAGES__` 中存在对应 `id` 时,**优先使用该页面** |
|
|
35
|
+
| 2 | **`path` + `findPageByPath`** | 上一步无结果时(无 `pageId`、或 id 在列表中不存在),用菜单 **`path`** 与页面 **`route`** 匹配(精确 + 最长前缀 `/*`),与路由侧解析规则一致 |
|
|
36
|
+
|
|
37
|
+
仅 `type === 'page'` 的菜单项会走上述逻辑;`findPageByPath` 仅匹配 **`enabled === true`** 的页面。
|
|
38
|
+
|
|
28
39
|
### 数据流
|
|
29
40
|
|
|
30
41
|
```
|
|
@@ -35,7 +46,7 @@ Before:
|
|
|
35
46
|
|
|
36
47
|
After:
|
|
37
48
|
patchClientRoutes ← getDynamicRoutes() → PAGES 优先,降级 MENUS
|
|
38
|
-
layouts 权限校验 ←
|
|
49
|
+
layouts 权限校验 ← PAGES 上 routeKey ∈ menu_perms;无 page 时 allowedMenuRoutes 兜底
|
|
39
50
|
菜单渲染 ← parseMenuItems(MENUS) ← 不变
|
|
40
51
|
```
|
|
41
52
|
|
|
@@ -53,26 +64,22 @@ page.accessControlEnabled === false?
|
|
|
53
64
|
├── 是 → 跳过 SSO 认证 + 跳过权限校验(公开页面)
|
|
54
65
|
│
|
|
55
66
|
▼
|
|
56
|
-
|
|
67
|
+
非动态路由(currentRoute 不在 PAGES 动态列表中)?
|
|
57
68
|
├── 是 → 交给 Umi 处理(404 等)
|
|
58
69
|
│
|
|
59
70
|
▼
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
│
|
|
63
|
-
│
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
│
|
|
70
|
-
│
|
|
71
|
-
|
|
72
|
-
│ accessControlEnabled: true │
|
|
73
|
-
│ → routeKey ∈ sideMenus ? 放行 : 403 │
|
|
74
|
-
│ 其他 → 放行 │
|
|
75
|
-
└─────────────────────────────────────────────┘
|
|
71
|
+
能解析到 PAGES 中的页面配置?
|
|
72
|
+
├── 是 → adminOnly → 403
|
|
73
|
+
│ accessControlEnabled && routeKey
|
|
74
|
+
│ → routeKey ∈ menu_perms ? 放行 : 403
|
|
75
|
+
│ 其他 → 放行
|
|
76
|
+
│
|
|
77
|
+
▼
|
|
78
|
+
路径仅在菜单路由中出现(无 page 元数据)?
|
|
79
|
+
├── 是 → 在 filterMenuItems(menu_perms) 后的 allowedMenuRoutes 中?
|
|
80
|
+
│ ├── 是 → 放行
|
|
81
|
+
│ └── 否 → 403
|
|
82
|
+
└── 否 → 放行(非本逻辑覆盖)
|
|
76
83
|
```
|
|
77
84
|
|
|
78
85
|
### 降级策略
|
|
@@ -93,9 +100,10 @@ getDynamicRoutes():
|
|
|
93
100
|
| 文件路径 | 修改内容 |
|
|
94
101
|
| --- | --- |
|
|
95
102
|
| `src/common/menu/types.ts` | `PageConfig` 新增 `accessControlEnabled`、`routeKey` 字段;新增 `PublicPageItem` 类型(`Omit<PageConfig, ...>`);Window 声明新增 `__MICO_PAGES__` |
|
|
96
|
-
| `src/common/
|
|
103
|
+
| `src/common/portal-data.ts` | `findPageByPath`、`getMenuPage`(pageId 优先,其次 path 匹配页面 route) |
|
|
104
|
+
| `src/common/menu/parser.ts` | `extractRoutesFromPages`、`getDynamicRoutes`;从 portal-data 再导出 `findPageByPath`;菜单过滤与 `extractRoutes` 消费 `getMenuPage` |
|
|
97
105
|
| `src/app.tsx` | `patchClientRoutes` 改为调用 `getDynamicRoutes()` |
|
|
98
|
-
| `src/layouts/index.tsx` |
|
|
106
|
+
| `src/layouts/index.tsx` | 路由匹配 `allPageRoutes`;权限以 `menu_perms` + `routeKey` 为主,无 page 时菜单路由兜底 |
|
|
99
107
|
|
|
100
108
|
### 未修改文件
|
|
101
109
|
|
|
@@ -119,16 +127,19 @@ type PublicPageItem = Omit<
|
|
|
119
127
|
| 字段 | 类型 | 说明 |
|
|
120
128
|
| --- | --- | --- |
|
|
121
129
|
| `accessControlEnabled` | `boolean` | 是否开启访问控制。`false` 时跳过 SSO 认证和权限校验(公开页面);`true` 时需要登录且通过 `routeKey` 校验权限 |
|
|
122
|
-
| `routeKey` | `string \| null` |
|
|
130
|
+
| `routeKey` | `string \| null` | 路由权限标识,与用户信息 `menu_perms`、菜单项权限 code 一致 |
|
|
123
131
|
|
|
124
|
-
###
|
|
132
|
+
### 页面数据函数(`portal-data.ts` / `parser.ts`)
|
|
125
133
|
|
|
126
134
|
```typescript
|
|
127
135
|
/** 获取 window.__MICO_PAGES__ */
|
|
128
|
-
function
|
|
136
|
+
function getPages(): PublicPageItem[];
|
|
129
137
|
|
|
130
138
|
/** 判断页面数据是否可用 */
|
|
131
|
-
function
|
|
139
|
+
function hasPages(): boolean;
|
|
140
|
+
|
|
141
|
+
/** 菜单项 → 关联页面:pageId 优先,无匹配再用 path 与页面 route 匹配 */
|
|
142
|
+
function getMenuPage(item: MenuItem): PublicPageItem | undefined;
|
|
132
143
|
|
|
133
144
|
/** 从页面数据提取路由配置 */
|
|
134
145
|
function extractRoutesFromPages(pages: PublicPageItem[]): ParsedRoute[];
|
|
@@ -136,8 +147,11 @@ function extractRoutesFromPages(pages: PublicPageItem[]): ParsedRoute[];
|
|
|
136
147
|
/** 获取动态路由(PAGES 优先,降级 MENUS) */
|
|
137
148
|
function getDynamicRoutes(): ParsedRoute[];
|
|
138
149
|
|
|
139
|
-
/** 根据路径查找页面配置(精确匹配 +
|
|
140
|
-
function findPageByPath(
|
|
150
|
+
/** 根据路径查找页面配置(精确匹配 + 最长前缀 `/*`) */
|
|
151
|
+
function findPageByPath(
|
|
152
|
+
pages: PublicPageItem[],
|
|
153
|
+
pathname: string,
|
|
154
|
+
): PublicPageItem | undefined;
|
|
141
155
|
```
|
|
142
156
|
|
|
143
157
|
### Window 全局变量
|
|
@@ -156,24 +170,25 @@ interface Window {
|
|
|
156
170
|
| 决策点 | 选择 | 理由 |
|
|
157
171
|
| --- | --- | --- |
|
|
158
172
|
| 路由数据源 | `__MICO_PAGES__` 独立于菜单 | 页面和菜单是不同维度:页面定义"有什么路由",菜单定义"导航怎么组织";解耦后支持隐藏页面 |
|
|
159
|
-
| 权限方案 |
|
|
173
|
+
| 权限方案 | `menu_perms` + `routeKey` | 与后端新用户信息接口一致;菜单项 code 与 `menu_perms` 一一对应 |
|
|
160
174
|
| 降级策略 | `getDynamicRoutes()` 自动降级 | `__MICO_PAGES__` 未注入时回退到旧行为,零配置向后兼容 |
|
|
161
175
|
| PublicPageItem 定义 | `Omit<PageConfig, ...>` 派生 | 保持与 PageConfig 的类型继承关系,避免字段重复定义和类型漂移 |
|
|
162
176
|
| accessControlEnabled 语义 | `false` = 公开页面(跳过认证+授权),`true` = 需要权限检查 | 一个字段统一控制 SSO 认证、权限校验、子应用加载等待,减少配置冗余 |
|
|
163
177
|
| 隐藏页面权限 | 先 adminOnly 再 accessControlEnabled | adminOnly 是硬拦截,accessControlEnabled + routeKey 提供用户级粒度控制 |
|
|
178
|
+
| 菜单 → 页面解析 | `pageId` 优先,再 `path` | 中台以 id 为准;路径匹配用于无 id 或 id 失效时的兜底,与 `findPageByPath` 一致 |
|
|
164
179
|
|
|
165
180
|
## 已知限制与待改进
|
|
166
181
|
|
|
167
|
-
-
|
|
168
|
-
-
|
|
182
|
+
- 用户信息接口为 **`GET /user/info/`**(OpenAPI 8),与旧 `/api/user/info` 非同一后端;`fetchUserInfo` 已切换
|
|
183
|
+
- 外链类型菜单项需配置 `nameKey` 作为权限 code,否则无法匹配 `menu_perms`
|
|
169
184
|
|
|
170
185
|
## 注意事项
|
|
171
186
|
|
|
172
187
|
- `__MICO_PAGES__` 只包含用户在后台配置的微应用页面,404/403 等静态页面不在其中
|
|
173
|
-
- `__MICO_MENUS__`
|
|
174
|
-
-
|
|
188
|
+
- `__MICO_MENUS__` 中 `page` 类型项通过 **`pageId`**(优先)或 **`path` 与页面 `route` 匹配** 关联到 `__MICO_PAGES__` 中的页面配置
|
|
189
|
+
- 调试时可在控制台过滤 `routePermission` 查看结构化权限日志(详见 [路由权限日志](./feature-路由权限日志.md))
|
|
175
190
|
|
|
176
191
|
## 相关文档
|
|
177
192
|
|
|
178
193
|
- [微前端模式](./feature-微前端模式.md) - 微应用加载机制
|
|
179
|
-
- [菜单权限控制](./feature-菜单权限控制.md) -
|
|
194
|
+
- [菜单权限控制](./feature-菜单权限控制.md) - `menu_perms` 与菜单过滤
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# 路由权限日志
|
|
2
|
+
|
|
3
|
+
> 创建时间:2026-03-27
|
|
4
|
+
|
|
5
|
+
## 功能概述
|
|
6
|
+
|
|
7
|
+
在 layout 中输出两类结构化开发日志,便于本地排查权限问题:
|
|
8
|
+
|
|
9
|
+
1. **`routePermission`**:`BasicLayout` 中 **`isForbidden` 路由权限判定**(为何放行 / 为何 403)。
|
|
10
|
+
2. **`menuFilter`**:`filterMenuItems` 中**被隐藏的菜单项**及**隐藏原因**(侧栏不可见条目)。
|
|
11
|
+
|
|
12
|
+
日志均经 `layoutLogger` 输出,**仅开发环境生效**,生产构建无控制台输出。
|
|
13
|
+
|
|
14
|
+
## 技术方案
|
|
15
|
+
|
|
16
|
+
### 技术栈
|
|
17
|
+
|
|
18
|
+
- 框架:React 18 + @umijs/max
|
|
19
|
+
- 日志:`apps/layout/src/common/logger.ts` 的 `layoutLogger`(`NODE_ENV === 'development'` 时 `console.log`)
|
|
20
|
+
|
|
21
|
+
### 核心实现
|
|
22
|
+
|
|
23
|
+
1. 在 `BasicLayout` 的 `useMemo`(`isForbidden`)中,于各分支返回前调用 `layoutLogger.log('routePermission', payload)`。
|
|
24
|
+
2. 统一使用 **`routePermission`** 作为首参,便于控制台过滤;`payload` 为结构化对象,包含 `verdict`、`reason`、`branch`(部分分支)及路径、页面上下文。
|
|
25
|
+
3. **不记录**「无动态路由 `currentRoute`」与「无页面元数据且未命中菜单兜底」的常见放行路径,避免刷屏。
|
|
26
|
+
4. 在 `parser.ts` 的 `filterMenuItems` 中,于菜单项被剔除时输出 **`menuFilter`**;**关闭权限**或**超级用户**时不打菜单隐藏日志(此时无实质过滤)。
|
|
27
|
+
|
|
28
|
+
## 文件清单
|
|
29
|
+
|
|
30
|
+
### 修改文件
|
|
31
|
+
|
|
32
|
+
| 文件路径 | 说明 |
|
|
33
|
+
|----------|------|
|
|
34
|
+
| `apps/layout/src/layouts/index.tsx` | `isForbidden` 内各关键分支的 `routePermission` 日志 |
|
|
35
|
+
| `apps/layout/src/common/menu/parser.ts` | `getMenuItemFilterOutcome`、`filterMenuItems` 内 `menuFilter` 隐藏日志 |
|
|
36
|
+
|
|
37
|
+
## 日志字段说明
|
|
38
|
+
|
|
39
|
+
### 通用字段
|
|
40
|
+
|
|
41
|
+
| 字段 | 说明 |
|
|
42
|
+
|------|------|
|
|
43
|
+
| `verdict` | `skip`:跳过权限校验;`allow`:允许访问;`deny`:403 |
|
|
44
|
+
| `reason` | 见下表 |
|
|
45
|
+
| `pathname` | 当前 `location.pathname` |
|
|
46
|
+
| `routePath` | 匹配到的动态路由 `currentRoute.path`(有 `currentRoute` 时) |
|
|
47
|
+
| `userId` | `currentUser.id` |
|
|
48
|
+
| `isSuperuser` | 是否超管 |
|
|
49
|
+
| `menuPermsCount` | `menu_perms` 长度 |
|
|
50
|
+
|
|
51
|
+
### `reason` 取值
|
|
52
|
+
|
|
53
|
+
| reason | verdict | 含义 |
|
|
54
|
+
|--------|---------|------|
|
|
55
|
+
| `disableAuth` | skip | `__MICO_CONFIG__.disableAuth` 等效关闭权限 |
|
|
56
|
+
| `noPermissionRoute` | skip | 命中免权限路由列表 |
|
|
57
|
+
| `superuser` | skip | 超级用户不校验 |
|
|
58
|
+
| `adminOnly` | deny | 页面 `adminOnly`,非超管 |
|
|
59
|
+
| `publicPage` | allow | `accessControlEnabled === false` |
|
|
60
|
+
| `accessControlNoRouteKey` | allow | 已开启访问控制但未配置 `routeKey`,不做 `menu_perms` 校验(与侧栏 `isMenuPageRequiringPermCode` 一致) |
|
|
61
|
+
| `routeKeyNotInMenuPerms` | deny | `routeKey` 不在 `menu_perms` |
|
|
62
|
+
| `routeKeyInMenuPerms` | allow | `routeKey` 在 `menu_perms` |
|
|
63
|
+
| `notInFilteredMenuRoutes` | deny | 菜单兜底分支:过滤后路由不包含当前路径 |
|
|
64
|
+
| `inFilteredMenuRoutes` | allow | 菜单兜底分支:过滤后仍包含 |
|
|
65
|
+
|
|
66
|
+
### 分支字段 `branch`
|
|
67
|
+
|
|
68
|
+
| branch | 含义 |
|
|
69
|
+
|--------|------|
|
|
70
|
+
| `pageMeta` | 已解析到 `__MICO_PAGES__` 页面配置时的 `routeKey` / `menu_perms` 判定 |
|
|
71
|
+
| `menuRouteFallback` | 无页面元数据时,用「全量菜单路由 vs `filterMenuItems` 后菜单路由」比对 |
|
|
72
|
+
|
|
73
|
+
### 页面元数据分支额外字段
|
|
74
|
+
|
|
75
|
+
| 字段 | 说明 |
|
|
76
|
+
|------|------|
|
|
77
|
+
| `pageId` | 页面配置 id |
|
|
78
|
+
| `routeKey` | 页面 `routeKey` |
|
|
79
|
+
| `keyInMenuPerms` | `routeKey` 是否在 `menu_perms` 中(仅当需校验 `routeKey` 时输出) |
|
|
80
|
+
|
|
81
|
+
### 菜单兜底分支额外字段
|
|
82
|
+
|
|
83
|
+
| 字段 | 说明 |
|
|
84
|
+
|------|------|
|
|
85
|
+
| `inAllowed` | 当前路径是否在过滤后的允许菜单路由中 |
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 菜单过滤日志 `menuFilter`
|
|
90
|
+
|
|
91
|
+
### 通用字段
|
|
92
|
+
|
|
93
|
+
| 字段 | 说明 |
|
|
94
|
+
|------|------|
|
|
95
|
+
| `verdict` | 固定 `hidden` |
|
|
96
|
+
| `reason` | 见下表 |
|
|
97
|
+
| `menuId` | 菜单项 `id` |
|
|
98
|
+
| `menuType` | `group` / `page` / `link` |
|
|
99
|
+
| `name` | 菜单展示名(配置中的 `name`) |
|
|
100
|
+
| `nameKey` | 菜单 `nameKey`(若有) |
|
|
101
|
+
| `permCode` | `getMenuItemPermCode` 解析出的权限 code(可能为 `null`) |
|
|
102
|
+
| `menuPermsCount` | 当前用户 `menu_perms` 长度 |
|
|
103
|
+
| `path` | 菜单路由或关联页路由 |
|
|
104
|
+
|
|
105
|
+
### `menuFilter.reason` 取值
|
|
106
|
+
|
|
107
|
+
| reason | 含义 |
|
|
108
|
+
|--------|------|
|
|
109
|
+
| `adminOnly` | 菜单项 `adminOnly`,非超管不可见 |
|
|
110
|
+
| `missingPermCode` | `page` 无可用 `routeKey`、`link` 无 `nameKey`,无法匹配 `menu_perms` |
|
|
111
|
+
| `menuPermsEmpty` | `menu_perms` 为空数组 |
|
|
112
|
+
| `permCodeNotInMenuPerms` | 权限 code 不在 `menu_perms` 中 |
|
|
113
|
+
| `groupNoVisibleChildren` | 分组下子菜单全部过滤后无可见项 |
|
|
114
|
+
| `allChildrenFilteredOut` | 父级单项通过(`page`/`link` 有权限),但子级全部过滤后无子项可展示 |
|
|
115
|
+
|
|
116
|
+
说明:`group` 作为容器在子项仍可见时**不会**被记为 `hidden`;仅当整组被剔除时打日志。
|
|
117
|
+
|
|
118
|
+
## 使用示例
|
|
119
|
+
|
|
120
|
+
在浏览器控制台过滤:
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
routePermission
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
或:
|
|
127
|
+
|
|
128
|
+
```text
|
|
129
|
+
menuFilter
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
或:
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
[Layout]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
(具体前缀以 `layoutLogger` 实现为准。)
|
|
139
|
+
|
|
140
|
+
## 设计决策
|
|
141
|
+
|
|
142
|
+
| 决策点 | 选择 | 理由 |
|
|
143
|
+
|--------|------|------|
|
|
144
|
+
| 日志前缀 | 固定 `'routePermission'` + 对象载荷 | 可过滤、可结构化,便于 AI/人工检索 |
|
|
145
|
+
| 生产环境 | 静默 | `layoutLogger` 在 `production` 下不输出 |
|
|
146
|
+
| 无 `currentRoute` 不打日志 | 跳过 | 静态路由访问频繁,避免噪音 |
|
|
147
|
+
| `menuFilter` 与超管/关权限 | 不打 | 无过滤效果时无隐藏项可记 |
|
|
148
|
+
|
|
149
|
+
## 已知限制与待改进
|
|
150
|
+
|
|
151
|
+
- 日志仅在**开发环境**可见;线上问题需依赖 Sentry 或其它监控,不在本功能内。
|
|
152
|
+
- `renderContent` 另有 `renderContent:` 日志,与 `routePermission` 独立,排查时可同时参考。
|
|
153
|
+
|
|
154
|
+
## 注意事项
|
|
155
|
+
|
|
156
|
+
- 权限判定逻辑以 `layouts/index.tsx` 与 [菜单权限控制](./feature-菜单权限控制.md)、[路由与菜单解耦](./feature-路由与菜单解耦.md) 为准;本文档**仅描述日志字段**,不替代权限语义说明。
|
|
157
|
+
|
|
158
|
+
## 相关文档
|
|
159
|
+
|
|
160
|
+
- [菜单权限控制](./feature-菜单权限控制.md)
|
|
161
|
+
- [路由与菜单解耦](./feature-路由与菜单解耦.md)
|
|
162
|
+
- [日志与常量](./arch-日志与常量.md)(`layoutLogger` 行为)
|