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.
Files changed (64) hide show
  1. package/generators/micro-react/templates/.cursor/rules/layout-app.mdc +1 -1
  2. package/generators/micro-react/templates/.cursor/rules/request-auth.mdc +2 -2
  3. package/generators/micro-react/templates/.cursor/rules/theme-system.mdc +2 -2
  4. package/generators/micro-react/templates/CLAUDE.md +27 -11
  5. package/generators/micro-react/templates/README.md +2 -0
  6. package/generators/micro-react/templates/apps/layout/config/config.dev.ts +11 -4
  7. package/generators/micro-react/templates/apps/layout/config/config.prod.development.ts +1 -11
  8. package/generators/micro-react/templates/apps/layout/config/config.prod.testing.ts +0 -7
  9. package/generators/micro-react/templates/apps/layout/config/routes.ts +10 -0
  10. 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
  11. package/generators/micro-react/templates/apps/layout/docs/arch-/350/257/267/346/261/202/346/250/241/345/235/227.md +17 -15
  12. 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
  13. 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
  14. 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
  15. package/generators/micro-react/templates/apps/layout/mock/api.mock.ts +15 -15
  16. package/generators/micro-react/templates/apps/layout/src/app.tsx +43 -28
  17. package/generators/micro-react/templates/apps/layout/src/common/auth/{cs-auth-manager.ts → auth-manager.ts} +6 -63
  18. package/generators/micro-react/templates/apps/layout/src/common/auth/index.ts +1 -1
  19. package/generators/micro-react/templates/apps/layout/src/common/auth/tool.ts +2 -3
  20. package/generators/micro-react/templates/apps/layout/src/common/auth/type.ts +38 -0
  21. package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +109 -2
  22. package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +74 -1
  23. package/generators/micro-react/templates/apps/layout/src/common/micro/index.ts +0 -8
  24. package/generators/micro-react/templates/apps/layout/src/common/micro/types.ts +0 -2
  25. package/generators/micro-react/templates/apps/layout/src/common/request/config.ts +1 -1
  26. package/generators/micro-react/templates/apps/layout/src/common/request/index.ts +3 -11
  27. package/generators/micro-react/templates/apps/layout/src/common/request/interceptors.ts +2 -2
  28. package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +3 -2
  29. package/generators/micro-react/templates/apps/layout/src/common/request/token-refresh.ts +2 -2
  30. package/generators/micro-react/templates/apps/layout/src/common/request/url-resolver.ts +32 -48
  31. package/generators/micro-react/templates/apps/layout/src/components/AppTabs/index.less +1 -1
  32. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +62 -14
  33. package/generators/micro-react/templates/apps/layout/src/global.less +5 -1
  34. package/generators/micro-react/templates/apps/layout/src/hooks/useAuth.ts +2 -3
  35. package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.less +1 -1
  36. package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.tsx +17 -13
  37. package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.less +1 -1
  38. package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +11 -3
  39. package/generators/micro-react/templates/apps/layout/src/layouts/index.less +1 -1
  40. package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +52 -8
  41. package/generators/micro-react/templates/apps/layout/src/pages/403/index.tsx +28 -0
  42. package/generators/micro-react/templates/apps/layout/src/pages/User/Login/index.less +275 -0
  43. package/generators/micro-react/templates/apps/layout/src/pages/User/Login/index.tsx +142 -0
  44. package/generators/micro-react/templates/apps/layout/src/services/auth.ts +1 -0
  45. package/generators/micro-react/templates/apps/layout/src/services/user.ts +25 -0
  46. package/generators/micro-react/templates/packages/shared-styles/README.md +16 -14
  47. package/generators/micro-react/templates/packages/shared-styles/arco-design-mobile-override.less +91 -0
  48. package/generators/micro-react/templates/packages/shared-styles/arco-override.less +41 -0
  49. package/generators/micro-react/templates/packages/shared-styles/index.d.ts +44 -0
  50. package/generators/micro-react/templates/packages/shared-styles/index.less +0 -1
  51. package/generators/micro-react/templates/packages/shared-styles/package.json +6 -3
  52. package/generators/micro-react/templates/packages/shared-styles/themes/dark/custom-var.less +118 -74
  53. package/generators/micro-react/templates/packages/shared-styles/themes/normal/custom-var.less +175 -101
  54. package/generators/micro-react/templates/packages/shared-styles/variables-only.less +357 -225
  55. package/generators/micro-react/templates/packages/shared-styles/variables.less +290 -201
  56. package/generators/subapp-react/templates/homepage/config/config.prod.ts +2 -2
  57. package/generators/subapp-react/templates/homepage/config/config.ts +6 -0
  58. package/generators/subapp-react/templates/homepage/mock/api.mock.ts +43 -43
  59. package/generators/subapp-react/templates/homepage/typings.d.ts +76 -0
  60. package/package.json +1 -1
  61. package/generators/micro-react/templates/apps/layout/src/styles/arco-override.less +0 -78
  62. package/generators/micro-react/templates/apps/layout/src/styles/themes/dark/custom-var.less +0 -244
  63. package/generators/micro-react/templates/apps/layout/src/styles/themes/normal/custom-var.less +0 -195
  64. 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
- * 拼接 baseUrl 和路径,自动处理多余的斜杠
9
+ * 判断是否为绝对 URL
13
10
  */
14
- export const joinBaseUrl = (baseUrl: string, url: string): string => {
15
- if (
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
- export const joinProxyAndPath = (proxySuffix: string, path: string): string => {
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. 生产环境baseURL + (skipProxy ? '' : 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
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
- const useRelative = process.env.NODE_ENV !== 'production' && !isAlwaysRemote;
82
- if (useRelative) {
83
- return url;
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
- // 确定 baseURL
87
- const baseURL = options?.baseURL ?? resolveApiBaseUrl();
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
- // 确定是否拼接 proxySuffix
90
- const shouldUseProxy = !options?.skipProxy;
91
- const proxySuffix = shouldUseProxy ? getProxySuffix(options?.proxySuffix) : '';
74
+ // 没有 apiBaseUrl 时,仅拼接 proxySuffix(相对路径)
75
+ if (proxySuffix) {
76
+ return joinProxyAndPath(proxySuffix, url);
77
+ }
92
78
 
93
- // 拼接最终 URL
94
- const pathWithProxy = proxySuffix ? joinProxyAndPath(proxySuffix, url) : url;
95
- return joinBaseUrl(baseURL, pathWithProxy);
79
+ return url;
96
80
  };
97
81
 
98
82
  /**
@@ -1,4 +1,4 @@
1
- @import '@/styles/variables.less';
1
+ @import '<%= packageScope %>/shared-styles/variables-only';
2
2
 
3
3
  .app-tabs.arco-tabs {
4
4
  min-height: 46px;
@@ -1,4 +1,4 @@
1
- import { getAuthInfo } from '@/common/auth/cs-auth-manager';
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
- { sandbox: { loose: true } },
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
- setError(err instanceof Error ? err.message : 'Unknown error');
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
- if (error) {
161
- return (
162
- <div className="micro-app-container">
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
- </div>
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
- // 导入共享主题变量(CSS Variables + Less Variables)
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/tool';
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<AuthInfo | null>(null);
15
+ const [authInfo, setAuthInfo] = useState<IAuthInfo | null>(null);
17
16
  const [isAuthenticated, setIsAuthenticated] = useState(false);
18
17
  const [loading, setLoading] = useState(true);
19
18
 
@@ -1,4 +1,4 @@
1
- @import '@/styles/variables.less';
1
+ @import '<%= packageScope %>/shared-styles/variables-only';
2
2
 
3
3
  // Common mixins
4
4
  .header-gray-btn-mixin() {
@@ -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
- // TODO: Implement logout logic
32
- console.log('logout');
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 || 'AUDIT CENTER'}
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
- <Avatar size={32} style={{ marginRight: 8 }}>
80
- {authInfo.avatar ? (
81
- <img src={authInfo.avatar} alt="avatar" />
82
- ) : (
83
- <IconUser />
84
- )}
85
- </Avatar>
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
- {authInfo.nickname || initialState?.currentUser?.name || '用户'}
90
+ {initialState?.currentUser?.user_name ||
91
+ initialState?.currentUser?.email}
88
92
  </span>
89
93
  </div>
90
94
  </Dropdown>
@@ -1,4 +1,4 @@
1
- @import '@/styles/variables.less';
1
+ @import '<%= packageScope %>/shared-styles/variables-only';
2
2
 
3
3
  .arco-layout-sider-collapsed {
4
4
  .layout-menu {
@@ -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
- return parseMenuItems(menus);
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,4 +1,4 @@
1
- @import '@/styles/variables.less';
1
+ @import '<%= packageScope %>/shared-styles/variables-only';
2
2
 
3
3
  .layout-container {
4
4
  min-width: 100vw;
@@ -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
- const routes = useMemo(() => {
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(routes, location.pathname);
51
- }, [routes, location.pathname]);
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
- routes,
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;