generator-mico-cli 0.2.28 → 0.2.30

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 (112) hide show
  1. package/README.md +7 -20
  2. package/bin/mico.js +27 -62
  3. package/generators/micro-react/index.js +25 -1
  4. package/generators/micro-react/templates/.cursor/rules/always-read-docs.mdc +3 -0
  5. package/generators/micro-react/templates/.cursor/rules/project-overview.mdc +1 -0
  6. package/generators/micro-react/templates/CICD/start_dev.sh +11 -0
  7. package/generators/micro-react/templates/CICD/start_local.sh +9 -0
  8. package/generators/micro-react/templates/CICD/start_prod.sh +13 -0
  9. package/generators/micro-react/templates/CICD/start_test.sh +11 -0
  10. package/generators/micro-react/templates/CLAUDE.md +1 -0
  11. package/generators/micro-react/templates/README.md +1 -1
  12. package/generators/micro-react/templates/apps/layout/config/config.dev.ts +13 -5
  13. package/generators/micro-react/templates/apps/layout/config/config.prod.development.ts +12 -0
  14. package/generators/micro-react/templates/apps/layout/config/config.prod.testing.ts +12 -0
  15. package/generators/micro-react/templates/apps/layout/config/config.prod.ts +14 -0
  16. package/generators/micro-react/templates/apps/layout/docs/feature-PermissionFilter/346/214/211/351/222/256/346/235/203/351/231/220.md +116 -0
  17. package/generators/micro-react/templates/apps/layout/docs/feature-/345/233/275/351/231/205/345/214/226.md +121 -0
  18. 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 +8 -0
  19. package/generators/micro-react/templates/apps/layout/docs/feature-/350/217/234/345/215/225/346/235/203/351/231/220/346/216/247/345/210/266.md +83 -77
  20. package/generators/micro-react/templates/apps/layout/docs/feature-/350/267/257/347/224/261/344/270/216/350/217/234/345/215/225/350/247/243/350/200/246.md +50 -35
  21. package/generators/micro-react/templates/apps/layout/docs/feature-/350/267/257/347/224/261/346/235/203/351/231/220/346/227/245/345/277/227.md +162 -0
  22. package/generators/micro-react/templates/apps/layout/mock/api.mock.ts +23 -31
  23. package/generators/micro-react/templates/apps/layout/mock/menus.ts +14 -0
  24. package/generators/micro-react/templates/apps/layout/mock/pages.ts +27 -8
  25. package/generators/micro-react/templates/apps/layout/package.json +2 -0
  26. package/generators/micro-react/templates/apps/layout/src/app.tsx +85 -4
  27. package/generators/micro-react/templates/apps/layout/src/common/auth/index.ts +3 -0
  28. package/generators/micro-react/templates/apps/layout/src/common/auth/tenant.ts +25 -0
  29. package/generators/micro-react/templates/apps/layout/src/common/auth/type.ts +41 -27
  30. package/generators/micro-react/templates/apps/layout/src/common/intl/formatLayoutMessage.ts +30 -0
  31. package/generators/micro-react/templates/apps/layout/src/common/intl/index.ts +6 -0
  32. package/generators/micro-react/templates/apps/layout/src/common/intl/intlRuntime.ts +14 -0
  33. package/generators/micro-react/templates/apps/layout/src/common/intl/localeMapping.ts +30 -0
  34. package/generators/micro-react/templates/apps/layout/src/common/intl/types.ts +14 -0
  35. package/generators/micro-react/templates/apps/layout/src/common/intl/useLayoutIntl.ts +40 -0
  36. package/generators/micro-react/templates/apps/layout/src/common/logger.ts +3 -4
  37. package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +148 -85
  38. package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +29 -6
  39. package/generators/micro-react/templates/apps/layout/src/common/micro/types.ts +23 -0
  40. package/generators/micro-react/templates/apps/layout/src/common/portal-data.ts +46 -2
  41. package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +74 -15
  42. package/generators/micro-react/templates/apps/layout/src/common/request/token-refresh.ts +2 -0
  43. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +32 -6
  44. package/generators/micro-react/templates/apps/layout/src/components/PermissionFilter/index.tsx +51 -0
  45. package/generators/micro-react/templates/apps/layout/src/components/RightContent/AvatarDropdown.tsx +10 -1
  46. package/generators/micro-react/templates/apps/layout/src/components/RightContent/TenantDropdown.tsx +76 -0
  47. package/generators/micro-react/templates/apps/layout/src/components/RightContent/index.ts +1 -0
  48. package/generators/micro-react/templates/apps/layout/src/components/RightContent/tenant-dropdown.less +48 -0
  49. package/generators/micro-react/templates/apps/layout/src/constants/index.ts +1 -0
  50. package/generators/micro-react/templates/apps/layout/src/hooks/index.ts +1 -0
  51. package/generators/micro-react/templates/apps/layout/src/hooks/useMenuState.ts +18 -0
  52. package/generators/micro-react/templates/apps/layout/src/hooks/useTenant.ts +41 -0
  53. package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.tsx +4 -1
  54. package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +21 -9
  55. package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +105 -60
  56. package/generators/micro-react/templates/apps/layout/src/locales/en-US.ts +28 -0
  57. package/generators/micro-react/templates/apps/layout/src/locales/zh-CN.ts +26 -0
  58. package/generators/micro-react/templates/apps/layout/src/pages/404/index.tsx +7 -3
  59. package/generators/micro-react/templates/apps/layout/src/pages/Home/index.less +32 -0
  60. package/generators/micro-react/templates/apps/layout/src/pages/Home/index.tsx +148 -4
  61. package/generators/micro-react/templates/apps/layout/src/requestErrorConfig.ts +2 -1
  62. package/generators/micro-react/templates/apps/layout/src/services/user.ts +79 -21
  63. package/generators/micro-react/templates/apps/layout/typings.d.ts +16 -0
  64. package/generators/micro-react/templates/docs/package-shared.md +189 -0
  65. package/generators/micro-react/templates/package.json +1 -1
  66. package/generators/micro-react/templates/packages/common-intl/README.md +78 -368
  67. package/generators/micro-react/templates/packages/common-intl/package.json +3 -13
  68. package/generators/micro-react/templates/packages/common-intl/src/index.ts +5 -6
  69. package/generators/micro-react/templates/packages/common-intl/src/intl.ts +115 -28
  70. package/generators/micro-react/templates/packages/common-intl/src/umiLocaleBridge.ts +101 -0
  71. package/generators/micro-react/templates/packages/common-intl/tsconfig.json +2 -4
  72. package/generators/micro-react/templates/packages/shared/README.md +120 -0
  73. package/generators/micro-react/templates/packages/shared/package.json +26 -0
  74. package/generators/micro-react/templates/packages/shared/services/common/index.ts +43 -0
  75. package/generators/micro-react/templates/packages/shared/services/index.ts +21 -0
  76. package/generators/micro-react/templates/packages/shared/services/request.ts +43 -0
  77. package/generators/micro-react/templates/packages/shared/timezone/index.ts +228 -0
  78. package/generators/micro-react/templates/packages/shared/tsconfig.json +20 -0
  79. package/generators/micro-react/templates/scripts/apply-sentry-plugin.ts +6 -1
  80. package/generators/micro-react/templates/turbo.json +9 -1
  81. package/generators/subapp-react/index.js +28 -22
  82. package/generators/subapp-react/templates/homepage/README.md +1 -0
  83. package/generators/subapp-react/templates/homepage/config/config.prod.development.ts +1 -0
  84. package/generators/subapp-react/templates/homepage/config/config.prod.testing.ts +1 -0
  85. package/generators/subapp-react/templates/homepage/config/config.prod.ts +1 -0
  86. package/generators/subapp-react/templates/homepage/config/config.ts +10 -0
  87. package/generators/subapp-react/templates/homepage/docs/feature-PermissionFilter/346/214/211/351/222/256/346/235/203/351/231/220.md +35 -0
  88. package/generators/subapp-react/templates/homepage/docs/feature-/345/233/275/351/231/205/345/214/226.md +124 -0
  89. package/generators/subapp-react/templates/homepage/package.json +3 -1
  90. package/generators/subapp-react/templates/homepage/src/app.tsx +104 -2
  91. package/generators/subapp-react/templates/homepage/src/common/intl/index.ts +15 -0
  92. package/generators/subapp-react/templates/homepage/src/common/intl/intlRuntime.ts +14 -0
  93. package/generators/subapp-react/templates/homepage/src/common/intl/localeMapping.ts +24 -0
  94. package/generators/subapp-react/templates/homepage/src/common/intl/subappIntlConfig.ts +28 -0
  95. package/generators/subapp-react/templates/homepage/src/common/intl/subappLocale.ts +18 -0
  96. package/generators/subapp-react/templates/homepage/src/common/intl/subappOwnIntl.ts +63 -0
  97. package/generators/subapp-react/templates/homepage/src/common/intl/types.ts +14 -0
  98. package/generators/subapp-react/templates/homepage/src/common/intl/useSubappIntl.ts +61 -0
  99. package/generators/subapp-react/templates/homepage/src/common/locale.ts +80 -0
  100. package/generators/subapp-react/templates/homepage/src/common/mainApp.ts +41 -2
  101. package/generators/subapp-react/templates/homepage/src/components/PermissionFilter/index.tsx +48 -0
  102. package/generators/subapp-react/templates/homepage/src/locales/en-US.ts +6 -0
  103. package/generators/subapp-react/templates/homepage/src/locales/zh-CN.ts +6 -0
  104. package/generators/subapp-react/templates/homepage/src/pages/index.less +10 -0
  105. package/generators/subapp-react/templates/homepage/src/pages/index.tsx +86 -1
  106. package/generators/subapp-react/templates/homepage/typings.d.ts +12 -0
  107. package/lib/utils.js +0 -1
  108. package/package.json +2 -2
  109. package/generators/micro-react/templates/apps/layout/docs/common-intl.md +0 -372
  110. package/generators/micro-react/templates/packages/common-intl/src/indexedDBUtils.ts +0 -51
  111. package/generators/micro-react/templates/packages/common-intl/src/utils.ts +0 -482
  112. package/generators/micro-react/templates/packages/common-intl/vite.config.ts +0 -25
@@ -10,13 +10,9 @@ export interface PageConfig {
10
10
  id: number;
11
11
  /** 页面名称 */
12
12
  name: string;
13
- /** 英文名称(用于英文环境权限匹配) */
14
- nameEn?: string;
15
- /** 菜单唯一标识符(用于权限匹配的兜底) */
16
- nameKey?: string;
17
13
  /** 路由路径 */
18
14
  route: string;
19
- /** 路由前缀路径 */
15
+ /** 路由前缀路径(微应用在主应用中的挂载前缀,可与 route 不同) */
20
16
  base: string;
21
17
  /** 是否启用 */
22
18
  enabled: boolean;
@@ -34,7 +30,7 @@ export interface PageConfig {
34
30
  adminOnly?: boolean;
35
31
  /** 是否开启权限控制 */
36
32
  accessControlEnabled: boolean;
37
- /** 路由权限标识(用于匹配 sideMenus) */
33
+ /** 路由权限标识(与用户信息 `menu_perms`、菜单项 code 一致) */
38
34
  routeKey: string | null;
39
35
  /** 关联的主文档 ID */
40
36
  mainDocumentId: number;
@@ -219,6 +215,33 @@ declare global {
219
215
  disableAuth?: boolean;
220
216
  /** 获取时区列表的 API 地址 */
221
217
  timezoneListUrl?: string;
218
+ /**
219
+ * 多语言中台 initIntl 参数(优先于下方 commonIntl;与 packages/common-intl/src/intl.ts 合并默认项)
220
+ */
221
+ intl?: {
222
+ tag: string;
223
+ app_name: string;
224
+ indexedDBParams?: {
225
+ dbName?: string;
226
+ dbVersion?: number;
227
+ storeName?: string;
228
+ keyPathKey?: string;
229
+ };
230
+ };
231
+ /**
232
+ * 多语言中台:配置有效 `tag`、`app_name` 时主应用首屏拉取中台文案(见 packages/common-intl/README.md)
233
+ * @deprecated 优先使用 `intl`;仍保留以兼容旧 head 注入
234
+ */
235
+ commonIntl?: {
236
+ tag: string;
237
+ app_name: string;
238
+ indexedDBParams?: {
239
+ dbName?: string;
240
+ dbVersion?: number;
241
+ storeName?: string;
242
+ keyPathKey?: string;
243
+ };
244
+ };
222
245
  [key: string]: unknown;
223
246
  };
224
247
  __MICO_WORKSPACE__?: WorkspaceConfig | null;
@@ -1,3 +1,6 @@
1
+ import type { ITenantItem } from '@/common/auth/type';
2
+ import type { SupportedLocale } from '@/common/locale';
3
+
1
4
  /**
2
5
  * 微前端 Props 类型定义
3
6
  * 主应用通过 qiankun 传递给子应用的配置数据
@@ -8,6 +11,7 @@
8
11
  * - 1: 鉴权失败(token 无效/过期),需要主应用重新获取 token
9
12
  * - 2: 业务错误(参数错误、逻辑拒绝、网络超时、权限不足等)
10
13
  */
14
+
11
15
  export enum EMicroAppErrorStatus {
12
16
  /** 鉴权失败,需重获 token */
13
17
  AUTH_FAILURE = 1,
@@ -163,6 +167,25 @@ export interface IMicroAppProps {
163
167
  * 子应用可以直接使用这些服务,无需重复初始化
164
168
  */
165
169
  services?: ISharedServices;
170
+ /** 主应用标识(固定为当前主应用名) */
171
+ mainApp?: string;
172
+ /**
173
+ * 主应用的 request 实例(umi-request/axios 封装)
174
+ * 子应用可直接使用,复用主应用的拦截器与鉴权信息
175
+ */
176
+ request?: (...args: any[]) => Promise<any>;
177
+ /** 当前多语言类型(zh-CN / en-US) */
178
+ locale?: SupportedLocale;
179
+ /** 按钮级权限列表(与 currentUser.button_perms 一致,便于子应用直接消费) */
180
+ button_perms?: string[];
181
+ /** 菜单级权限列表 */
182
+ menu_perms?: string[];
183
+ /** 区域级权限列表 */
184
+ region_perms?: string[];
185
+ /** 当前租户 code(多租户模式下由主应用控制,单租户时为默认租户 code) */
186
+ current_tenant?: string;
187
+ /** 当前用户有权限的租户列表(已按 app_perms 过滤) */
188
+ allowed_tenants?: ITenantItem[];
166
189
  /** 其他扩展字段 */
167
190
  [key: string]: unknown;
168
191
  }
@@ -7,6 +7,37 @@
7
7
  */
8
8
  import type { MenuItem, PublicPageItem } from './menu/types';
9
9
 
10
+ /**
11
+ * 根据路径查找对应的页面配置(与路由侧 `findRouteByPath` 规则一致:精确匹配 + 最长前缀通配 `/*`)
12
+ */
13
+ export const findPageByPath = (
14
+ pages: PublicPageItem[],
15
+ pathname: string,
16
+ ): PublicPageItem | undefined => {
17
+ let exact: PublicPageItem | undefined;
18
+ let bestWildcard: { page: PublicPageItem; basePath: string } | undefined;
19
+
20
+ for (const page of pages) {
21
+ if (!page.enabled) continue;
22
+
23
+ if (page.route === pathname) {
24
+ exact = page;
25
+ continue;
26
+ }
27
+
28
+ if (page.route.endsWith('/*')) {
29
+ const basePath = page.route.slice(0, -2);
30
+ if (pathname === basePath || pathname.startsWith(`${basePath}/`)) {
31
+ if (!bestWildcard || basePath.length > bestWildcard.basePath.length) {
32
+ bestWildcard = { page, basePath };
33
+ }
34
+ }
35
+ }
36
+ }
37
+
38
+ return exact || bestWildcard?.page;
39
+ };
40
+
10
41
  /** 获取页面列表 (window.__MICO_PAGES__) */
11
42
  export const getPages = (): PublicPageItem[] => {
12
43
  if (typeof window === 'undefined') return [];
@@ -39,7 +70,20 @@ export const getPageById = (pageId: number): PublicPageItem | undefined => {
39
70
  return getPageIdIndex().get(pageId);
40
71
  };
41
72
 
42
- /** 获取菜单项关联的页面 */
73
+ /**
74
+ * 获取菜单项关联的页面
75
+ * - 优先按 `pageId` 在页面列表中查找
76
+ * - 无匹配时再用菜单跳转路径 `path` 与页面 `route` 匹配(同 `findPageByPath`)
77
+ */
43
78
  export const getMenuPage = (item: MenuItem): PublicPageItem | undefined => {
44
- return item.pageId ? getPageById(item.pageId) : undefined;
79
+ if (item.type !== 'page') return undefined;
80
+ if (item.pageId) {
81
+ const byId = getPageById(item.pageId);
82
+ if (byId) return byId;
83
+ }
84
+ const path = item.path;
85
+ if (path) {
86
+ return findPageByPath(getPages(), path);
87
+ }
88
+ return undefined;
45
89
  };
@@ -9,6 +9,9 @@ import {
9
9
  } from '@/common/auth/auth-manager';
10
10
  import { isPageAuthFree } from '@/common/menu';
11
11
  import { isNoAuthRoute, ROUTES } from '@/constants';
12
+ import { captureError } from '@common-web/sentry';
13
+ import { formatLayoutMessage } from '@/common/intl';
14
+ import { Modal } from '@mico-platform/ui';
12
15
  import {
13
16
  getTicketParam,
14
17
  resolveAuthToken,
@@ -26,6 +29,30 @@ import { removeParamFromUrl } from './url-resolver';
26
29
 
27
30
  let ticketPromise: Promise<void> | null = null;
28
31
 
32
+ /** SSO 换取 token 的时间戳(毫秒),null 表示本次会话未经过 SSO 换取 */
33
+ let tokenAcquiredAt: number | null = null;
34
+
35
+ /** 登录失败弹窗是否已显示,防止多个请求同时失败时重复弹出 */
36
+ let redirectLimitModalShown = false;
37
+
38
+ /**
39
+ * 执行 SSO 重定向跳转,回跳地址中固定携带 redirect_count=1
40
+ */
41
+ const performSsoRedirect = (): void => {
42
+ const externalLoginPath = resolveExternalLoginPath();
43
+ console.log('[SSO] 执行 SSO 重定向', { externalLoginPath });
44
+
45
+ const redirectUrl = new URL(window.location.href);
46
+ redirectUrl.searchParams.delete('redirect_count');
47
+ redirectUrl.searchParams.set('redirect_count', '1');
48
+ redirectUrl.searchParams.delete('ticket');
49
+ const serviceUrl = redirectUrl.toString();
50
+
51
+ window.location.href = `${
52
+ externalLoginPath ?? ROUTES.LOGIN
53
+ }?service=${encodeURIComponent(serviceUrl)}`;
54
+ };
55
+
29
56
  /**
30
57
  * 处理认证失败后的重定向
31
58
  */
@@ -50,23 +77,54 @@ export const handleAuthFailureRedirect = (): void => {
50
77
  ? parseInt(redirectCountParam, 10)
51
78
  : 0;
52
79
 
53
- // 如果 redirect 次数小于 1 次,则执行 redirect 跳转登录
80
+ console.log('[SSO] handleAuthFailureRedirect', { redirectCount });
81
+
82
+ // 如果 redirect 次数小于 1 次,则执行自动 redirect 跳转登录
54
83
  if (redirectCount < 1) {
55
- const externalLoginPath = resolveExternalLoginPath();
56
- console.log('handleAuthFailureRedirect', externalLoginPath);
57
-
58
- // 构建回跳地址
59
- const redirectUrl = new URL(window.location.href);
60
- redirectUrl.searchParams.delete('redirect_count');
61
- redirectUrl.searchParams.set('redirect_count', '1');
62
- const serviceUrl = redirectUrl.toString();
63
-
64
- window.location.href = `${
65
- externalLoginPath ?? ROUTES.LOGIN
66
- }?service=${encodeURIComponent(serviceUrl)}`;
67
- } else {
68
- console.warn('认证失败,但已达到最大重定向次数,停止重定向');
84
+ performSsoRedirect();
85
+ return;
86
+ }
87
+
88
+ // redirect 次数已达上限,停止自动重定向,记录日志并弹窗询问用户是否手动重试
89
+ console.warn('[SSO] 认证失败,已达到最大重定向次数,停止重定向', {
90
+ redirectCount,
91
+ });
92
+
93
+ const tokenAge =
94
+ tokenAcquiredAt !== null
95
+ ? Math.round((Date.now() - tokenAcquiredAt) / 1000)
96
+ : null;
97
+
98
+ captureError(new Error('认证失败:redirect_count 已达上限,停止重定向'), {
99
+ tags: { scene: 'auth_redirect_limit' },
100
+ extra: {
101
+ tokenAcquiredAt: tokenAcquiredAt
102
+ ? new Date(tokenAcquiredAt).toISOString()
103
+ : null,
104
+ tokenAgeSeconds: tokenAge,
105
+ },
106
+ });
107
+
108
+ if (redirectLimitModalShown) {
109
+ console.log('[SSO] 登录失败弹窗已显示,跳过重复弹出');
110
+ return;
69
111
  }
112
+
113
+ redirectLimitModalShown = true;
114
+ Modal.confirm({
115
+ title: formatLayoutMessage({ id: 'sso_auth_failure_modal_title', defaultMessage: '登录提示' }),
116
+ content: formatLayoutMessage({ id: 'sso_auth_failure_modal_content', defaultMessage: '自动登录失败,是否重新尝试登录?' }),
117
+ okText: formatLayoutMessage({ id: 'sso_auth_failure_modal_ok', defaultMessage: '重新登录' }),
118
+ cancelText: formatLayoutMessage({ id: 'sso_auth_failure_modal_cancel', defaultMessage: '取消' }),
119
+ onOk: () => {
120
+ console.log('[SSO] 用户确认重新登录,执行手动重试重定向');
121
+ redirectLimitModalShown = false;
122
+ performSsoRedirect();
123
+ },
124
+ onCancel: () => {
125
+ redirectLimitModalShown = false;
126
+ },
127
+ });
70
128
  };
71
129
 
72
130
  /**
@@ -113,6 +171,7 @@ export const ensureSsoSession = async (): Promise<void> => {
113
171
  setFetchingToken(false);
114
172
 
115
173
  if (resolveAuthToken()) {
174
+ tokenAcquiredAt = Date.now();
116
175
  processPendingRequests();
117
176
  } else {
118
177
  throw new Error('SSO 认证失败');
@@ -10,6 +10,7 @@ import {
10
10
  setStoredRefreshToken,
11
11
  } from '@/common/auth/auth-manager';
12
12
  import { resolveRefreshEndpoint } from './config';
13
+ import { buildDefaultHeaders } from './interceptors';
13
14
  import type { PendingRequest, RequestContext } from './types';
14
15
 
15
16
  // 当前是否在请求用ticket换取token
@@ -82,6 +83,7 @@ export const refreshAuthToken = async (): Promise<string | null> => {
82
83
  }>(endpoint, {
83
84
  method: 'POST',
84
85
  data: { refresh: refreshToken },
86
+ headers: buildDefaultHeaders(),
85
87
  });
86
88
 
87
89
  maybePersistTokens(result);
@@ -1,3 +1,4 @@
1
+ import type { IUserInfo } from '@/common/auth/type';
1
2
  import { getAuthInfo } from '@/common/auth/auth-manager';
2
3
  import { EEnv, getEnv } from '@/common/env';
3
4
  import { getCurrentLocale } from '@/common/locale';
@@ -55,6 +56,7 @@ const MicroAppLoader: React.FC<MicroAppLoaderProps> = ({
55
56
  });
56
57
 
57
58
  const { initialState } = useModel('@@initialState');
59
+ const currentUser = initialState?.currentUser;
58
60
  const location = useLocation();
59
61
  const isAuthReady =
60
62
  isAuthDisabled() ||
@@ -74,16 +76,24 @@ const MicroAppLoader: React.FC<MicroAppLoaderProps> = ({
74
76
  : 'production';
75
77
  }, []);
76
78
 
77
- // 构建传递给子应用的 props
79
+ // 构建传递给子应用的 props(用户信息以 initialState.currentUser 为准,token 仍读存储)
78
80
  const buildProps = useCallback(() => {
79
81
  const authInfo = getAuthInfo();
80
- return {
82
+ const displayName = currentUser?.user_name || currentUser?.name || '';
83
+ // 用户有权限的租户列表:tenant ∩ app_perms(按 app_perm.key === tenant.code 匹配)
84
+ const allowedTenantCodes = new Set(
85
+ (currentUser?.app_perms ?? []).map((p) => p.key),
86
+ );
87
+ const allowedTenants = (currentUser?.tenant ?? []).filter((t) =>
88
+ allowedTenantCodes.has(t.code),
89
+ );
90
+ const props = {
81
91
  mainApp: '<%= projectName %>',
82
92
  env,
83
93
  authToken: authInfo.token,
84
- uid: authInfo.uid,
85
- avatar: authInfo.avatar,
86
- nickname: authInfo.nickname,
94
+ uid: currentUser ? String(currentUser.id) : authInfo.uid,
95
+ avatar: currentUser?.avatar ?? authInfo.avatar,
96
+ nickname: displayName || authInfo.nickname,
87
97
  base: normalizeMicroAppBase(base || '/'),
88
98
  // 传递当前路由路径,让子应用进行内部路由切换
89
99
  routePath,
@@ -91,8 +101,24 @@ const MicroAppLoader: React.FC<MicroAppLoaderProps> = ({
91
101
  request,
92
102
  // 传递当前多语言类型给子应用(zh-CN, en-US)
93
103
  locale: getCurrentLocale(),
104
+ // 与 fetchUserInfo 一致:子应用用于按钮级权限(如 PermissionFilter)
105
+ button_perms: currentUser?.button_perms ?? [],
106
+ menu_perms: currentUser?.menu_perms ?? [],
107
+ region_perms: currentUser?.region_perms ?? [],
108
+ is_superuser: currentUser?.is_superuser,
109
+ // 将当前用户信息传递给子应用,仅传使用到的字段
110
+ currentUser: {
111
+ tenant: currentUser?.tenant,
112
+ isMultiTenant: currentUser?.isMultiTenant,
113
+ email: currentUser?.email,
114
+ },
115
+ current_tenant: currentUser?.current_tenant,
116
+ allowed_tenants: allowedTenants,
94
117
  };
95
- }, [base, env, routePath]);
118
+ const { request: _request, ...loggableProps } = props;
119
+ console.log('[MicroAppLoader] buildProps', { appName, ...loggableProps });
120
+ return props;
121
+ }, [base, env, routePath, currentUser, appName]);
96
122
 
97
123
  // ref 持有最新的 buildProps,避免加载 effect 依赖它导致子应用被重载
98
124
  const buildPropsRef = useRef(buildProps);
@@ -0,0 +1,51 @@
1
+ import { isSuperuserUser } from '@/common/menu/parser';
2
+ import { useModel } from '@umijs/max';
3
+ import React from 'react';
4
+
5
+ export interface IPermissionFilterProps {
6
+ /** 按钮/操作权限标识,需在用户 `button_perms` 中 */
7
+ permissionKey: string;
8
+ children?: React.ReactNode;
9
+ /** 无权限时渲染内容;不传则渲染 null */
10
+ fallback?: React.ReactNode;
11
+ }
12
+
13
+ function hasButtonPermission(
14
+ permissionKey: string,
15
+ buttonPerms: string[] | undefined,
16
+ isSuperuser?: boolean | number,
17
+ ): boolean {
18
+ if (!permissionKey?.trim()) {
19
+ return false;
20
+ }
21
+ if (isSuperuserUser(isSuperuser)) {
22
+ return true;
23
+ }
24
+ return Boolean(buttonPerms?.includes(permissionKey));
25
+ }
26
+
27
+ /**
28
+ * 按当前用户 `button_perms`(来自 fetchUserInfo)判断是否渲染子节点。
29
+ * 超级用户(is_superuser)视为拥有全部按钮权限。
30
+ */
31
+ const PermissionFilter: React.FC<IPermissionFilterProps> = ({
32
+ permissionKey,
33
+ children,
34
+ fallback = null,
35
+ }) => {
36
+ const { initialState } = useModel('@@initialState');
37
+ const currentUser = initialState?.currentUser;
38
+
39
+ const allowed = hasButtonPermission(
40
+ permissionKey,
41
+ currentUser?.button_perms,
42
+ currentUser?.is_superuser,
43
+ );
44
+
45
+ if (!allowed) {
46
+ return <>{fallback}</>;
47
+ }
48
+ return <>{children}</>;
49
+ };
50
+
51
+ export default PermissionFilter;
@@ -42,8 +42,17 @@ interface IMenuItem {
42
42
 
43
43
  // TODO: 临时假数据,后续接入真实登录态
44
44
  const MOCK_USER: Partial<IUserInfo> = {
45
+ id: 0,
46
+ name: 'Test User',
45
47
  user_name: 'Test User',
46
48
  username: 'test@micous.com',
49
+ email: 'test@micous.com',
50
+ avatar: '',
51
+ app_perms: [],
52
+ region_perms: [],
53
+ menu_perms: [],
54
+ button_perms: [],
55
+ is_superuser: true,
47
56
  };
48
57
 
49
58
  export const AvatarName: React.FC<{ userName?: string }> = ({ userName }) => {
@@ -371,7 +380,7 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
371
380
  </Avatar>
372
381
  </div>
373
382
  )}
374
- <AvatarName userName={currentUser?.user_name} />
383
+ <AvatarName userName={currentUser?.user_name || currentUser?.email} />
375
384
  <IconFont
376
385
  type="webcs-outline_down1"
377
386
  className="avatar-dropdown-icon"
@@ -0,0 +1,76 @@
1
+ import { setStoredTenantCode } from '@/common/auth/tenant';
2
+ import IconFont from '@/components/IconFont';
3
+ import useTenant from '@/hooks/useTenant';
4
+ import { Menu } from '@mico-platform/ui';
5
+ import { useModel } from '@umijs/max';
6
+ import React, { useMemo } from 'react';
7
+ import HeaderDropdown from '../HeaderDropdown';
8
+ import './tenant-dropdown.less';
9
+
10
+ const MenuItem = Menu.Item;
11
+
12
+ export const TenantDropdown: React.FC = () => {
13
+ const { isMultiTenant, tenants } = useTenant();
14
+ const { initialState } = useModel('@@initialState');
15
+ const currentUser = initialState?.currentUser;
16
+ const currentTenantCode = currentUser?.current_tenant;
17
+
18
+ // 仅保留 app_perms 中授权的租户(按 app_perm.key === tenant.code 匹配)
19
+ const allowedTenants = useMemo(() => {
20
+ const allowedCodes = new Set(
21
+ (currentUser?.app_perms ?? []).map((p) => p.key),
22
+ );
23
+ return tenants.filter((t) => allowedCodes.has(t.code));
24
+ }, [tenants, currentUser?.app_perms]);
25
+
26
+ const currentTenantName =
27
+ allowedTenants.find((t) => t.code === currentTenantCode)?.name || '';
28
+
29
+ if (!isMultiTenant || allowedTenants.length === 0) {
30
+ return null;
31
+ }
32
+
33
+ const onClickTenant = (code: string) => {
34
+ if (!code || code === currentTenantCode) {
35
+ return;
36
+ }
37
+ setStoredTenantCode(code);
38
+ // 参考语言/时区切换:整页 reload,确保菜单、路由、权限全部重建
39
+ window.location.reload();
40
+ };
41
+
42
+ return (
43
+ <HeaderDropdown
44
+ position="br"
45
+ triggerProps={{ popupAlign: { bottom: [0, 20] } }}
46
+ droplist={
47
+ <Menu className="tenant-dropdown-menu">
48
+ {allowedTenants.map((tenant) => {
49
+ const selected = tenant.code === currentTenantCode;
50
+ return (
51
+ <MenuItem
52
+ key={tenant.code}
53
+ onClick={() => onClickTenant(tenant.code)}
54
+ >
55
+ <span className={selected ? 'selected-label' : ''}>
56
+ {tenant.name}
57
+ </span>
58
+ </MenuItem>
59
+ );
60
+ })}
61
+ </Menu>
62
+ }
63
+ >
64
+ <div className="flex items-center tenant-dropdown-trigger">
65
+ <span className="tenant-name">{currentTenantName}</span>
66
+ <IconFont
67
+ type="webcs-outline_down1"
68
+ className="tenant-dropdown-icon"
69
+ fontSize={12}
70
+ />
71
+ </div>
72
+ </HeaderDropdown>
73
+ );
74
+ };
75
+
76
+ export default TenantDropdown;
@@ -1,2 +1,3 @@
1
1
  export { AvatarDropdown, AvatarName } from './AvatarDropdown';
2
2
  export type { GlobalHeaderRightProps } from './AvatarDropdown';
3
+ export { TenantDropdown } from './TenantDropdown';
@@ -0,0 +1,48 @@
1
+ @import '@mico-platform/theme/variables';
2
+
3
+ @tenant-dropdown-width: 140px;
4
+
5
+ .tenant-dropdown-trigger {
6
+ display: inline-flex;
7
+ justify-content: space-between;
8
+ width: @tenant-dropdown-width;
9
+ background-color: @color-fill-2;
10
+ border-radius: @border-radius-button;
11
+ height: 32px;
12
+ padding: 1px 10px;
13
+ color: @color-text-1;
14
+ cursor: pointer;
15
+ box-sizing: border-box;
16
+ }
17
+
18
+ .tenant-dropdown-trigger:hover {
19
+ background: @color-fill-3;
20
+ }
21
+
22
+ .tenant-dropdown-trigger .tenant-name {
23
+ flex: 1;
24
+ min-width: 0;
25
+ overflow: hidden;
26
+ text-overflow: ellipsis;
27
+ white-space: nowrap;
28
+ }
29
+
30
+ .tenant-dropdown-trigger.arco-dropdown-popup-visible .arco-icon-down {
31
+ transform: rotate(180deg);
32
+ }
33
+
34
+ .tenant-dropdown-menu.arco-dropdown-menu {
35
+ width: @tenant-dropdown-width;
36
+ max-height: 320px;
37
+ overflow-y: auto;
38
+ box-sizing: border-box;
39
+ }
40
+
41
+ .tenant-dropdown-icon {
42
+ margin-left: 4px;
43
+ transition: transform 0.2s;
44
+ }
45
+
46
+ .tenant-dropdown-menu .selected-label {
47
+ color: @Brand1-6;
48
+ }
@@ -171,6 +171,7 @@ export const STORAGE_KEYS = {
171
171
  GROUPS: 'groups',
172
172
  TIMEZONE: 'mico-cs-timezone',
173
173
  TIMEZONE_REGION: 'mico-cs-timezone-region',
174
+ CURRENT_TENANT: 'mico-current-tenant',
174
175
  } as const;
175
176
 
176
177
  /**
@@ -1,3 +1,4 @@
1
1
  export { useAuth } from './useAuth';
2
2
  export { useMenu } from './useMenu';
3
+ export { useTenant } from './useTenant';
3
4
  export { useTheme } from './useTheme';
@@ -22,6 +22,8 @@ interface UseMenuStateReturn {
22
22
  isCollapsed: boolean,
23
23
  type: 'clickTrigger' | 'responsive',
24
24
  ) => void;
25
+ handleMouseEnter: () => void;
26
+ handleMouseLeave: () => void;
25
27
  setOpenKeys: (keys: string[]) => void;
26
28
  }
27
29
 
@@ -98,6 +100,20 @@ export const useMenuState = ({
98
100
  [triggerMode],
99
101
  );
100
102
 
103
+ // hover 模式下鼠标进入自动展开
104
+ const handleMouseEnter = useCallback(() => {
105
+ if (triggerMode === ETriggerMode.HOVER_TRIGGER) {
106
+ setCollapsed(false);
107
+ }
108
+ }, [triggerMode]);
109
+
110
+ // hover 模式下鼠标离开自动收起
111
+ const handleMouseLeave = useCallback(() => {
112
+ if (triggerMode === ETriggerMode.HOVER_TRIGGER) {
113
+ setCollapsed(true);
114
+ }
115
+ }, [triggerMode]);
116
+
101
117
  return {
102
118
  selectedKeys,
103
119
  openKeys,
@@ -105,6 +121,8 @@ export const useMenuState = ({
105
121
  triggerMode,
106
122
  handleClickMenuItem,
107
123
  handleCollapsed,
124
+ handleMouseEnter,
125
+ handleMouseLeave,
108
126
  setOpenKeys,
109
127
  };
110
128
  };
@@ -0,0 +1,41 @@
1
+ import { ETenantModel, type ITenantItem } from '@/common/auth/type';
2
+ import { useModel } from '@umijs/max';
3
+
4
+ export interface IUseTenantResult {
5
+ /** 是否多租户模式 */
6
+ isMultiTenant: boolean;
7
+ /** 当前租户模式原始值(1 单租户,2 多租户) */
8
+ tenantModel: number;
9
+ /** 用户所属租户列表 */
10
+ tenants: ITenantItem[];
11
+ }
12
+
13
+ /**
14
+ * 租户模式 Hook
15
+ *
16
+ * 从 initialState.currentUser 读取 tenant_model / tenant 字段,
17
+ * 对外暴露语义化的 isMultiTenant 标志和 tenants 列表,供业务组件直接使用。
18
+ *
19
+ * @example
20
+ * const { isMultiTenant, tenants } = useTenant();
21
+ * if (isMultiTenant) {
22
+ * // 渲染租户选择器
23
+ * }
24
+ */
25
+ export function useTenant(): IUseTenantResult {
26
+ const { initialState } = useModel('@@initialState');
27
+ const currentUser = initialState?.currentUser;
28
+
29
+ const tenantModel = currentUser?.tenant_model ?? ETenantModel.Single;
30
+ const tenants = currentUser?.tenant ?? [];
31
+ const isMultiTenant =
32
+ currentUser?.isMultiTenant ?? tenantModel === ETenantModel.Multi;
33
+
34
+ return {
35
+ isMultiTenant,
36
+ tenantModel,
37
+ tenants,
38
+ };
39
+ }
40
+
41
+ export default useTenant;
@@ -1,4 +1,4 @@
1
- import { AvatarDropdown } from '@/components/RightContent';
1
+ import { AvatarDropdown, TenantDropdown } from '@/components/RightContent';
2
2
  import { DEFAULT_NAME } from '@/constants';
3
3
  import { Layout, Space } from '@mico-platform/ui';
4
4
  import React from 'react';
@@ -28,6 +28,9 @@ const LayoutHeader: React.FC = () => {
28
28
  {theme === 'dark' ? <IconSunFill /> : <IconMoonFill />}
29
29
  </div> */}
30
30
 
31
+ {/* Tenant switcher (仅多租户模式可见) */}
32
+ <TenantDropdown />
33
+
31
34
  <span className="split-line" />
32
35
 
33
36
  {/* User info */}