generator-mico-cli 0.2.8 → 0.2.10
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/generators/micro-react/index.js +8 -3
- package/generators/micro-react/templates/apps/layout/config/config.dev.ts +1 -1
- 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 +187 -53
- package/generators/micro-react/templates/apps/layout/src/app.tsx +16 -4
- package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +15 -0
- package/generators/micro-react/templates/apps/layout/src/common/request/index.ts +2 -2
- package/generators/micro-react/templates/apps/layout/src/components/AppTabs/index.tsx +2 -2
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +8 -2
- package/generators/micro-react/templates/apps/layout/src/constants/index.ts +108 -5
- package/generators/micro-react/templates/apps/layout/src/hooks/useRoutePermissionRefresh.ts +2 -2
- package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.tsx +1 -1
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +8 -2
- package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +16 -5
- package/generators/micro-react/templates/apps/layout/src/services/user.ts +1 -0
- package/generators/subapp-react/index.js +10 -1
- package/generators/subapp-react/templates/homepage/config/config.prod.development.ts +0 -1
- package/generators/subapp-react/templates/homepage/config/config.prod.testing.ts +0 -1
- package/package.json +1 -1
|
@@ -139,15 +139,20 @@ module.exports = class extends Generator {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
install() {
|
|
143
|
+
this.log('');
|
|
144
|
+
this.log('📦 正在安装依赖...');
|
|
145
|
+
this.spawnCommandSync('pnpm', ['install'], {
|
|
146
|
+
cwd: this.destDir
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
142
150
|
end() {
|
|
143
151
|
this.log('');
|
|
144
152
|
this.log('✅ 项目创建成功!');
|
|
145
153
|
this.log('');
|
|
146
154
|
this.log(' 后续步骤:');
|
|
147
155
|
this.log('');
|
|
148
|
-
this.log(' # 安装依赖');
|
|
149
|
-
this.log(' pnpm install');
|
|
150
|
-
this.log('');
|
|
151
156
|
this.log(' # 启动开发服务器');
|
|
152
157
|
this.log(' pnpm dev');
|
|
153
158
|
this.log('');
|
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
# 菜单权限控制
|
|
2
2
|
|
|
3
3
|
> 创建时间:2026-01-24
|
|
4
|
+
> 更新时间:2026-01-26
|
|
4
5
|
|
|
5
6
|
## 功能概述
|
|
6
7
|
|
|
7
8
|
基于用户信息中的 `side_menus` 字段实现菜单和路由的白名单权限控制。非超级用户只能看到和访问 `side_menus` 中配置的菜单项,访问无权限路由时显示 403 页面。
|
|
8
9
|
|
|
10
|
+
**v2 更新**:支持认证(Authentication)与授权(Authorization)分离配置,可独立控制"跳过 SSO 登录"和"跳过菜单权限校验"。
|
|
11
|
+
|
|
12
|
+
## 核心概念
|
|
13
|
+
|
|
14
|
+
### 认证 vs 授权
|
|
15
|
+
|
|
16
|
+
| 概念 | 英文 | 作用 | 配置项 |
|
|
17
|
+
| --- | --- | --- | --- |
|
|
18
|
+
| 认证 | Authentication | 确认用户身份(是否登录) | `noAuthRouteList` |
|
|
19
|
+
| 授权 | Authorization | 确认用户权限(能否访问) | `noPermissionRouteList` |
|
|
20
|
+
|
|
21
|
+
### 四种路由场景
|
|
22
|
+
|
|
23
|
+
| 场景 | noAuthRouteList | noPermissionRouteList | 示例 |
|
|
24
|
+
| --- | --- | --- | --- |
|
|
25
|
+
| 公开页面 | ✅ | ✅ | 首页、活动页 |
|
|
26
|
+
| 登录后公共页面 | ❌ | ✅ | 个人设置、帮助中心 |
|
|
27
|
+
| 需要权限的页面 | ❌ | ❌ | 后台管理、业务功能 |
|
|
28
|
+
| 登录页等特殊页面 | ✅ | ✅ | /user/login、/403 |
|
|
29
|
+
|
|
9
30
|
## 技术方案
|
|
10
31
|
|
|
11
32
|
### 技术栈
|
|
@@ -14,24 +35,68 @@
|
|
|
14
35
|
- UI 组件:Arco Design (Result 组件)
|
|
15
36
|
- 状态管理:Umi initialState
|
|
16
37
|
|
|
17
|
-
###
|
|
38
|
+
### 权限控制流程
|
|
18
39
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
40
|
+
```
|
|
41
|
+
用户访问路由
|
|
42
|
+
│
|
|
43
|
+
▼
|
|
44
|
+
┌─────────────────────────────────┐
|
|
45
|
+
│ 1. SSO 认证检查 (app.tsx) │
|
|
46
|
+
│ isNoAuthRoute(pathname)? │
|
|
47
|
+
│ ├── 是 → 跳过 SSO │
|
|
48
|
+
│ └── 否 → 执行 ensureSsoSession()
|
|
49
|
+
└─────────────────────────────────┘
|
|
50
|
+
│
|
|
51
|
+
▼
|
|
52
|
+
┌─────────────────────────────────┐
|
|
53
|
+
│ 2. 权限校验 (layouts/index.tsx) │
|
|
54
|
+
│ isNoPermissionRoute(pathname)?
|
|
55
|
+
│ ├── 是 → 跳过权限校验 │
|
|
56
|
+
│ └── 否 → 检查 side_menus │
|
|
57
|
+
│ ├── 有权限 → 正常渲染
|
|
58
|
+
│ └── 无权限 → 显示 403
|
|
59
|
+
└─────────────────────────────────┘
|
|
60
|
+
│
|
|
61
|
+
▼
|
|
62
|
+
┌─────────────────────────────────┐
|
|
63
|
+
│ 3. 菜单渲染 (menu/index.tsx) │
|
|
64
|
+
│ isNoPermissionRoute(pathname)?
|
|
65
|
+
│ ├── 是 → 显示全部菜单 │
|
|
66
|
+
│ └── 否 → 按 side_menus 过滤 │
|
|
67
|
+
└─────────────────────────────────┘
|
|
68
|
+
│
|
|
69
|
+
▼
|
|
70
|
+
┌─────────────────────────────────┐
|
|
71
|
+
│ 4. 子应用加载 (MicroAppLoader) │
|
|
72
|
+
│ isNoPermissionRoute(pathname)?
|
|
73
|
+
│ ├── 是 → 直接加载 │
|
|
74
|
+
│ └── 否 → 等待 currentUser │
|
|
75
|
+
└─────────────────────────────────┘
|
|
76
|
+
```
|
|
23
77
|
|
|
24
|
-
###
|
|
78
|
+
### 权限判断逻辑(详细)
|
|
25
79
|
|
|
26
80
|
```
|
|
27
81
|
用户访问 /some-path
|
|
28
|
-
|
|
82
|
+
│
|
|
83
|
+
▼
|
|
84
|
+
是否 disableAuth === true?
|
|
85
|
+
├── 是 → 允许所有访问(调试模式)
|
|
86
|
+
│
|
|
87
|
+
▼
|
|
88
|
+
是否在 noPermissionRouteList 中?
|
|
89
|
+
├── 是 → 允许访问,显示全部菜单
|
|
90
|
+
│
|
|
91
|
+
▼
|
|
29
92
|
是否是超级用户?
|
|
30
93
|
├── 是 → 允许访问所有菜单
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
94
|
+
│
|
|
95
|
+
▼
|
|
96
|
+
检查 side_menus 白名单
|
|
97
|
+
├── 精确匹配:menuPath === side_menus[i]
|
|
98
|
+
├── 前缀匹配:side_menus[i].startsWith(menuPath + '.')
|
|
99
|
+
└── 都不匹配 → 禁止访问,显示 403
|
|
35
100
|
```
|
|
36
101
|
|
|
37
102
|
## 文件清单
|
|
@@ -39,20 +104,59 @@
|
|
|
39
104
|
### 新增文件
|
|
40
105
|
|
|
41
106
|
| 文件路径 | 说明 |
|
|
42
|
-
|
|
107
|
+
| --- | --- |
|
|
43
108
|
| `src/pages/403/index.tsx` | 403 无权限页面组件 |
|
|
44
109
|
|
|
45
110
|
### 修改文件
|
|
46
111
|
|
|
47
112
|
| 文件路径 | 修改内容 |
|
|
48
|
-
|
|
113
|
+
| --- | --- |
|
|
114
|
+
| `src/common/menu/types.ts` | 新增 `noPermissionRouteList` 类型定义 |
|
|
115
|
+
| `src/constants/index.ts` | 新增 `isNoPermissionRoute`、`getNoPermissionRouteList` 函数 |
|
|
49
116
|
| `src/common/menu/parser.ts` | 新增 `filterMenuItems`、`isMenuAllowed` 等权限过滤函数 |
|
|
50
|
-
| `src/layouts/index.tsx` | 新增 `isForbidden`
|
|
51
|
-
| `src/layouts/components/menu/index.tsx` |
|
|
117
|
+
| `src/layouts/index.tsx` | 新增 `isForbidden` 判断,集成 `isNoPermissionRoute` |
|
|
118
|
+
| `src/layouts/components/menu/index.tsx` | 集成菜单过滤,支持免权限校验路由 |
|
|
119
|
+
| `src/components/MicroAppLoader/index.tsx` | 集成 `isNoPermissionRoute`,免权限路由直接加载 |
|
|
120
|
+
| `src/app.tsx` | SSO 认证检查,使用 `isNoAuthRoute` |
|
|
52
121
|
| `config/routes.ts` | 新增 `/403` 路由 |
|
|
53
122
|
|
|
54
123
|
## API / 组件接口
|
|
55
124
|
|
|
125
|
+
### Window 配置
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
interface Window {
|
|
129
|
+
__MICO_CONFIG__?: {
|
|
130
|
+
/**
|
|
131
|
+
* 免认证路由列表(跳过 SSO 登录)
|
|
132
|
+
* 支持精确匹配和前缀匹配(以 /* 结尾)
|
|
133
|
+
*/
|
|
134
|
+
noAuthRouteList?: string[];
|
|
135
|
+
/**
|
|
136
|
+
* 免权限校验路由列表(跳过菜单权限检查)
|
|
137
|
+
* 支持精确匹配和前缀匹配(以 /* 结尾)
|
|
138
|
+
* 注意:如果同时需要跳过 SSO,需同时配置 noAuthRouteList
|
|
139
|
+
*/
|
|
140
|
+
noPermissionRouteList?: string[];
|
|
141
|
+
/** 关闭权限控制(菜单全部显示,路由不校验权限) */
|
|
142
|
+
disableAuth?: boolean;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 路由判断函数
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// 判断是否为免认证路由(跳过 SSO)
|
|
151
|
+
function isNoAuthRoute(pathname: string): boolean;
|
|
152
|
+
|
|
153
|
+
// 判断是否为免权限校验路由(跳过菜单权限检查)
|
|
154
|
+
function isNoPermissionRoute(pathname: string): boolean;
|
|
155
|
+
|
|
156
|
+
// 判断是否关闭权限控制
|
|
157
|
+
function isAuthDisabled(): boolean;
|
|
158
|
+
```
|
|
159
|
+
|
|
56
160
|
### MenuFilterOptions
|
|
57
161
|
|
|
58
162
|
```typescript
|
|
@@ -77,26 +181,6 @@ function filterMenuItems(
|
|
|
77
181
|
): MenuItem[];
|
|
78
182
|
```
|
|
79
183
|
|
|
80
|
-
**参数说明**:
|
|
81
|
-
|
|
82
|
-
| 参数 | 类型 | 必填 | 说明 |
|
|
83
|
-
|------|------|------|------|
|
|
84
|
-
| items | MenuItem[] | 是 | 原始菜单数据 |
|
|
85
|
-
| options | MenuFilterOptions | 否 | 过滤选项 |
|
|
86
|
-
| parentPath | string | 否 | 父级路径(递归用) |
|
|
87
|
-
|
|
88
|
-
### isRouteAllowed
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
/**
|
|
92
|
-
* 检查路由是否允许访问
|
|
93
|
-
*/
|
|
94
|
-
function isRouteAllowed(
|
|
95
|
-
menuPath: string,
|
|
96
|
-
options?: MenuFilterOptions,
|
|
97
|
-
): boolean;
|
|
98
|
-
```
|
|
99
|
-
|
|
100
184
|
### 用户信息相关字段
|
|
101
185
|
|
|
102
186
|
```typescript
|
|
@@ -111,14 +195,45 @@ interface IUserInfo {
|
|
|
111
195
|
|
|
112
196
|
## 使用示例
|
|
113
197
|
|
|
114
|
-
###
|
|
198
|
+
### 配置免认证+免权限路由
|
|
199
|
+
|
|
200
|
+
```javascript
|
|
201
|
+
// config/config.dev.ts
|
|
202
|
+
window.__MICO_CONFIG__ = {
|
|
203
|
+
// 免认证路由(跳过 SSO 登录)
|
|
204
|
+
noAuthRouteList: [
|
|
205
|
+
'/',
|
|
206
|
+
'/subapp',
|
|
207
|
+
'/public/*', // 前缀匹配
|
|
208
|
+
],
|
|
209
|
+
// 免权限校验路由(跳过菜单权限检查)
|
|
210
|
+
noPermissionRouteList: [
|
|
211
|
+
'/',
|
|
212
|
+
'/subapp',
|
|
213
|
+
'/settings', // 登录后的公共页面
|
|
214
|
+
],
|
|
215
|
+
disableAuth: false,
|
|
216
|
+
};
|
|
217
|
+
```
|
|
115
218
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
219
|
+
### 静态配置(代码中)
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// src/constants/index.ts
|
|
223
|
+
|
|
224
|
+
// 静态免认证路由(始终生效)
|
|
225
|
+
export const NO_AUTH_ROUTE_LIST: string[] = [
|
|
226
|
+
'/user/login',
|
|
227
|
+
'/user/register',
|
|
228
|
+
'/403',
|
|
229
|
+
'/404',
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
// 静态免权限校验路由(始终生效)
|
|
233
|
+
export const NO_PERMISSION_ROUTE_LIST: string[] = [
|
|
234
|
+
'/403',
|
|
235
|
+
'/404',
|
|
236
|
+
];
|
|
122
237
|
```
|
|
123
238
|
|
|
124
239
|
### 菜单过滤结果
|
|
@@ -138,38 +253,57 @@ interface IUserInfo {
|
|
|
138
253
|
```tsx
|
|
139
254
|
// layouts/index.tsx
|
|
140
255
|
const isForbidden = useMemo(() => {
|
|
141
|
-
|
|
256
|
+
// 关闭权限控制时,不校验权限
|
|
257
|
+
if (isAuthDisabled()) return false;
|
|
258
|
+
// 免权限校验路由,不检查菜单权限
|
|
259
|
+
if (isNoPermissionRoute(location.pathname)) return false;
|
|
260
|
+
// 如果在有权限的路由中找到了,说明有权限
|
|
261
|
+
if (currentRoute) return false;
|
|
262
|
+
// 在所有路由中存在但无权限
|
|
142
263
|
const routeInAll = findRouteByPath(allRoutes, location.pathname);
|
|
143
|
-
return !!routeInAll;
|
|
264
|
+
return !!routeInAll;
|
|
144
265
|
}, [currentRoute, allRoutes, location.pathname]);
|
|
266
|
+
```
|
|
145
267
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
268
|
+
### MicroAppLoader 中的认证判断
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
// components/MicroAppLoader/index.tsx
|
|
272
|
+
const isAuthReady =
|
|
273
|
+
isAuthDisabled() ||
|
|
274
|
+
isNoPermissionRoute(location.pathname) ||
|
|
275
|
+
!!initialState?.currentUser;
|
|
276
|
+
|
|
277
|
+
// 未准备好时不加载子应用
|
|
278
|
+
if (!isAuthReady) {
|
|
279
|
+
return; // 继续等待
|
|
280
|
+
}
|
|
152
281
|
```
|
|
153
282
|
|
|
154
283
|
## 设计决策
|
|
155
284
|
|
|
156
285
|
| 决策点 | 选择 | 理由 |
|
|
157
|
-
|
|
286
|
+
| --- | --- | --- |
|
|
287
|
+
| 认证与授权分离 | 两个独立配置项 | 不同场景需要不同组合,如"需要登录但不需要权限"的个人设置页 |
|
|
158
288
|
| 权限模型 | 白名单 (`side_menus`) | 后端返回的 `side_menus` 是允许列表,比黑名单更安全 |
|
|
159
289
|
| 403 处理 | 原地渲染组件 | 保持 URL 不变,用户体验更好,便于分享链接 |
|
|
160
290
|
| 父级菜单显示 | 前缀匹配 | 子菜单有权限时,父级菜单需要作为容器显示 |
|
|
161
291
|
| 超级用户 | 跳过所有检查 | 管理员需要完整访问权限 |
|
|
162
|
-
|
|
|
292
|
+
| 免权限路由的菜单 | 显示全部 | 用户访问公开页面时应能看到所有导航选项 |
|
|
293
|
+
| 免权限路由的子应用 | 直接加载 | 不等待 currentUser,避免加载卡住 |
|
|
163
294
|
|
|
164
295
|
## 注意事项
|
|
165
296
|
|
|
297
|
+
- `noAuthRouteList` 和 `noPermissionRouteList` 是独立的,需要根据场景分别配置
|
|
298
|
+
- 两个列表都支持 `/*` 后缀进行前缀匹配
|
|
166
299
|
- `side_menus` 为空时,非超级用户没有任何菜单权限
|
|
167
300
|
- `side_menus` 格式为菜单路径,如 `"列队管理.配置队列"`
|
|
168
301
|
- `miss_permissions` 用于按钮级别权限控制,不影响菜单显示
|
|
169
302
|
- 403 页面在 Layout 内渲染,不会触发路由跳转
|
|
170
303
|
- 调试时可在控制台搜索 `isForbidden check` 查看权限判断日志
|
|
304
|
+
- **常见问题**:配置了 `noAuthRouteList` 但页面仍显示 403,需同时配置 `noPermissionRouteList`
|
|
171
305
|
|
|
172
306
|
## 相关文档
|
|
173
307
|
|
|
174
308
|
- [微前端模式](./feature-微前端模式.md) - 路由和菜单解析
|
|
175
|
-
- [
|
|
309
|
+
- [日志与常量](./arch-日志与常量.md) - 常量管理
|
|
@@ -10,15 +10,18 @@ import { getStoredAuthToken } from './common/auth/auth-manager';
|
|
|
10
10
|
import type { IUserInfo } from './common/auth/type';
|
|
11
11
|
import { fetchUserInfo } from './services/user';
|
|
12
12
|
import { extractRoutes, getWindowMenus } from './common/menu';
|
|
13
|
-
import { ensureSsoSession } from './common/request/sso';
|
|
14
13
|
import {
|
|
15
14
|
clearMicroAppProps,
|
|
16
15
|
type IMicroAppProps,
|
|
17
16
|
setMicroAppProps,
|
|
18
17
|
} from './common/micro';
|
|
18
|
+
import {
|
|
19
|
+
ensureSsoSession,
|
|
20
|
+
handleAuthFailureRedirect,
|
|
21
|
+
} from './common/request/sso';
|
|
19
22
|
import { initTheme } from './common/theme';
|
|
20
23
|
import MicroAppLoader from './components/MicroAppLoader';
|
|
21
|
-
import {
|
|
24
|
+
import { isNoAuthRoute } from '@/constants';
|
|
22
25
|
import './global.less';
|
|
23
26
|
|
|
24
27
|
// ==================== qiankun 全局错误处理 ====================
|
|
@@ -110,10 +113,10 @@ export async function getInitialState(): Promise<{
|
|
|
110
113
|
};
|
|
111
114
|
|
|
112
115
|
const { location } = history;
|
|
113
|
-
const
|
|
116
|
+
const noAuthRoute = isNoAuthRoute(location.pathname);
|
|
114
117
|
|
|
115
118
|
// 非免认证路由:走 SSO 流程
|
|
116
|
-
if (!
|
|
119
|
+
if (!noAuthRoute) {
|
|
117
120
|
await ensureSsoSession();
|
|
118
121
|
}
|
|
119
122
|
|
|
@@ -128,6 +131,15 @@ export async function getInitialState(): Promise<{
|
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
133
|
|
|
134
|
+
// 非免认证路由且没有 token,跳转到 SSO 登录
|
|
135
|
+
if (!noAuthRoute) {
|
|
136
|
+
handleAuthFailureRedirect();
|
|
137
|
+
// 返回空状态,页面会被重定向
|
|
138
|
+
return {
|
|
139
|
+
fetchUserInfo: fetchUserInfoFn,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
131
143
|
return {
|
|
132
144
|
fetchUserInfo: fetchUserInfoFn,
|
|
133
145
|
};
|
|
@@ -157,6 +157,21 @@ declare global {
|
|
|
157
157
|
apiBaseUrl?: string;
|
|
158
158
|
/** 默认重定向路径,访问 "/" 时自动跳转到此路径 */
|
|
159
159
|
defaultPath?: string;
|
|
160
|
+
/**
|
|
161
|
+
* 免认证路由列表(跳过 SSO 登录)
|
|
162
|
+
* 支持精确匹配和前缀匹配(以 /* 结尾)
|
|
163
|
+
*/
|
|
164
|
+
noAuthRouteList?: string[];
|
|
165
|
+
/**
|
|
166
|
+
* 免权限校验路由列表(跳过菜单权限检查)
|
|
167
|
+
* 支持精确匹配和前缀匹配(以 /* 结尾)
|
|
168
|
+
* 注意:如果同时需要跳过 SSO,需同时配置 noAuthRouteList
|
|
169
|
+
*/
|
|
170
|
+
noPermissionRouteList?: string[];
|
|
171
|
+
/** 动态配置的不显示布局路由列表,支持精确匹配和前缀匹配(以 /* 结尾) */
|
|
172
|
+
noLayoutRouteList?: string[];
|
|
173
|
+
/** 关闭权限控制(菜单全部显示,路由不校验权限) */
|
|
174
|
+
disableAuth?: boolean;
|
|
160
175
|
[key: string]: unknown;
|
|
161
176
|
};
|
|
162
177
|
__MICO_WORKSPACE__?: WorkspaceConfig | null;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { request as rawRequest } from '@umijs/max';
|
|
18
18
|
import { setStoredAuthToken } from '../auth/auth-manager';
|
|
19
|
-
import {
|
|
19
|
+
import { isNoAuthRoute } from '@/constants';
|
|
20
20
|
|
|
21
21
|
// 配置相关
|
|
22
22
|
import {
|
|
@@ -64,7 +64,7 @@ initDefaultInterceptors(isFetchingToken, addToPendingQueue);
|
|
|
64
64
|
* 判断当前路由是否跳过认证
|
|
65
65
|
*/
|
|
66
66
|
const shouldSkipAuth = (): boolean => {
|
|
67
|
-
return
|
|
67
|
+
return isNoAuthRoute(location.pathname);
|
|
68
68
|
};
|
|
69
69
|
|
|
70
70
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { findRouteByPath } from '@/common/menu';
|
|
2
|
-
import {
|
|
2
|
+
import { isNoLayoutRoute } from '@/constants';
|
|
3
3
|
import useMenu from '@/hooks/useMenu';
|
|
4
4
|
import { Tabs } from '@arco-design/web-react';
|
|
5
5
|
import { history, useLocation } from '@umijs/max';
|
|
@@ -47,7 +47,7 @@ function AppTabs() {
|
|
|
47
47
|
const [tabs, setTabs] = useState<TabItem[]>([]);
|
|
48
48
|
|
|
49
49
|
const showTabs = useMemo(() => {
|
|
50
|
-
return !
|
|
50
|
+
return !isNoLayoutRoute(location.pathname);
|
|
51
51
|
}, [location.pathname]);
|
|
52
52
|
|
|
53
53
|
const microAppPrefixes = useMemo(() => {
|
package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { getAuthInfo } from '@/common/auth/auth-manager';
|
|
2
2
|
import { EEnv, getEnv } from '@/common/env';
|
|
3
3
|
import { request } from '@/common/request';
|
|
4
|
+
import { isAuthDisabled, isNoPermissionRoute } from '@/constants';
|
|
4
5
|
import { Spin } from '@arco-design/web-react';
|
|
5
|
-
import { useModel } from '@umijs/max';
|
|
6
|
+
import { useLocation, useModel } from '@umijs/max';
|
|
6
7
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
7
8
|
import './index.less';
|
|
8
9
|
import { microAppManager, type MicroAppState } from './micro-app-manager';
|
|
@@ -45,8 +46,13 @@ const MicroAppLoader: React.FC<MicroAppLoaderProps> = ({
|
|
|
45
46
|
});
|
|
46
47
|
|
|
47
48
|
// 获取 initialState,等待用户信息准备好再加载子应用
|
|
49
|
+
// disableAuth 模式或免权限校验路由下跳过等待
|
|
48
50
|
const { initialState } = useModel('@@initialState');
|
|
49
|
-
const
|
|
51
|
+
const location = useLocation();
|
|
52
|
+
const isAuthReady =
|
|
53
|
+
isAuthDisabled() ||
|
|
54
|
+
isNoPermissionRoute(location.pathname) ||
|
|
55
|
+
!!initialState?.currentUser;
|
|
50
56
|
|
|
51
57
|
const appName = sanitizeId(name);
|
|
52
58
|
|
|
@@ -19,7 +19,7 @@ export const ROUTES = {
|
|
|
19
19
|
} as const;
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
22
|
+
* 无需认证的路由列表(静态配置)
|
|
23
23
|
*/
|
|
24
24
|
export const NO_AUTH_ROUTE_LIST: string[] = [
|
|
25
25
|
ROUTES.LOGIN,
|
|
@@ -29,12 +29,115 @@ export const NO_AUTH_ROUTE_LIST: string[] = [
|
|
|
29
29
|
ROUTES.NOT_FOUND,
|
|
30
30
|
];
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* 获取合并后的免鉴权路由列表
|
|
34
|
+
* 合并静态常量 + window.__MICO_CONFIG__.noAuthRouteList
|
|
35
|
+
*/
|
|
36
|
+
export const getNoAuthRouteList = (): string[] => {
|
|
37
|
+
const dynamicRoutes = window.__MICO_CONFIG__?.noAuthRouteList ?? [];
|
|
38
|
+
return [...new Set([...NO_AUTH_ROUTE_LIST, ...dynamicRoutes])];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 判断路径是否匹配路由列表(支持精确匹配和前缀匹配)
|
|
43
|
+
* @param pathname - 当前路由路径
|
|
44
|
+
* @param routes - 路由列表
|
|
45
|
+
* @returns 是否匹配
|
|
46
|
+
*/
|
|
47
|
+
const matchRouteList = (pathname: string, routes: string[]): boolean => {
|
|
48
|
+
return routes.some((route) => {
|
|
49
|
+
// 前缀匹配:/public/* 匹配 /public/xxx
|
|
50
|
+
if (route.endsWith('/*')) {
|
|
51
|
+
const prefix = route.slice(0, -1); // 去掉末尾的 *,保留 /
|
|
52
|
+
return pathname.startsWith(prefix) || pathname === prefix.slice(0, -1);
|
|
53
|
+
}
|
|
54
|
+
// 精确匹配
|
|
55
|
+
return pathname === route;
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 判断指定路径是否为免认证路由(跳过 SSO 登录)
|
|
61
|
+
* 支持精确匹配和前缀匹配(以 /* 结尾的模式)
|
|
62
|
+
* @param pathname - 当前路由路径
|
|
63
|
+
* @returns 是否免认证
|
|
64
|
+
*/
|
|
65
|
+
export const isNoAuthRoute = (pathname: string): boolean => {
|
|
66
|
+
return matchRouteList(pathname, getNoAuthRouteList());
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 免权限校验的路由列表(静态配置)
|
|
71
|
+
* 这些路由不检查菜单权限,但仍可能需要登录
|
|
72
|
+
*/
|
|
73
|
+
export const NO_PERMISSION_ROUTE_LIST: string[] = [
|
|
74
|
+
ROUTES.FORBIDDEN,
|
|
75
|
+
ROUTES.NOT_FOUND,
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 获取合并后的免权限校验路由列表
|
|
80
|
+
* 合并静态常量 + window.__MICO_CONFIG__.noPermissionRouteList
|
|
81
|
+
*/
|
|
82
|
+
export const getNoPermissionRouteList = (): string[] => {
|
|
83
|
+
const dynamicRoutes = window.__MICO_CONFIG__?.noPermissionRouteList ?? [];
|
|
84
|
+
return [...new Set([...NO_PERMISSION_ROUTE_LIST, ...dynamicRoutes])];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 判断指定路径是否为免权限校验路由(跳过菜单权限检查)
|
|
89
|
+
* 支持精确匹配和前缀匹配(以 /* 结尾的模式)
|
|
90
|
+
* @param pathname - 当前路由路径
|
|
91
|
+
* @returns 是否免权限校验
|
|
92
|
+
*/
|
|
93
|
+
export const isNoPermissionRoute = (pathname: string): boolean => {
|
|
94
|
+
return matchRouteList(pathname, getNoPermissionRouteList());
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 不显示布局的路由列表(静态配置)
|
|
99
|
+
* 注意:403/404 保留布局,方便用户通过导航返回
|
|
100
|
+
*/
|
|
101
|
+
export const NO_LAYOUT_ROUTE_LIST: string[] = [
|
|
102
|
+
ROUTES.LOGIN,
|
|
103
|
+
ROUTES.REGISTER,
|
|
104
|
+
ROUTES.REGISTER_RESULT,
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 获取合并后的不显示布局路由列表
|
|
109
|
+
* 合并静态常量 + window.__MICO_CONFIG__.noLayoutRouteList
|
|
110
|
+
*/
|
|
111
|
+
export const getNoLayoutRouteList = (): string[] => {
|
|
112
|
+
const dynamicRoutes = window.__MICO_CONFIG__?.noLayoutRouteList ?? [];
|
|
113
|
+
return [...new Set([...NO_LAYOUT_ROUTE_LIST, ...dynamicRoutes])];
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 判断指定路径是否不显示布局
|
|
118
|
+
* 支持精确匹配和前缀匹配(以 /* 结尾的模式)
|
|
119
|
+
* @param pathname - 当前路由路径
|
|
120
|
+
* @returns 是否不显示布局
|
|
121
|
+
*/
|
|
122
|
+
export const isNoLayoutRoute = (pathname: string): boolean => {
|
|
123
|
+
return matchRouteList(pathname, getNoLayoutRouteList());
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 判断是否关闭权限控制
|
|
128
|
+
* 关闭后:菜单全部显示,路由不校验权限
|
|
129
|
+
* @returns 是否关闭权限控制
|
|
130
|
+
*/
|
|
131
|
+
export const isAuthDisabled = (): boolean => {
|
|
132
|
+
return window.__MICO_CONFIG__?.disableAuth === true;
|
|
133
|
+
};
|
|
134
|
+
|
|
32
135
|
/**
|
|
33
136
|
* 主题相关常量
|
|
34
137
|
*/
|
|
35
138
|
export const THEME = {
|
|
36
139
|
/** localStorage 存储键 */
|
|
37
|
-
STORAGE_KEY: '
|
|
140
|
+
STORAGE_KEY: '<%= ProjectName %>-theme',
|
|
38
141
|
/** 默认主题 */
|
|
39
142
|
DEFAULT: 'light' as const,
|
|
40
143
|
/** 可选主题值 */
|
|
@@ -46,9 +149,9 @@ export const THEME = {
|
|
|
46
149
|
*/
|
|
47
150
|
export const TIMEZONE = {
|
|
48
151
|
/** localStorage 存储键(IANA 时区,如 Asia/Shanghai) */
|
|
49
|
-
STORAGE_KEY: '
|
|
152
|
+
STORAGE_KEY: '<%= ProjectName %>-timezone',
|
|
50
153
|
/** localStorage 存储键(用于展示的地区/名称,可选) */
|
|
51
|
-
REGION_STORAGE_KEY: '
|
|
154
|
+
REGION_STORAGE_KEY: '<%= ProjectName %>-timezone-region',
|
|
52
155
|
} as const;
|
|
53
156
|
|
|
54
157
|
/**
|
|
@@ -56,7 +159,7 @@ export const TIMEZONE = {
|
|
|
56
159
|
*/
|
|
57
160
|
export const PRESENCE = {
|
|
58
161
|
/** localStorage 存储键 */
|
|
59
|
-
STORAGE_KEY: '
|
|
162
|
+
STORAGE_KEY: '<%= ProjectName %>-presence-status',
|
|
60
163
|
} as const;
|
|
61
164
|
|
|
62
165
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useLocation, useModel } from '@umijs/max';
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { layoutLogger } from '@/common/logger';
|
|
4
|
-
import {
|
|
4
|
+
import { isNoAuthRoute } from '@/constants';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* 路由切换时自动刷新用户权限
|
|
@@ -39,7 +39,7 @@ export function useRoutePermissionRefresh() {
|
|
|
39
39
|
prevPathRef.current = location.pathname;
|
|
40
40
|
|
|
41
41
|
// 免认证路由不需要刷新
|
|
42
|
-
if (
|
|
42
|
+
if (isNoAuthRoute(location.pathname)) {
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
|
package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.tsx
CHANGED
|
@@ -57,7 +57,7 @@ const LayoutHeader: React.FC = () => {
|
|
|
57
57
|
{/* Logo */}
|
|
58
58
|
<div className="layout-header-logo">
|
|
59
59
|
<span className="logo-text">
|
|
60
|
-
{window.__MICO_MENUS__?.appName || '
|
|
60
|
+
{window.__MICO_MENUS__?.appName || '<%= projectName %>'}
|
|
61
61
|
</span>
|
|
62
62
|
</div>
|
|
63
63
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { ParsedMenuItem } from '@/common/menu';
|
|
2
2
|
import { filterMenuItems, getWindowMenus, parseMenuItems } from '@/common/menu';
|
|
3
|
+
import { isAuthDisabled, isNoPermissionRoute } from '@/constants';
|
|
3
4
|
import IconFont from '@/components/IconFont';
|
|
4
5
|
import { useMenuState } from '@/hooks/useMenuState';
|
|
5
6
|
import { useTheme } from '@/hooks/useTheme';
|
|
6
7
|
import { Layout, Menu } from '@arco-design/web-react';
|
|
7
8
|
import * as Icons from '@arco-design/web-react/icon';
|
|
8
|
-
import { useModel } from '@umijs/max';
|
|
9
|
+
import { useLocation, useModel } from '@umijs/max';
|
|
9
10
|
import React, { useEffect, useMemo, useRef } from 'react';
|
|
10
11
|
import './index.less';
|
|
11
12
|
|
|
@@ -103,19 +104,24 @@ interface LayoutMenuProps {
|
|
|
103
104
|
const LayoutMenu: React.FC<LayoutMenuProps> = () => {
|
|
104
105
|
const siderRef = useRef<HTMLDivElement>(null);
|
|
105
106
|
const { isDark } = useTheme();
|
|
107
|
+
const location = useLocation();
|
|
106
108
|
|
|
107
109
|
const { initialState } = useModel('@@initialState');
|
|
108
110
|
const currentUser = initialState?.currentUser;
|
|
109
111
|
|
|
110
112
|
// Parse menu data
|
|
113
|
+
// disableAuth 或免权限校验路由时不过滤,显示全部菜单
|
|
111
114
|
const menuItems = useMemo(() => {
|
|
112
115
|
const menus = getWindowMenus();
|
|
116
|
+
if (isAuthDisabled() || isNoPermissionRoute(location.pathname)) {
|
|
117
|
+
return parseMenuItems(menus);
|
|
118
|
+
}
|
|
113
119
|
const filteredMenus = filterMenuItems(menus, {
|
|
114
120
|
isSuperuser: currentUser?.is_superuser,
|
|
115
121
|
sideMenus: (currentUser?.side_menus || []) as string[],
|
|
116
122
|
});
|
|
117
123
|
return parseMenuItems(filteredMenus);
|
|
118
|
-
}, [currentUser?.is_superuser, currentUser?.side_menus]);
|
|
124
|
+
}, [currentUser?.is_superuser, currentUser?.side_menus, location.pathname]);
|
|
119
125
|
|
|
120
126
|
// 使用菜单状态 Hook
|
|
121
127
|
const {
|
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
import { getAppNameFromEntry } from '@/common/micro';
|
|
9
9
|
import AppTabs from '@/components/AppTabs';
|
|
10
10
|
import MicroAppLoader from '@/components/MicroAppLoader';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
isAuthDisabled,
|
|
13
|
+
isNoLayoutRoute,
|
|
14
|
+
isNoPermissionRoute,
|
|
15
|
+
} from '@/constants';
|
|
12
16
|
import { useRoutePermissionRefresh } from '@/hooks/useRoutePermissionRefresh';
|
|
13
17
|
import ForbiddenPage from '@/pages/403';
|
|
14
18
|
import { Layout, Spin } from '@arco-design/web-react';
|
|
@@ -48,20 +52,27 @@ const BasicLayout: React.FC = () => {
|
|
|
48
52
|
return extractRoutes(menus);
|
|
49
53
|
}, []);
|
|
50
54
|
|
|
51
|
-
//
|
|
55
|
+
// 有权限的路由(disableAuth 时不过滤,显示全部)
|
|
52
56
|
const allowedRoutes = useMemo(() => {
|
|
57
|
+
if (isAuthDisabled()) {
|
|
58
|
+
return allRoutes;
|
|
59
|
+
}
|
|
53
60
|
const menus = getWindowMenus();
|
|
54
61
|
const filteredMenus = filterMenuItems(menus, filterOptions);
|
|
55
62
|
return extractRoutes(filteredMenus);
|
|
56
|
-
}, [filterOptions]);
|
|
63
|
+
}, [filterOptions, allRoutes]);
|
|
57
64
|
|
|
58
65
|
// 查找当前路由配置(优先从有权限的路由中查找)
|
|
59
66
|
const currentRoute = useMemo(() => {
|
|
60
67
|
return findRouteByPath(allowedRoutes, location.pathname);
|
|
61
68
|
}, [allowedRoutes, location.pathname]);
|
|
62
69
|
|
|
63
|
-
//
|
|
70
|
+
// 判断是否是动态路由但无权限(disableAuth 时始终返回 false)
|
|
64
71
|
const isForbidden = useMemo(() => {
|
|
72
|
+
// 关闭权限控制时,不校验权限
|
|
73
|
+
if (isAuthDisabled()) return false;
|
|
74
|
+
// 免权限校验路由,不检查菜单权限
|
|
75
|
+
if (isNoPermissionRoute(location.pathname)) return false;
|
|
65
76
|
// 如果在有权限的路由中找到了,说明有权限
|
|
66
77
|
if (currentRoute) return false;
|
|
67
78
|
// 如果在所有路由中也找不到,说明不是动态路由,交给 Umi 处理
|
|
@@ -83,7 +94,7 @@ const BasicLayout: React.FC = () => {
|
|
|
83
94
|
}, [currentRoute, allRoutes, allowedRoutes, location.pathname, filterOptions]);
|
|
84
95
|
|
|
85
96
|
// 判断是否需要显示布局
|
|
86
|
-
const showLayout = !
|
|
97
|
+
const showLayout = !isNoLayoutRoute(location.pathname);
|
|
87
98
|
|
|
88
99
|
// 渲染页面内容
|
|
89
100
|
const renderContent = () => {
|
|
@@ -17,6 +17,7 @@ export interface IUserInfoResponse {
|
|
|
17
17
|
export async function fetchUserInfo(): Promise<IUserInfo> {
|
|
18
18
|
const response = await request<IUserInfoResponse>(USER_INFO_API, {
|
|
19
19
|
method: 'GET',
|
|
20
|
+
skipProxy: true,
|
|
20
21
|
});
|
|
21
22
|
if (response.code === 200 && response.data) {
|
|
22
23
|
return response.data;
|
|
@@ -177,12 +177,21 @@ module.exports = class extends Generator {
|
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
install() {
|
|
181
|
+
this.log('');
|
|
182
|
+
this.log('📦 正在安装依赖...');
|
|
183
|
+
this.spawnCommandSync('pnpm', ['install'], {
|
|
184
|
+
cwd: this.monorepoRoot
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
180
188
|
end() {
|
|
181
189
|
this.log('');
|
|
182
190
|
this.log('✅ 子应用创建成功!');
|
|
183
191
|
this.log('');
|
|
192
|
+
this.log(' 后续步骤:');
|
|
193
|
+
this.log('');
|
|
184
194
|
this.log(` cd apps/${this.appName}`);
|
|
185
|
-
this.log(' pnpm install');
|
|
186
195
|
this.log(' pnpm dev');
|
|
187
196
|
this.log('');
|
|
188
197
|
}
|