generator-mico-cli 0.1.28 → 0.2.1
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/.cursor/rules/layout-app.mdc +1 -1
- package/generators/micro-react/templates/.cursor/rules/request-auth.mdc +2 -2
- package/generators/micro-react/templates/.cursor/rules/theme-system.mdc +2 -2
- package/generators/micro-react/templates/CLAUDE.md +27 -11
- package/generators/micro-react/templates/README.md +2 -0
- package/generators/micro-react/templates/apps/layout/config/config.dev.ts +11 -4
- package/generators/micro-react/templates/apps/layout/config/config.prod.development.ts +1 -11
- package/generators/micro-react/templates/apps/layout/config/config.prod.testing.ts +0 -7
- package/generators/micro-react/templates/apps/layout/config/routes.ts +10 -0
- package/generators/micro-react/templates/apps/layout/docs/arch-/346/227/245/345/277/227/344/270/216/345/270/270/351/207/217.md +105 -0
- package/generators/micro-react/templates/apps/layout/docs/arch-/350/257/267/346/261/202/346/250/241/345/235/227.md +17 -15
- package/generators/micro-react/templates/apps/layout/docs/feature-/344/270/273/351/242/230/350/211/262/345/210/207/346/215/242.md +234 -0
- 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 +432 -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 +175 -0
- package/generators/micro-react/templates/apps/layout/mock/api.mock.ts +15 -15
- package/generators/micro-react/templates/apps/layout/src/app.tsx +43 -28
- package/generators/micro-react/templates/apps/layout/src/common/auth/{cs-auth-manager.ts → auth-manager.ts} +6 -63
- package/generators/micro-react/templates/apps/layout/src/common/auth/index.ts +1 -1
- package/generators/micro-react/templates/apps/layout/src/common/auth/tool.ts +2 -3
- package/generators/micro-react/templates/apps/layout/src/common/auth/type.ts +38 -0
- package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +109 -2
- package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +74 -1
- package/generators/micro-react/templates/apps/layout/src/common/micro/index.ts +0 -8
- package/generators/micro-react/templates/apps/layout/src/common/micro/types.ts +0 -2
- package/generators/micro-react/templates/apps/layout/src/common/request/config.ts +1 -1
- package/generators/micro-react/templates/apps/layout/src/common/request/index.ts +3 -11
- package/generators/micro-react/templates/apps/layout/src/common/request/interceptors.ts +2 -2
- package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +3 -2
- package/generators/micro-react/templates/apps/layout/src/common/request/token-refresh.ts +2 -2
- package/generators/micro-react/templates/apps/layout/src/common/request/url-resolver.ts +32 -48
- package/generators/micro-react/templates/apps/layout/src/components/AppTabs/index.less +1 -1
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +62 -14
- package/generators/micro-react/templates/apps/layout/src/global.less +5 -1
- package/generators/micro-react/templates/apps/layout/src/hooks/useAuth.ts +2 -3
- package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.less +1 -1
- package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.tsx +17 -13
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.less +1 -1
- 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.less +1 -1
- package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +52 -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.less +275 -0
- package/generators/micro-react/templates/apps/layout/src/pages/User/Login/index.tsx +142 -0
- package/generators/micro-react/templates/apps/layout/src/services/auth.ts +1 -0
- package/generators/micro-react/templates/apps/layout/src/services/user.ts +25 -0
- package/generators/micro-react/templates/packages/shared-styles/README.md +16 -14
- package/generators/micro-react/templates/packages/shared-styles/arco-design-mobile-override.less +91 -0
- package/generators/micro-react/templates/packages/shared-styles/arco-override.less +41 -0
- package/generators/micro-react/templates/packages/shared-styles/index.d.ts +44 -0
- package/generators/micro-react/templates/packages/shared-styles/index.less +0 -1
- package/generators/micro-react/templates/packages/shared-styles/package.json +6 -3
- package/generators/micro-react/templates/packages/shared-styles/themes/dark/custom-var.less +118 -74
- package/generators/micro-react/templates/packages/shared-styles/themes/normal/custom-var.less +175 -101
- package/generators/micro-react/templates/packages/shared-styles/variables-only.less +357 -225
- package/generators/micro-react/templates/packages/shared-styles/variables.less +290 -201
- package/generators/subapp-react/templates/homepage/config/config.prod.ts +2 -2
- package/generators/subapp-react/templates/homepage/config/config.ts +6 -0
- package/generators/subapp-react/templates/homepage/mock/api.mock.ts +43 -43
- package/generators/subapp-react/templates/homepage/typings.d.ts +76 -0
- package/package.json +1 -1
- package/generators/micro-react/templates/apps/layout/src/styles/arco-override.less +0 -78
- package/generators/micro-react/templates/apps/layout/src/styles/themes/dark/custom-var.less +0 -244
- package/generators/micro-react/templates/apps/layout/src/styles/themes/normal/custom-var.less +0 -195
- package/generators/micro-react/templates/apps/layout/src/styles/variables.less +0 -5
|
@@ -2,40 +2,24 @@
|
|
|
2
2
|
* URL 解析与拼接工具
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
resolveProxySuffix as getProxySuffix,
|
|
7
|
-
resolveApiBaseUrl,
|
|
8
|
-
} from './config';
|
|
5
|
+
import { resolveApiBaseUrl, resolveProxySuffix } from './config';
|
|
9
6
|
import type { UnifiedRequestOptions } from './types';
|
|
10
7
|
|
|
11
8
|
/**
|
|
12
|
-
*
|
|
9
|
+
* 判断是否为绝对 URL
|
|
13
10
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
const isAbsoluteUrl = (url: string): boolean => {
|
|
12
|
+
return (
|
|
16
13
|
url.startsWith('http://') ||
|
|
17
14
|
url.startsWith('https://') ||
|
|
18
15
|
url.startsWith('//')
|
|
19
|
-
)
|
|
20
|
-
return url;
|
|
21
|
-
}
|
|
22
|
-
const normalizedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
23
|
-
const normalizedPath = url.startsWith('/') ? url : `/${url}`;
|
|
24
|
-
return `${normalizedBase}${normalizedPath}`;
|
|
16
|
+
);
|
|
25
17
|
};
|
|
26
18
|
|
|
27
19
|
/**
|
|
28
20
|
* 拼接代理路径和 API 路径,避免双斜杠
|
|
29
21
|
*/
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
path.startsWith('http://') ||
|
|
33
|
-
path.startsWith('https://') ||
|
|
34
|
-
path.startsWith('//')
|
|
35
|
-
) {
|
|
36
|
-
// 绝对 URL 直接透传,不拼接代理前缀
|
|
37
|
-
return path;
|
|
38
|
-
}
|
|
22
|
+
const joinProxyAndPath = (proxySuffix: string, path: string): string => {
|
|
39
23
|
const normalizedProxy = proxySuffix.endsWith('/')
|
|
40
24
|
? proxySuffix.slice(0, -1)
|
|
41
25
|
: proxySuffix;
|
|
@@ -43,30 +27,22 @@ export const joinProxyAndPath = (proxySuffix: string, path: string): string => {
|
|
|
43
27
|
return `${normalizedProxy}${normalizedPath}`;
|
|
44
28
|
};
|
|
45
29
|
|
|
46
|
-
/**
|
|
47
|
-
* 判断是否为绝对 URL
|
|
48
|
-
*/
|
|
49
|
-
const isAbsoluteUrl = (url: string): boolean => {
|
|
50
|
-
return (
|
|
51
|
-
url.startsWith('http://') ||
|
|
52
|
-
url.startsWith('https://') ||
|
|
53
|
-
url.startsWith('//')
|
|
54
|
-
);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
30
|
/**
|
|
58
31
|
* 解析最终请求 URL
|
|
59
32
|
*
|
|
60
33
|
* 配置优先级:
|
|
61
34
|
* 1. rawUrl: true → 直接返回原始 url
|
|
62
35
|
* 2. 绝对 URL → 直接返回
|
|
63
|
-
* 3.
|
|
64
|
-
* 4.
|
|
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
|
|
65
42
|
*/
|
|
66
43
|
export const resolveRequestUrl = (
|
|
67
44
|
url: string,
|
|
68
45
|
options?: UnifiedRequestOptions,
|
|
69
|
-
isAlwaysRemote = false,
|
|
70
46
|
): string => {
|
|
71
47
|
// rawUrl: 完全透传,不做任何处理
|
|
72
48
|
if (options?.rawUrl) {
|
|
@@ -78,21 +54,29 @@ export const resolveRequestUrl = (
|
|
|
78
54
|
return url;
|
|
79
55
|
}
|
|
80
56
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
57
|
+
// 拼接 apiBaseUrl + proxySuffix + url
|
|
58
|
+
const apiBaseUrl = resolveApiBaseUrl();
|
|
59
|
+
// skipProxy: true 时不拼接 proxySuffix
|
|
60
|
+
const proxySuffix = options?.skipProxy
|
|
61
|
+
? ''
|
|
62
|
+
: resolveProxySuffix(options?.proxySuffix);
|
|
85
63
|
|
|
86
|
-
|
|
87
|
-
|
|
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}`;
|
|
72
|
+
}
|
|
88
73
|
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
74
|
+
// 没有 apiBaseUrl 时,仅拼接 proxySuffix(相对路径)
|
|
75
|
+
if (proxySuffix) {
|
|
76
|
+
return joinProxyAndPath(proxySuffix, url);
|
|
77
|
+
}
|
|
92
78
|
|
|
93
|
-
|
|
94
|
-
const pathWithProxy = proxySuffix ? joinProxyAndPath(proxySuffix, url) : url;
|
|
95
|
-
return joinBaseUrl(baseURL, pathWithProxy);
|
|
79
|
+
return url;
|
|
96
80
|
};
|
|
97
81
|
|
|
98
82
|
/**
|
package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getAuthInfo } from '@/common/auth/
|
|
1
|
+
import { getAuthInfo } from '@/common/auth/auth-manager';
|
|
2
2
|
import { EEnv, getEnv } from '@/common/env';
|
|
3
3
|
import { request } from '@/common/request';
|
|
4
4
|
import { Spin } from '@arco-design/web-react';
|
|
@@ -63,7 +63,6 @@ const MicroAppLoader: React.FC<MicroAppLoaderProps> = ({
|
|
|
63
63
|
mainApp: 'portal-web',
|
|
64
64
|
env,
|
|
65
65
|
authToken: authInfo.token,
|
|
66
|
-
wsToken: authInfo.wsToken,
|
|
67
66
|
uid: authInfo.uid,
|
|
68
67
|
avatar: authInfo.avatar,
|
|
69
68
|
nickname: authInfo.nickname,
|
|
@@ -116,16 +115,60 @@ const MicroAppLoader: React.FC<MicroAppLoaderProps> = ({
|
|
|
116
115
|
container,
|
|
117
116
|
props: buildPropsRef.current(),
|
|
118
117
|
},
|
|
119
|
-
{
|
|
118
|
+
{
|
|
119
|
+
sandbox: { loose: true },
|
|
120
|
+
// 自定义 fetch,包装原生 fetch 以便在网络层面捕获错误
|
|
121
|
+
fetch: async (url, options) => {
|
|
122
|
+
try {
|
|
123
|
+
const response = await window.fetch(url, options);
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`HTTP ${response.status}: ${response.statusText}`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return response;
|
|
130
|
+
} catch (err) {
|
|
131
|
+
// 将网络错误转换为更友好的错误信息
|
|
132
|
+
const message =
|
|
133
|
+
err instanceof Error ? err.message : 'Network error';
|
|
134
|
+
throw new Error(`加载资源失败 (${url}): ${message}`);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
},
|
|
120
138
|
);
|
|
121
139
|
|
|
122
140
|
setMicroApp(appName, microApp);
|
|
123
141
|
|
|
142
|
+
// 关键:必须同时处理 loadPromise 和 bootstrapPromise/mountPromise 的错误
|
|
143
|
+
// qiankun 内部会产生多个 Promise 链,任何一个未处理的 rejection 都会导致页面崩溃
|
|
144
|
+
const handlePromiseError = (err: unknown) => {
|
|
145
|
+
if (!isCancelled) {
|
|
146
|
+
const message =
|
|
147
|
+
err instanceof Error ? err.message : 'Unknown error';
|
|
148
|
+
setError(message);
|
|
149
|
+
setLoading(false);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// 为所有 qiankun 返回的 Promise 添加错误处理器
|
|
154
|
+
microApp.loadPromise.catch(handlePromiseError);
|
|
155
|
+
microApp.bootstrapPromise.catch(handlePromiseError);
|
|
156
|
+
microApp.mountPromise.catch(handlePromiseError);
|
|
157
|
+
|
|
124
158
|
if (isCancelled) {
|
|
125
159
|
unmountApp(appName);
|
|
126
160
|
return;
|
|
127
161
|
}
|
|
128
162
|
|
|
163
|
+
// 等待加载完成,这里的错误会被上层 catch 捕获
|
|
164
|
+
await microApp.loadPromise;
|
|
165
|
+
|
|
166
|
+
if (isCancelled) {
|
|
167
|
+
unmountApp(appName);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 等待挂载完成
|
|
129
172
|
await microApp.mountPromise;
|
|
130
173
|
|
|
131
174
|
if (!isCancelled) {
|
|
@@ -134,7 +177,12 @@ const MicroAppLoader: React.FC<MicroAppLoaderProps> = ({
|
|
|
134
177
|
}
|
|
135
178
|
} catch (err) {
|
|
136
179
|
if (!isCancelled) {
|
|
137
|
-
|
|
180
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
181
|
+
// 提取更友好的错误信息
|
|
182
|
+
const friendlyMessage = message.includes('Failed to fetch')
|
|
183
|
+
? '无法连接到子应用服务,请检查子应用是否已启动'
|
|
184
|
+
: message;
|
|
185
|
+
setError(friendlyMessage);
|
|
138
186
|
setLoading(false);
|
|
139
187
|
}
|
|
140
188
|
}
|
|
@@ -157,24 +205,24 @@ const MicroAppLoader: React.FC<MicroAppLoaderProps> = ({
|
|
|
157
205
|
// 注意:不依赖 sharedServices,websocket 变化时通过 update() 更新 props 而不重新加载微应用
|
|
158
206
|
}, [entry, appName, env]);
|
|
159
207
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
208
|
+
return (
|
|
209
|
+
<div className="micro-app-container">
|
|
210
|
+
{error && (
|
|
163
211
|
<div className="micro-app-error">
|
|
164
212
|
<p>微应用加载失败</p>
|
|
165
213
|
<p className="error-detail">{error}</p>
|
|
166
214
|
</div>
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return (
|
|
172
|
-
<div className="micro-app-container" ref={wrapperRef}>
|
|
173
|
-
{loading && (
|
|
215
|
+
)}
|
|
216
|
+
{loading && !error && (
|
|
174
217
|
<div className="micro-app-loading">
|
|
175
218
|
<Spin dot tip={`正在加载 ${displayName || name}...`} />
|
|
176
219
|
</div>
|
|
177
220
|
)}
|
|
221
|
+
{/* qiankun 容器挂载点,error 时隐藏 */}
|
|
222
|
+
<div
|
|
223
|
+
ref={wrapperRef}
|
|
224
|
+
style={{ display: error ? 'none' : 'block', height: '100%' }}
|
|
225
|
+
/>
|
|
178
226
|
</div>
|
|
179
227
|
);
|
|
180
228
|
};
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
// ==================== 全局样式 ====================
|
|
2
|
-
//
|
|
2
|
+
// 先导入 Arco Design CSS(确保它在项目样式之前)
|
|
3
|
+
@import (css) '~@arco-design/web-react/dist/css/arco.css';
|
|
4
|
+
|
|
5
|
+
// 再导入共享主题变量(CSS Variables + Less Variables)
|
|
6
|
+
// 这样项目的变量定义会在 Arco 之后,可以正确覆盖
|
|
3
7
|
@import '<%= packageScope %>/shared-styles';
|
|
4
8
|
|
|
5
9
|
* {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { getAuthInfo } from '@/common/auth/
|
|
2
|
-
import type { AuthInfo } from '@/common/auth/type';
|
|
1
|
+
import { getAuthInfo, type IAuthInfo } from '@/common/auth/auth-manager';
|
|
3
2
|
import { handleAuthFailureRedirect, resolveAuthToken } from '@/common/request';
|
|
4
3
|
import { useCallback, useEffect, useState } from 'react';
|
|
5
4
|
|
|
@@ -13,7 +12,7 @@ interface UseAuthOptions {
|
|
|
13
12
|
*/
|
|
14
13
|
export const useAuth = (options: UseAuthOptions = {}) => {
|
|
15
14
|
const { skipAuthCheck = false } = options;
|
|
16
|
-
const [authInfo, setAuthInfo] = useState<
|
|
15
|
+
const [authInfo, setAuthInfo] = useState<IAuthInfo | null>(null);
|
|
17
16
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
18
17
|
const [loading, setLoading] = useState(true);
|
|
19
18
|
|
package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { getAuthInfo } from '@/common/auth/tool';
|
|
2
1
|
import { useTheme } from '@/hooks/useTheme';
|
|
2
|
+
import { logout } from '@/common/auth';
|
|
3
|
+
import { ROUTES } from '@/common/constants';
|
|
3
4
|
import {
|
|
4
5
|
Avatar,
|
|
5
6
|
Divider,
|
|
@@ -24,12 +25,14 @@ const Header = Layout.Header;
|
|
|
24
25
|
const LayoutHeader: React.FC = () => {
|
|
25
26
|
const { initialState } = useModel('@@initialState');
|
|
26
27
|
const { theme, toggleTheme } = useTheme();
|
|
27
|
-
const authInfo = getAuthInfo();
|
|
28
28
|
|
|
29
29
|
const handleMenuClick = useCallback((key: string) => {
|
|
30
30
|
if (key === 'logout') {
|
|
31
|
-
//
|
|
32
|
-
|
|
31
|
+
// 清除本地存储的认证信息
|
|
32
|
+
logout();
|
|
33
|
+
// 跳转到登录页
|
|
34
|
+
window.location.href = ROUTES.LOGIN;
|
|
35
|
+
return;
|
|
33
36
|
} else if (key === 'settings') {
|
|
34
37
|
// TODO: Navigate to settings page
|
|
35
38
|
console.log('settings');
|
|
@@ -55,7 +58,7 @@ const LayoutHeader: React.FC = () => {
|
|
|
55
58
|
{/* Logo */}
|
|
56
59
|
<div className="layout-header-logo">
|
|
57
60
|
<span className="logo-text">
|
|
58
|
-
{window.__MICO_MENUS__?.appName || '
|
|
61
|
+
{window.__MICO_MENUS__?.appName || 'Mico CENTER'}
|
|
59
62
|
</span>
|
|
60
63
|
</div>
|
|
61
64
|
|
|
@@ -76,15 +79,16 @@ const LayoutHeader: React.FC = () => {
|
|
|
76
79
|
{/* User info */}
|
|
77
80
|
<Dropdown droplist={droplist} position="br" trigger="click">
|
|
78
81
|
<div className="layout-header-user">
|
|
79
|
-
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
{initialState?.currentUser?.avatar && (
|
|
83
|
+
<Avatar size={32} style={{ marginRight: 8 }}>
|
|
84
|
+
{initialState?.currentUser?.avatar ? (
|
|
85
|
+
<img src={initialState.currentUser.avatar} alt="avatar" />
|
|
86
|
+
) : null}
|
|
87
|
+
</Avatar>
|
|
88
|
+
)}
|
|
86
89
|
<span className="user-name">
|
|
87
|
-
{
|
|
90
|
+
{initialState?.currentUser?.user_name ||
|
|
91
|
+
initialState?.currentUser?.email}
|
|
88
92
|
</span>
|
|
89
93
|
</div>
|
|
90
94
|
</Dropdown>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { ParsedMenuItem } from '@/common/menu';
|
|
2
|
-
import { getWindowMenus, parseMenuItems } from '@/common/menu';
|
|
2
|
+
import { filterMenuItems, getWindowMenus, parseMenuItems } from '@/common/menu';
|
|
3
3
|
import IconFont from '@/components/IconFont';
|
|
4
4
|
import { useMenuState } from '@/hooks/useMenuState';
|
|
5
5
|
import { useTheme } from '@/hooks/useTheme';
|
|
6
6
|
import { Layout, Menu } from '@arco-design/web-react';
|
|
7
7
|
import * as Icons from '@arco-design/web-react/icon';
|
|
8
|
+
import { useModel } from '@umijs/max';
|
|
8
9
|
import React, { useEffect, useMemo, useRef } from 'react';
|
|
9
10
|
import './index.less';
|
|
10
11
|
|
|
@@ -103,11 +104,18 @@ const LayoutMenu: React.FC<LayoutMenuProps> = () => {
|
|
|
103
104
|
const siderRef = useRef<HTMLDivElement>(null);
|
|
104
105
|
const { isDark } = useTheme();
|
|
105
106
|
|
|
107
|
+
const { initialState } = useModel('@@initialState');
|
|
108
|
+
const currentUser = initialState?.currentUser;
|
|
109
|
+
|
|
106
110
|
// Parse menu data
|
|
107
111
|
const menuItems = useMemo(() => {
|
|
108
112
|
const menus = getWindowMenus();
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
const filteredMenus = filterMenuItems(menus, {
|
|
114
|
+
isSuperuser: currentUser?.is_superuser,
|
|
115
|
+
sideMenus: (currentUser?.side_menus || []) as string[],
|
|
116
|
+
});
|
|
117
|
+
return parseMenuItems(filteredMenus);
|
|
118
|
+
}, [currentUser?.is_superuser, currentUser?.side_menus]);
|
|
111
119
|
|
|
112
120
|
// 使用菜单状态 Hook
|
|
113
121
|
const {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { layoutLogger } from '@/common/logger';
|
|
2
|
-
import { extractRoutes, findRouteByPath, getWindowMenus } from '@/common/menu';
|
|
2
|
+
import { extractRoutes, filterMenuItems, findRouteByPath, getWindowMenus } from '@/common/menu';
|
|
3
3
|
import AppTabs from '@/components/AppTabs';
|
|
4
4
|
import MicroAppLoader from '@/components/MicroAppLoader';
|
|
5
5
|
import { NO_AUTH_ROUTE_LIST } from '@/constants';
|
|
6
|
+
import ForbiddenPage from '@/pages/403';
|
|
6
7
|
import { Layout, Spin } from '@arco-design/web-react';
|
|
7
|
-
import { Outlet, useLocation } from '@umijs/max';
|
|
8
|
+
import { Outlet, useLocation, useModel } from '@umijs/max';
|
|
8
9
|
import React, { Suspense, useMemo } from 'react';
|
|
9
10
|
import LayoutHeader from './components/header';
|
|
10
11
|
import LayoutMenu from './components/menu';
|
|
@@ -38,17 +39,55 @@ const getAppNameFromEntry = (entry: string): string => {
|
|
|
38
39
|
*/
|
|
39
40
|
const BasicLayout: React.FC = () => {
|
|
40
41
|
const location = useLocation();
|
|
42
|
+
const { initialState } = useModel('@@initialState');
|
|
43
|
+
const currentUser = initialState?.currentUser;
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
const filterOptions = useMemo(
|
|
46
|
+
() => ({
|
|
47
|
+
isSuperuser: currentUser?.is_superuser,
|
|
48
|
+
sideMenus: (currentUser?.side_menus || []) as string[],
|
|
49
|
+
}),
|
|
50
|
+
[currentUser?.is_superuser, currentUser?.side_menus],
|
|
51
|
+
);
|
|
52
|
+
// 所有路由(不过滤权限,用于判断路径是否存在)
|
|
53
|
+
const allRoutes = useMemo(() => {
|
|
44
54
|
const menus = getWindowMenus();
|
|
45
55
|
return extractRoutes(menus);
|
|
46
56
|
}, []);
|
|
47
57
|
|
|
48
|
-
//
|
|
58
|
+
// 有权限的路由
|
|
59
|
+
const allowedRoutes = useMemo(() => {
|
|
60
|
+
const menus = getWindowMenus();
|
|
61
|
+
const filteredMenus = filterMenuItems(menus, filterOptions);
|
|
62
|
+
return extractRoutes(filteredMenus);
|
|
63
|
+
}, [filterOptions]);
|
|
64
|
+
|
|
65
|
+
// 查找当前路由配置(优先从有权限的路由中查找)
|
|
49
66
|
const currentRoute = useMemo(() => {
|
|
50
|
-
return findRouteByPath(
|
|
51
|
-
}, [
|
|
67
|
+
return findRouteByPath(allowedRoutes, location.pathname);
|
|
68
|
+
}, [allowedRoutes, location.pathname]);
|
|
69
|
+
|
|
70
|
+
// 判断是否是动态路由但无权限
|
|
71
|
+
const isForbidden = useMemo(() => {
|
|
72
|
+
// 如果在有权限的路由中找到了,说明有权限
|
|
73
|
+
if (currentRoute) return false;
|
|
74
|
+
// 如果在所有路由中也找不到,说明不是动态路由,交给 Umi 处理
|
|
75
|
+
const routeInAll = findRouteByPath(allRoutes, location.pathname);
|
|
76
|
+
// 在所有路由中存在,但在有权限的路由中不存在 → 无权限
|
|
77
|
+
const forbidden = !!routeInAll;
|
|
78
|
+
|
|
79
|
+
layoutLogger.log('isForbidden check:', {
|
|
80
|
+
pathname: location.pathname,
|
|
81
|
+
currentRoute,
|
|
82
|
+
routeInAll,
|
|
83
|
+
forbidden,
|
|
84
|
+
allRoutesCount: allRoutes.length,
|
|
85
|
+
allowedRoutesCount: allowedRoutes.length,
|
|
86
|
+
filterOptions,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return forbidden;
|
|
90
|
+
}, [currentRoute, allRoutes, allowedRoutes, location.pathname, filterOptions]);
|
|
52
91
|
|
|
53
92
|
// 判断是否需要显示布局
|
|
54
93
|
const showLayout = !NO_AUTH_ROUTE_LIST.includes(location.pathname);
|
|
@@ -58,9 +97,14 @@ const BasicLayout: React.FC = () => {
|
|
|
58
97
|
layoutLogger.log('renderContent:', {
|
|
59
98
|
pathname: location.pathname,
|
|
60
99
|
currentRoute,
|
|
61
|
-
|
|
100
|
+
isForbidden,
|
|
62
101
|
});
|
|
63
102
|
|
|
103
|
+
// 无权限,显示 403
|
|
104
|
+
if (isForbidden) {
|
|
105
|
+
return <ForbiddenPage />;
|
|
106
|
+
}
|
|
107
|
+
|
|
64
108
|
// 如果有匹配的动态路由配置且需要加载微应用
|
|
65
109
|
if (currentRoute?.loadType === 'microapp' && currentRoute.entry) {
|
|
66
110
|
layoutLogger.log('Loading microapp:', currentRoute);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Button, Result } from '@arco-design/web-react';
|
|
2
|
+
import { history } from '@umijs/max';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
const ForbiddenPage: React.FC = () => {
|
|
6
|
+
return (
|
|
7
|
+
<div style={{
|
|
8
|
+
display: 'flex',
|
|
9
|
+
justifyContent: 'center',
|
|
10
|
+
alignItems: 'center',
|
|
11
|
+
height: '100%',
|
|
12
|
+
minHeight: 400,
|
|
13
|
+
}}>
|
|
14
|
+
<Result
|
|
15
|
+
status="403"
|
|
16
|
+
title="无权限访问"
|
|
17
|
+
subTitle="抱歉,您没有权限访问此页面"
|
|
18
|
+
extra={
|
|
19
|
+
<Button type="primary" onClick={() => history.push('/')}>
|
|
20
|
+
返回首页
|
|
21
|
+
</Button>
|
|
22
|
+
}
|
|
23
|
+
/>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default ForbiddenPage;
|