generator-mico-cli 0.1.29 → 0.2.2
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/templates/apps/layout/config/config.dev.ts +2 -1
- package/generators/micro-react/templates/apps/layout/config/routes.ts +5 -0
- package/generators/micro-react/templates/apps/layout/docs/arch-/350/257/267/346/261/202/346/250/241/345/235/227.md +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 +175 -0
- package/generators/micro-react/templates/apps/layout/src/app.tsx +89 -17
- package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +106 -1
- package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +2 -0
- package/generators/micro-react/templates/apps/layout/src/common/request/url-resolver.ts +23 -8
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/container-manager.ts +152 -20
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +100 -20
- package/generators/micro-react/templates/apps/layout/src/global.less +5 -1
- package/generators/micro-react/templates/apps/layout/src/hooks/useRoutePermissionRefresh.ts +69 -0
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +11 -3
- package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +62 -8
- package/generators/micro-react/templates/apps/layout/src/pages/403/index.tsx +28 -0
- package/generators/micro-react/templates/apps/layout/src/pages/User/Login/index.tsx +6 -2
- package/generators/micro-react/templates/package.json +5 -0
- package/generators/subapp-react/templates/homepage/config/config.prod.development.ts +0 -4
- package/generators/subapp-react/templates/homepage/config/config.prod.testing.ts +0 -4
- package/generators/subapp-react/templates/homepage/config/config.prod.ts +2 -2
- package/generators/subapp-react/templates/homepage/config/config.ts +6 -0
- package/package.json +1 -1
|
@@ -23,6 +23,7 @@ const config: ReturnType<typeof defineConfig> = {
|
|
|
23
23
|
window.__MICO_CONFIG__ = {
|
|
24
24
|
appName: 'Mico Center',
|
|
25
25
|
apiBaseUrl: '',
|
|
26
|
+
defaultPath: '',
|
|
26
27
|
};
|
|
27
28
|
`,
|
|
28
29
|
},
|
|
@@ -47,7 +48,7 @@ const config: ReturnType<typeof defineConfig> = {
|
|
|
47
48
|
define: {
|
|
48
49
|
'process.env.NODE_ENV': 'development',
|
|
49
50
|
'process.env.APP_ID': 'mibot_dev',
|
|
50
|
-
'process.env.API_BASE_URL': '
|
|
51
|
+
'process.env.API_BASE_URL': '',
|
|
51
52
|
'process.env.PROXY_SUFFIX': '/',
|
|
52
53
|
'process.env.LOGIN_ENDPOINT':
|
|
53
54
|
'',
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# 菜单权限控制
|
|
2
|
+
|
|
3
|
+
> 创建时间:2026-01-24
|
|
4
|
+
|
|
5
|
+
## 功能概述
|
|
6
|
+
|
|
7
|
+
基于用户信息中的 `side_menus` 字段实现菜单和路由的白名单权限控制。非超级用户只能看到和访问 `side_menus` 中配置的菜单项,访问无权限路由时显示 403 页面。
|
|
8
|
+
|
|
9
|
+
## 技术方案
|
|
10
|
+
|
|
11
|
+
### 技术栈
|
|
12
|
+
|
|
13
|
+
- 框架:React 18 + @umijs/max
|
|
14
|
+
- UI 组件:Arco Design (Result 组件)
|
|
15
|
+
- 状态管理:Umi initialState
|
|
16
|
+
|
|
17
|
+
### 核心实现
|
|
18
|
+
|
|
19
|
+
1. 用户信息接口返回 `side_menus` 字段(白名单)
|
|
20
|
+
2. `filterMenuItems` 根据白名单过滤菜单项
|
|
21
|
+
3. Layout 组件对比"所有路由"和"有权限路由"判断 403
|
|
22
|
+
4. 无权限时在 Layout 内直接渲染 403 组件(无跳转)
|
|
23
|
+
|
|
24
|
+
### 权限判断逻辑
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
用户访问 /some-path
|
|
28
|
+
↓
|
|
29
|
+
是否是超级用户?
|
|
30
|
+
├── 是 → 允许访问所有菜单
|
|
31
|
+
└── 否 → 检查 side_menus 白名单
|
|
32
|
+
├── 精确匹配:menuPath === side_menus[i]
|
|
33
|
+
├── 前缀匹配:side_menus[i].startsWith(menuPath + '.')
|
|
34
|
+
└── 都不匹配 → 禁止访问,显示 403
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 文件清单
|
|
38
|
+
|
|
39
|
+
### 新增文件
|
|
40
|
+
|
|
41
|
+
| 文件路径 | 说明 |
|
|
42
|
+
|----------|------|
|
|
43
|
+
| `src/pages/403/index.tsx` | 403 无权限页面组件 |
|
|
44
|
+
|
|
45
|
+
### 修改文件
|
|
46
|
+
|
|
47
|
+
| 文件路径 | 修改内容 |
|
|
48
|
+
|----------|----------|
|
|
49
|
+
| `src/common/menu/parser.ts` | 新增 `filterMenuItems`、`isMenuAllowed` 等权限过滤函数 |
|
|
50
|
+
| `src/layouts/index.tsx` | 新增 `isForbidden` 判断和 403 渲染逻辑 |
|
|
51
|
+
| `src/layouts/components/menu/index.tsx` | 集成菜单过滤 |
|
|
52
|
+
| `config/routes.ts` | 新增 `/403` 路由 |
|
|
53
|
+
|
|
54
|
+
## API / 组件接口
|
|
55
|
+
|
|
56
|
+
### MenuFilterOptions
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
interface MenuFilterOptions {
|
|
60
|
+
/** 是否是超级用户 */
|
|
61
|
+
isSuperuser?: boolean | number;
|
|
62
|
+
/** 允许访问的菜单路径列表(白名单) */
|
|
63
|
+
sideMenus?: string[];
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### filterMenuItems
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
/**
|
|
71
|
+
* 根据权限过滤菜单项(白名单逻辑)
|
|
72
|
+
*/
|
|
73
|
+
function filterMenuItems(
|
|
74
|
+
items: MenuItem[],
|
|
75
|
+
options?: MenuFilterOptions,
|
|
76
|
+
parentPath?: string,
|
|
77
|
+
): MenuItem[];
|
|
78
|
+
```
|
|
79
|
+
|
|
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
|
+
### 用户信息相关字段
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
interface IUserInfo {
|
|
104
|
+
is_superuser: boolean | number;
|
|
105
|
+
/** 允许访问的菜单路径列表 */
|
|
106
|
+
side_menus: string[];
|
|
107
|
+
/** 缺失的操作权限(用于按钮级别控制,非菜单) */
|
|
108
|
+
miss_permissions: string[];
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## 使用示例
|
|
113
|
+
|
|
114
|
+
### 用户信息接口返回
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"is_superuser": false,
|
|
119
|
+
"side_menus": ["列队管理.配置队列", "工作台"],
|
|
120
|
+
"miss_permissions": ["列队管理.配置队列.查看"]
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 菜单过滤结果
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
原始菜单:
|
|
128
|
+
├── 工作台 ✅ 精确匹配 "工作台"
|
|
129
|
+
├── 列队管理 ✅ 前缀匹配 "列队管理.配置队列"
|
|
130
|
+
│ └── 配置队列 ✅ 精确匹配 "列队管理.配置队列"
|
|
131
|
+
├── 质量管理 ❌ 不在白名单
|
|
132
|
+
│ └── 抽样检查 ❌ 不在白名单
|
|
133
|
+
└── 权限管理 ❌ 硬编码禁止(非超级用户)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Layout 中的权限判断
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
// layouts/index.tsx
|
|
140
|
+
const isForbidden = useMemo(() => {
|
|
141
|
+
if (currentRoute) return false; // 在有权限的路由中找到了
|
|
142
|
+
const routeInAll = findRouteByPath(allRoutes, location.pathname);
|
|
143
|
+
return !!routeInAll; // 在所有路由中存在但无权限
|
|
144
|
+
}, [currentRoute, allRoutes, location.pathname]);
|
|
145
|
+
|
|
146
|
+
const renderContent = () => {
|
|
147
|
+
if (isForbidden) {
|
|
148
|
+
return <ForbiddenPage />;
|
|
149
|
+
}
|
|
150
|
+
// ... 正常渲染
|
|
151
|
+
};
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## 设计决策
|
|
155
|
+
|
|
156
|
+
| 决策点 | 选择 | 理由 |
|
|
157
|
+
|--------|------|------|
|
|
158
|
+
| 权限模型 | 白名单 (`side_menus`) | 后端返回的 `side_menus` 是允许列表,比黑名单更安全 |
|
|
159
|
+
| 403 处理 | 原地渲染组件 | 保持 URL 不变,用户体验更好,便于分享链接 |
|
|
160
|
+
| 父级菜单显示 | 前缀匹配 | 子菜单有权限时,父级菜单需要作为容器显示 |
|
|
161
|
+
| 超级用户 | 跳过所有检查 | 管理员需要完整访问权限 |
|
|
162
|
+
| 权限管理菜单 | 硬编码禁止 | 非超级用户不应看到权限管理入口 |
|
|
163
|
+
|
|
164
|
+
## 注意事项
|
|
165
|
+
|
|
166
|
+
- `side_menus` 为空时,非超级用户没有任何菜单权限
|
|
167
|
+
- `side_menus` 格式为菜单路径,如 `"列队管理.配置队列"`
|
|
168
|
+
- `miss_permissions` 用于按钮级别权限控制,不影响菜单显示
|
|
169
|
+
- 403 页面在 Layout 内渲染,不会触发路由跳转
|
|
170
|
+
- 调试时可在控制台搜索 `isForbidden check` 查看权限判断日志
|
|
171
|
+
|
|
172
|
+
## 相关文档
|
|
173
|
+
|
|
174
|
+
- [微前端模式](./feature-微前端模式.md) - 路由和菜单解析
|
|
175
|
+
- [用户信息接口](./feature-用户信息接口.md) - 用户数据结构
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { history, type RequestConfig } from '@umijs/max';
|
|
2
|
-
import { prefetchApps } from 'qiankun';
|
|
1
|
+
import { history, Navigate, type RequestConfig } from '@umijs/max';
|
|
2
|
+
import { addGlobalUncaughtErrorHandler, prefetchApps } from 'qiankun';
|
|
3
3
|
import { errorConfig } from './requestErrorConfig';
|
|
4
4
|
// 解决「React19 中无法使用 Message/Notification」的问题。 @see https://github.com/arco-design/arco-design/issues/2900#issuecomment-2796571653
|
|
5
5
|
import * as arco from '@arco-design/web-react';
|
|
6
|
-
import '@arco-design/web-react/dist/css/arco.css';
|
|
7
|
-
import '@arco-design/web-react/es/_util/react-19-adapter';
|
|
8
6
|
import React from 'react';
|
|
9
7
|
import ReactDOM from 'react-dom/client';
|
|
10
8
|
|
|
@@ -23,6 +21,57 @@ import MicroAppLoader from './components/MicroAppLoader';
|
|
|
23
21
|
import { NO_AUTH_ROUTE_LIST } from './constants';
|
|
24
22
|
import './global.less';
|
|
25
23
|
|
|
24
|
+
// ==================== qiankun 全局错误处理 ====================
|
|
25
|
+
// 捕获子应用运行时未捕获的异常,防止页面崩溃
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 注册 qiankun 全局未捕获错误处理器
|
|
29
|
+
* 处理以下场景:
|
|
30
|
+
* 1. 子应用 JS 运行时错误
|
|
31
|
+
* 2. 子应用生命周期钩子抛出的异常
|
|
32
|
+
* 3. 子应用资源加载失败
|
|
33
|
+
*/
|
|
34
|
+
if (typeof window !== 'undefined') {
|
|
35
|
+
addGlobalUncaughtErrorHandler((event: Event | string) => {
|
|
36
|
+
// 提取错误信息
|
|
37
|
+
const error =
|
|
38
|
+
event instanceof ErrorEvent
|
|
39
|
+
? event.error
|
|
40
|
+
: event instanceof PromiseRejectionEvent
|
|
41
|
+
? event.reason
|
|
42
|
+
: event;
|
|
43
|
+
|
|
44
|
+
const errorMessage =
|
|
45
|
+
error instanceof Error
|
|
46
|
+
? error.message
|
|
47
|
+
: typeof error === 'string'
|
|
48
|
+
? error
|
|
49
|
+
: 'Unknown micro-app error';
|
|
50
|
+
|
|
51
|
+
console.error('[qiankun] Global uncaught error:', {
|
|
52
|
+
error,
|
|
53
|
+
message: errorMessage,
|
|
54
|
+
type: event instanceof Event ? event.type : 'unknown',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// 检查是否是子应用加载失败(资源 404 等)
|
|
58
|
+
const isLoadError =
|
|
59
|
+
errorMessage.includes('Failed to fetch') ||
|
|
60
|
+
errorMessage.includes('Loading chunk') ||
|
|
61
|
+
errorMessage.includes('load') ||
|
|
62
|
+
errorMessage.includes('Script error');
|
|
63
|
+
|
|
64
|
+
if (isLoadError) {
|
|
65
|
+
console.error(
|
|
66
|
+
'[qiankun] Micro-app resource loading failed. Please check if the micro-app server is running.',
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 注意:这里不阻止错误冒泡,让控制台仍能显示原始错误
|
|
71
|
+
// 如果需要阻止,可以 return true
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
26
75
|
// ==================== 微应用预加载 ====================
|
|
27
76
|
// 预加载所有微应用资源,避免快速切换时的竞态条件
|
|
28
77
|
// 当资源已预加载时,loadMicroApp 的异步加载会快速完成,减少容器不存在的错误
|
|
@@ -124,21 +173,22 @@ export async function getInitialState(): Promise<{
|
|
|
124
173
|
}
|
|
125
174
|
};
|
|
126
175
|
|
|
127
|
-
// 如果不是登录页面,执行
|
|
128
176
|
const { location } = history;
|
|
129
|
-
|
|
130
|
-
|
|
177
|
+
const isNoAuthRoute = NO_AUTH_ROUTE_LIST.includes(location.pathname);
|
|
178
|
+
|
|
179
|
+
// 非免认证路由:走 SSO 流程
|
|
180
|
+
if (!isNoAuthRoute) {
|
|
131
181
|
await ensureSsoSession();
|
|
182
|
+
}
|
|
132
183
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
184
|
+
// 有 token 就获取用户信息(无论在哪个页面,支持登录后 refresh 场景)
|
|
185
|
+
if (getStoredAuthToken()) {
|
|
186
|
+
const userInfo = await fetchUserInfoFn();
|
|
187
|
+
if (userInfo) {
|
|
188
|
+
return {
|
|
189
|
+
fetchUserInfo: fetchUserInfoFn,
|
|
190
|
+
currentUser: userInfo,
|
|
191
|
+
};
|
|
142
192
|
}
|
|
143
193
|
}
|
|
144
194
|
|
|
@@ -147,6 +197,7 @@ export async function getInitialState(): Promise<{
|
|
|
147
197
|
};
|
|
148
198
|
}
|
|
149
199
|
|
|
200
|
+
|
|
150
201
|
/**
|
|
151
202
|
* @name request 配置,可以配置错误处理
|
|
152
203
|
* 它基于 axios 和 ahooks 的 useRequest 提供了一套统一的网络请求和错误处理方案。
|
|
@@ -179,7 +230,11 @@ export function patchClientRoutes({ routes }: { routes: UmiRoute[] }) {
|
|
|
179
230
|
const menus = getWindowMenus();
|
|
180
231
|
const dynamicRoutes = extractRoutes(menus);
|
|
181
232
|
|
|
182
|
-
console.log('[app.tsx] patchClientRoutes:', {
|
|
233
|
+
console.log('[app.tsx] patchClientRoutes:', {
|
|
234
|
+
menus,
|
|
235
|
+
dynamicRoutes,
|
|
236
|
+
routes,
|
|
237
|
+
});
|
|
183
238
|
|
|
184
239
|
// 找到根路由(全局布局)
|
|
185
240
|
const rootRoute = routes.find((r) => r.path === '/');
|
|
@@ -222,6 +277,23 @@ export function patchClientRoutes({ routes }: { routes: UmiRoute[] }) {
|
|
|
222
277
|
});
|
|
223
278
|
}
|
|
224
279
|
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @name 路由变化处理
|
|
283
|
+
* @description 处理 defaultPath 重定向:访问 "/" 时自动跳转到配置的默认路径
|
|
284
|
+
* @doc https://umijs.org/docs/api/runtime-config#onroutechange
|
|
285
|
+
*/
|
|
286
|
+
export function onRouteChange({
|
|
287
|
+
location,
|
|
288
|
+
}: {
|
|
289
|
+
location: { pathname: string };
|
|
290
|
+
}) {
|
|
291
|
+
const defaultPath = window.__MICO_CONFIG__?.defaultPath;
|
|
292
|
+
if (location.pathname === '/' && defaultPath && defaultPath !== '/') {
|
|
293
|
+
history.replace(defaultPath);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
225
297
|
/**
|
|
226
298
|
* @name qiankun 微前端子应用生命周期
|
|
227
299
|
* @description 当应用作为 qiankun 子应用运行时,这些生命周期会被调用
|
|
@@ -9,6 +9,108 @@ export const getWindowMenus = (): MenuItem[] => {
|
|
|
9
9
|
return Array.isArray(menus) ? menus : [];
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
export interface MenuFilterOptions {
|
|
13
|
+
/** 是否是超级用户 */
|
|
14
|
+
isSuperuser?: boolean | number;
|
|
15
|
+
/** 允许访问的菜单路径列表(白名单) */
|
|
16
|
+
sideMenus?: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const isSuperuserUser = (value?: boolean | number): boolean => {
|
|
20
|
+
return value === true || value === 1;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const buildMenuPath = (parentPath: string, name: string): string => {
|
|
24
|
+
return parentPath ? `${parentPath}.${name}` : name;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 检查菜单路径是否允许访问(白名单逻辑)
|
|
29
|
+
* - 超级用户可以访问所有菜单
|
|
30
|
+
* - 非超级用户不能访问"权限管理"
|
|
31
|
+
* - 菜单路径在 sideMenus 中,或是 sideMenus 中某项的前缀(父级菜单)
|
|
32
|
+
*/
|
|
33
|
+
const isMenuAllowed = (
|
|
34
|
+
menuPath: string,
|
|
35
|
+
options: MenuFilterOptions,
|
|
36
|
+
): boolean => {
|
|
37
|
+
// 超级用户可以访问所有菜单
|
|
38
|
+
if (isSuperuserUser(options.isSuperuser)) return true;
|
|
39
|
+
|
|
40
|
+
// 非超级用户不能访问"权限管理"
|
|
41
|
+
if (menuPath === '权限管理') return false;
|
|
42
|
+
|
|
43
|
+
const sideMenus = options.sideMenus || [];
|
|
44
|
+
|
|
45
|
+
// 如果没有配置 sideMenus,非超级用户没有任何菜单权限
|
|
46
|
+
if (sideMenus.length === 0) return false;
|
|
47
|
+
|
|
48
|
+
// 检查是否在白名单中(精确匹配或前缀匹配)
|
|
49
|
+
return sideMenus.some((allowedPath) => {
|
|
50
|
+
// 精确匹配:菜单路径完全等于白名单中的路径
|
|
51
|
+
if (menuPath === allowedPath) return true;
|
|
52
|
+
// 前缀匹配:白名单路径以菜单路径开头(说明菜单是父级)
|
|
53
|
+
if (allowedPath.startsWith(menuPath + '.')) return true;
|
|
54
|
+
return false;
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const isRouteAllowed = (
|
|
59
|
+
menuPath: string,
|
|
60
|
+
options: MenuFilterOptions = {},
|
|
61
|
+
): boolean => {
|
|
62
|
+
return isMenuAllowed(menuPath, options);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 根据权限过滤菜单项(白名单逻辑)
|
|
67
|
+
*/
|
|
68
|
+
export const filterMenuItems = (
|
|
69
|
+
items: MenuItem[],
|
|
70
|
+
options: MenuFilterOptions = {},
|
|
71
|
+
parentPath = '',
|
|
72
|
+
): MenuItem[] => {
|
|
73
|
+
return items
|
|
74
|
+
.filter((item) => item.enabled)
|
|
75
|
+
.map((item) => {
|
|
76
|
+
const menuPath = buildMenuPath(parentPath, item.name);
|
|
77
|
+
const isAllowed = isMenuAllowed(menuPath, options);
|
|
78
|
+
|
|
79
|
+
// 递归处理子菜单
|
|
80
|
+
const nextChildren = item.children?.length
|
|
81
|
+
? filterMenuItems(item.children, options, menuPath)
|
|
82
|
+
: [];
|
|
83
|
+
|
|
84
|
+
// 分组类型:如果没有子菜单,不显示
|
|
85
|
+
if (item.type === 'group' && nextChildren.length === 0) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 当前菜单不允许访问
|
|
90
|
+
if (!isAllowed) {
|
|
91
|
+
// 但如果有允许的子菜单,仍需显示当前菜单作为容器
|
|
92
|
+
if (nextChildren.length > 0) {
|
|
93
|
+
return { ...item, children: nextChildren };
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 当前菜单允许访问
|
|
99
|
+
const hasChildren = (item.children?.length || 0) > 0;
|
|
100
|
+
if (!hasChildren) {
|
|
101
|
+
return item;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 有子菜单但过滤后为空,不显示
|
|
105
|
+
if (nextChildren.length === 0) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { ...item, children: nextChildren };
|
|
110
|
+
})
|
|
111
|
+
.filter((item): item is MenuItem => Boolean(item));
|
|
112
|
+
};
|
|
113
|
+
|
|
12
114
|
/**
|
|
13
115
|
* 判断页面的加载类型
|
|
14
116
|
* 有 htmlUrl 或 jsUrls 时使用 qiankun 微应用加载
|
|
@@ -40,10 +142,13 @@ const getEntry = (page: MenuItem['page']): string | undefined => {
|
|
|
40
142
|
export const extractRoutes = (
|
|
41
143
|
items: MenuItem[],
|
|
42
144
|
routes: ParsedRoute[] = [],
|
|
145
|
+
parentPath = '',
|
|
43
146
|
): ParsedRoute[] => {
|
|
44
147
|
for (const item of items) {
|
|
45
148
|
if (!item.enabled) continue;
|
|
46
149
|
|
|
150
|
+
const menuPath = buildMenuPath(parentPath, item.name);
|
|
151
|
+
|
|
47
152
|
if (item.type === 'page' && item.page && item.page.enabled) {
|
|
48
153
|
const loadType = getLoadType(item.page);
|
|
49
154
|
routes.push({
|
|
@@ -57,7 +162,7 @@ export const extractRoutes = (
|
|
|
57
162
|
}
|
|
58
163
|
|
|
59
164
|
if (item.children && item.children.length > 0) {
|
|
60
|
-
extractRoutes(item.children, routes);
|
|
165
|
+
extractRoutes(item.children, routes, menuPath);
|
|
61
166
|
}
|
|
62
167
|
}
|
|
63
168
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* URL 解析与拼接工具
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { resolveProxySuffix } from './config';
|
|
5
|
+
import { resolveApiBaseUrl, resolveProxySuffix } from './config';
|
|
6
6
|
import type { UnifiedRequestOptions } from './types';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -33,8 +33,12 @@ const joinProxyAndPath = (proxySuffix: string, path: string): string => {
|
|
|
33
33
|
* 配置优先级:
|
|
34
34
|
* 1. rawUrl: true → 直接返回原始 url
|
|
35
35
|
* 2. 绝对 URL → 直接返回
|
|
36
|
-
* 3. skipProxy: true →
|
|
37
|
-
* 4. 其他情况 → 拼接 proxySuffix + url
|
|
36
|
+
* 3. skipProxy: true → 拼接 apiBaseUrl + url(跳过 proxySuffix)
|
|
37
|
+
* 4. 其他情况 → 拼接 apiBaseUrl + proxySuffix + url
|
|
38
|
+
*
|
|
39
|
+
* 示例(testing 环境,apiBaseUrl = https://dashboard-api-test.micoplatform.com):
|
|
40
|
+
* - 默认: /api/user/info → https://dashboard-api-test.micoplatform.com/proxy/audit_svr/api/user/info
|
|
41
|
+
* - skipProxy: /api/user/info → https://dashboard-api-test.micoplatform.com/api/user/info
|
|
38
42
|
*/
|
|
39
43
|
export const resolveRequestUrl = (
|
|
40
44
|
url: string,
|
|
@@ -50,13 +54,24 @@ export const resolveRequestUrl = (
|
|
|
50
54
|
return url;
|
|
51
55
|
}
|
|
52
56
|
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
// 拼接 apiBaseUrl + proxySuffix + url
|
|
58
|
+
const apiBaseUrl = resolveApiBaseUrl();
|
|
59
|
+
// skipProxy: true 时不拼接 proxySuffix
|
|
60
|
+
const proxySuffix = options?.skipProxy
|
|
61
|
+
? ''
|
|
62
|
+
: resolveProxySuffix(options?.proxySuffix);
|
|
63
|
+
|
|
64
|
+
if (apiBaseUrl) {
|
|
65
|
+
// 有 apiBaseUrl 时,拼接完整的绝对路径
|
|
66
|
+
const pathPart = proxySuffix ? joinProxyAndPath(proxySuffix, url) : url;
|
|
67
|
+
const normalizedBase = apiBaseUrl.endsWith('/')
|
|
68
|
+
? apiBaseUrl.slice(0, -1)
|
|
69
|
+
: apiBaseUrl;
|
|
70
|
+
const normalizedPath = pathPart.startsWith('/') ? pathPart : `/${pathPart}`;
|
|
71
|
+
return `${normalizedBase}${normalizedPath}`;
|
|
56
72
|
}
|
|
57
73
|
|
|
58
|
-
//
|
|
59
|
-
const proxySuffix = resolveProxySuffix(options?.proxySuffix);
|
|
74
|
+
// 没有 apiBaseUrl 时,仅拼接 proxySuffix(相对路径)
|
|
60
75
|
if (proxySuffix) {
|
|
61
76
|
return joinProxyAndPath(proxySuffix, url);
|
|
62
77
|
}
|