generator-mico-cli 0.2.29 → 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 (83) hide show
  1. package/README.md +2 -0
  2. package/generators/micro-react/index.js +17 -1
  3. package/generators/micro-react/templates/CICD/start_dev.sh +11 -0
  4. package/generators/micro-react/templates/CICD/start_local.sh +9 -0
  5. package/generators/micro-react/templates/CICD/start_prod.sh +13 -0
  6. package/generators/micro-react/templates/CICD/start_test.sh +11 -0
  7. package/generators/micro-react/templates/apps/layout/config/config.dev.ts +9 -3
  8. package/generators/micro-react/templates/apps/layout/config/config.prod.development.ts +10 -0
  9. package/generators/micro-react/templates/apps/layout/config/config.prod.testing.ts +10 -0
  10. package/generators/micro-react/templates/apps/layout/config/config.prod.ts +12 -0
  11. package/generators/micro-react/templates/apps/layout/docs/feature-/345/233/275/351/231/205/345/214/226.md +121 -0
  12. 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
  13. package/generators/micro-react/templates/apps/layout/mock/menus.ts +14 -0
  14. package/generators/micro-react/templates/apps/layout/mock/pages.ts +22 -2
  15. package/generators/micro-react/templates/apps/layout/package.json +3 -2
  16. package/generators/micro-react/templates/apps/layout/src/app.tsx +58 -5
  17. package/generators/micro-react/templates/apps/layout/src/common/auth/index.ts +3 -0
  18. package/generators/micro-react/templates/apps/layout/src/common/auth/tenant.ts +25 -0
  19. package/generators/micro-react/templates/apps/layout/src/common/auth/type.ts +28 -2
  20. package/generators/micro-react/templates/apps/layout/src/common/intl/formatLayoutMessage.ts +30 -0
  21. package/generators/micro-react/templates/apps/layout/src/common/intl/index.ts +6 -0
  22. package/generators/micro-react/templates/apps/layout/src/common/intl/intlRuntime.ts +14 -0
  23. package/generators/micro-react/templates/apps/layout/src/common/intl/localeMapping.ts +30 -0
  24. package/generators/micro-react/templates/apps/layout/src/common/intl/types.ts +14 -0
  25. package/generators/micro-react/templates/apps/layout/src/common/intl/useLayoutIntl.ts +40 -0
  26. package/generators/micro-react/templates/apps/layout/src/common/logger.ts +3 -4
  27. package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +27 -0
  28. package/generators/micro-react/templates/apps/layout/src/common/micro/types.ts +23 -0
  29. package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +74 -15
  30. package/generators/micro-react/templates/apps/layout/src/common/request/token-refresh.ts +2 -0
  31. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +28 -6
  32. package/generators/micro-react/templates/apps/layout/src/components/RightContent/TenantDropdown.tsx +76 -0
  33. package/generators/micro-react/templates/apps/layout/src/components/RightContent/index.ts +1 -0
  34. package/generators/micro-react/templates/apps/layout/src/components/RightContent/tenant-dropdown.less +48 -0
  35. package/generators/micro-react/templates/apps/layout/src/constants/index.ts +1 -0
  36. package/generators/micro-react/templates/apps/layout/src/hooks/index.ts +1 -0
  37. package/generators/micro-react/templates/apps/layout/src/hooks/useMenuState.ts +18 -0
  38. package/generators/micro-react/templates/apps/layout/src/hooks/useTenant.ts +41 -0
  39. package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.tsx +4 -1
  40. package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +18 -6
  41. package/generators/micro-react/templates/apps/layout/src/locales/en-US.ts +11 -0
  42. package/generators/micro-react/templates/apps/layout/src/locales/zh-CN.ts +10 -0
  43. package/generators/micro-react/templates/apps/layout/src/pages/Home/index.less +27 -0
  44. package/generators/micro-react/templates/apps/layout/src/pages/Home/index.tsx +108 -12
  45. package/generators/micro-react/templates/apps/layout/src/requestErrorConfig.ts +2 -1
  46. package/generators/micro-react/templates/apps/layout/src/services/user.ts +53 -2
  47. package/generators/micro-react/templates/apps/layout/typings.d.ts +16 -0
  48. package/generators/micro-react/templates/docs/package-shared.md +189 -0
  49. package/generators/micro-react/templates/package.json +1 -1
  50. package/generators/micro-react/templates/packages/common-intl/README.md +3 -1
  51. package/generators/micro-react/templates/packages/common-intl/package.json +1 -1
  52. package/generators/micro-react/templates/packages/common-intl/src/index.ts +4 -2
  53. package/generators/micro-react/templates/packages/common-intl/src/intl.ts +104 -8
  54. package/generators/micro-react/templates/packages/common-intl/src/umiLocaleBridge.ts +101 -0
  55. package/generators/micro-react/templates/packages/shared/README.md +120 -0
  56. package/generators/micro-react/templates/packages/shared/package.json +26 -0
  57. package/generators/micro-react/templates/packages/shared/services/common/index.ts +43 -0
  58. package/generators/micro-react/templates/packages/shared/services/index.ts +21 -0
  59. package/generators/micro-react/templates/packages/shared/services/request.ts +43 -0
  60. package/generators/micro-react/templates/packages/shared/timezone/index.ts +228 -0
  61. package/generators/micro-react/templates/packages/shared/tsconfig.json +20 -0
  62. package/generators/micro-react/templates/scripts/apply-sentry-plugin.ts +6 -1
  63. package/generators/micro-react/templates/turbo.json +9 -1
  64. package/generators/subapp-react/templates/homepage/config/config.ts +10 -0
  65. package/generators/subapp-react/templates/homepage/docs/feature-/345/233/275/351/231/205/345/214/226.md +124 -0
  66. package/generators/subapp-react/templates/homepage/package.json +2 -1
  67. package/generators/subapp-react/templates/homepage/src/app.tsx +100 -5
  68. package/generators/subapp-react/templates/homepage/src/common/intl/index.ts +15 -0
  69. package/generators/subapp-react/templates/homepage/src/common/intl/intlRuntime.ts +14 -0
  70. package/generators/subapp-react/templates/homepage/src/common/intl/localeMapping.ts +24 -0
  71. package/generators/subapp-react/templates/homepage/src/common/intl/subappIntlConfig.ts +28 -0
  72. package/generators/subapp-react/templates/homepage/src/common/intl/subappLocale.ts +18 -0
  73. package/generators/subapp-react/templates/homepage/src/common/intl/subappOwnIntl.ts +63 -0
  74. package/generators/subapp-react/templates/homepage/src/common/intl/types.ts +14 -0
  75. package/generators/subapp-react/templates/homepage/src/common/intl/useSubappIntl.ts +61 -0
  76. package/generators/subapp-react/templates/homepage/src/common/locale.ts +80 -0
  77. package/generators/subapp-react/templates/homepage/src/common/mainApp.ts +2 -0
  78. package/generators/subapp-react/templates/homepage/src/locales/en-US.ts +6 -0
  79. package/generators/subapp-react/templates/homepage/src/locales/zh-CN.ts +6 -0
  80. package/generators/subapp-react/templates/homepage/src/pages/index.less +10 -0
  81. package/generators/subapp-react/templates/homepage/src/pages/index.tsx +51 -0
  82. package/generators/subapp-react/templates/homepage/typings.d.ts +12 -0
  83. package/package.json +1 -1
@@ -8,19 +8,43 @@ export const UID = 'uid';
8
8
  /** 微应用模式下主应用传递的环境标识 */
9
9
  export const MICRO_ENV_KEY = 'micro_env';
10
10
 
11
+ export interface IAppPerms {
12
+ key: string;
13
+ value: string;
14
+ }
15
+
16
+ export interface ITenantItem {
17
+ /** 租户名 */
18
+ name: string;
19
+ /** 租户 code */
20
+ code: string;
21
+ }
22
+
23
+ /** 租户模式枚举 */
24
+ export enum ETenantModel {
25
+ Single = 1,
26
+ Multi = 2,
27
+ }
28
+
11
29
  /**
12
- * OpenAPI 8 — GET /user/info/ 响应 `data`(与 Apifox 导出 8 一致)
30
+ * OpenAPI — GET /user/info/ 响应 `data`
13
31
  */
14
32
  export interface IUserInfoApiData {
15
33
  id: number;
16
34
  avatar: string;
17
35
  email: string;
18
36
  name: string;
19
- app_perms: string[];
37
+ app_perms: IAppPerms[];
20
38
  region_perms: string[];
21
39
  menu_perms: string[];
22
40
  button_perms: string[];
23
41
  is_superuser: boolean;
42
+ /** 租户模式:1 单租户,2 多租户 */
43
+ tenant_model: number;
44
+ /** 用户所属租户列表 */
45
+ tenant: ITenantItem[];
46
+ /** 当前租户 code,对应 ITenantItem.code */
47
+ current_tenant: string;
24
48
  }
25
49
 
26
50
  /**
@@ -32,4 +56,6 @@ export interface IUserInfo extends IUserInfoApiData {
32
56
  user_name: string;
33
57
  /** 登录名/账号展示,可与 `name` 或 `email` 一致 */
34
58
  username: string;
59
+ /** 是否多租户模式(tenant_model === 2 的语义化快捷字段) */
60
+ isMultiTenant: boolean;
35
61
  }
@@ -0,0 +1,30 @@
1
+ import { i18n } from '<%= packageScope %>/common-intl';
2
+ import { getIntl } from '@umijs/max';
3
+ import { isCommonIntlEnabled } from './intlRuntime';
4
+
5
+ /**
6
+ * 非组件上下文(如 request 拦截器、工具函数)的国际化方法。
7
+ *
8
+ * 与 useLayoutIntl.formatMessage 逻辑一致:
9
+ * - commonIntl 未启用 → 直接走 Umi getIntl()
10
+ * - commonIntl 启用 → 远程 i18n() 优先,返回 defaultMessage 时回退到 Umi
11
+ */
12
+ export function formatLayoutMessage(
13
+ descriptor: { id: string; defaultMessage?: string },
14
+ values?: Record<string, string | number | boolean | Date | null | undefined>,
15
+ ): string {
16
+ const umiIntl = getIntl();
17
+ const dm = descriptor.defaultMessage ?? descriptor.id;
18
+
19
+ if (!isCommonIntlEnabled()) {
20
+ return umiIntl.formatMessage(descriptor, values);
21
+ }
22
+
23
+ const fromIntl = i18n({ key: descriptor.id, defaultMessage: dm });
24
+ const fromUmi = umiIntl.formatMessage(descriptor, values);
25
+
26
+ if (fromIntl === dm && fromUmi !== fromIntl) {
27
+ return fromUmi || dm;
28
+ }
29
+ return fromIntl;
30
+ }
@@ -0,0 +1,6 @@
1
+ export { intl } from '<%= packageScope %>/common-intl';
2
+ export { getCommonIntlConfig, isCommonIntlEnabled } from './intlRuntime';
3
+ export { ilangToUmiLocale, umiLocaleToILang } from './localeMapping';
4
+ export type { IMicoConfigCommonIntl } from './types';
5
+ export { useLayoutIntl } from './useLayoutIntl';
6
+ export { formatLayoutMessage } from './formatLayoutMessage';
@@ -0,0 +1,14 @@
1
+ import type { IMicoConfigCommonIntl } from './types';
2
+
3
+ /**
4
+ * 是否启用多语言中台拉取:以 __MICO_CONFIG__.intl 或 commonIntl 含有效 tag、app_name 为准
5
+ */
6
+ export function isCommonIntlEnabled(): boolean {
7
+ const c = getCommonIntlConfig();
8
+ return !!(c?.tag && c?.app_name);
9
+ }
10
+
11
+ export function getCommonIntlConfig(): IMicoConfigCommonIntl | undefined {
12
+ const w = window.__MICO_CONFIG__;
13
+ return w?.intl ?? w?.commonIntl;
14
+ }
@@ -0,0 +1,30 @@
1
+ import { LANG, type ILang } from '<%= packageScope %>/common-intl';
2
+ import type { SupportedLocale } from '@/common/locale';
3
+ import { LOCALE } from '@/common/locale';
4
+
5
+ /** 与 packages/common-intl/src/umiLocaleBridge.ts 保持一致 */
6
+ const UMI_TO_ILANG: Record<SupportedLocale, ILang> = {
7
+ [LOCALE.ZH_CN]: LANG.ZH_CN,
8
+ [LOCALE.EN_US]: LANG.EN,
9
+ };
10
+
11
+ /**
12
+ * Umi 权威 locale → common-intl 中台 / 缓存键(ILang)
13
+ */
14
+ export function umiLocaleToILang(locale: SupportedLocale): ILang {
15
+ return UMI_TO_ILANG[locale] ?? LANG.EN;
16
+ }
17
+
18
+ const ILANG_TO_UMI: Partial<Record<ILang, SupportedLocale>> = {
19
+ [LANG.ZH_CN]: LOCALE.ZH_CN,
20
+ [LANG.EN]: LOCALE.EN_US,
21
+ [LANG.AR]: LOCALE.EN_US,
22
+ [LANG.TR]: LOCALE.EN_US,
23
+ };
24
+
25
+ /**
26
+ * ILang → Umi 存储用 locale(layout 仅支持 zh-CN / en-US 时,其余语系落到 en-US)
27
+ */
28
+ export function ilangToUmiLocale(lang: ILang): SupportedLocale {
29
+ return ILANG_TO_UMI[lang] ?? LOCALE.EN_US;
30
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * layout 运行时 common-intl 配置(见 window.__MICO_CONFIG__.intl,兼容 commonIntl)
3
+ */
4
+ export interface IMicoConfigCommonIntl {
5
+ /** 多语言中台 tag */
6
+ tag: string;
7
+ app_name: string;
8
+ indexedDBParams?: {
9
+ dbName?: string;
10
+ dbVersion?: number;
11
+ storeName?: string;
12
+ keyPathKey?: string;
13
+ };
14
+ }
@@ -0,0 +1,40 @@
1
+ import { i18n, intl } from '<%= packageScope %>/common-intl';
2
+ import { useIntl } from '@umijs/max';
3
+ import { getCurrentLocale } from '@/common/locale';
4
+ import { isCommonIntlEnabled } from './intlRuntime';
5
+
6
+ /**
7
+ * layout 统一国际化 Hook:未配置 commonIntl 时仅 Umi;配置后 common-intl 优先,无命中时 Umi 兜底。
8
+ */
9
+ export function useLayoutIntl() {
10
+ const umiIntl = useIntl();
11
+ const commonIntlEnabled = isCommonIntlEnabled();
12
+ const locale = getCurrentLocale();
13
+
14
+ const formatMessage = (
15
+ descriptor: { id: string; defaultMessage?: string },
16
+ values?: Record<string, string | number | boolean | Date | null | undefined>,
17
+ ): string => {
18
+ if (!commonIntlEnabled) {
19
+ return umiIntl.formatMessage(descriptor, values);
20
+ }
21
+ const dm = descriptor.defaultMessage ?? descriptor.id;
22
+ const fromIntl = i18n({
23
+ key: descriptor.id,
24
+ defaultMessage: dm,
25
+ });
26
+ const fromUmi = umiIntl.formatMessage(descriptor, values);
27
+ if (fromIntl === dm && fromUmi !== fromIntl) {
28
+ return fromUmi;
29
+ }
30
+ return fromIntl;
31
+ };
32
+
33
+ return {
34
+ commonIntlEnabled,
35
+ locale,
36
+ formatMessage,
37
+ /** 仅 common-intl 启用时有值 */
38
+ intl: commonIntlEnabled ? intl : undefined,
39
+ };
40
+ }
@@ -15,18 +15,17 @@ const noop = (): void => {};
15
15
  * 使用 bind 绑定前缀,保持控制台显示正确的调用位置
16
16
  */
17
17
  const createLogger = (prefix: string) => {
18
+ const formattedPrefix = `[${prefix}]`;
18
19
  if (!isDev) {
19
- // 生产环境返回空操作
20
+ // 生产环境返回空操作,但 error 仍然输出(便于生产排查)
20
21
  return {
21
22
  log: noop,
22
23
  info: noop,
23
24
  warn: noop,
24
- error: noop,
25
+ error: console.error.bind(console, new Error(formattedPrefix)),
25
26
  };
26
27
  }
27
28
 
28
- const formattedPrefix = `[${prefix}]`;
29
-
30
29
  return {
31
30
  log: console.log.bind(console, formattedPrefix),
32
31
  info: console.info.bind(console, formattedPrefix),
@@ -215,6 +215,33 @@ declare global {
215
215
  disableAuth?: boolean;
216
216
  /** 获取时区列表的 API 地址 */
217
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
+ };
218
245
  [key: string]: unknown;
219
246
  };
220
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
  }
@@ -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';
@@ -75,16 +76,24 @@ const MicroAppLoader: React.FC<MicroAppLoaderProps> = ({
75
76
  : 'production';
76
77
  }, []);
77
78
 
78
- // 构建传递给子应用的 props
79
+ // 构建传递给子应用的 props(用户信息以 initialState.currentUser 为准,token 仍读存储)
79
80
  const buildProps = useCallback(() => {
80
81
  const authInfo = getAuthInfo();
81
- 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 = {
82
91
  mainApp: '<%= projectName %>',
83
92
  env,
84
93
  authToken: authInfo.token,
85
- uid: authInfo.uid,
86
- avatar: authInfo.avatar,
87
- nickname: authInfo.nickname,
94
+ uid: currentUser ? String(currentUser.id) : authInfo.uid,
95
+ avatar: currentUser?.avatar ?? authInfo.avatar,
96
+ nickname: displayName || authInfo.nickname,
88
97
  base: normalizeMicroAppBase(base || '/'),
89
98
  // 传递当前路由路径,让子应用进行内部路由切换
90
99
  routePath,
@@ -94,9 +103,22 @@ const MicroAppLoader: React.FC<MicroAppLoaderProps> = ({
94
103
  locale: getCurrentLocale(),
95
104
  // 与 fetchUserInfo 一致:子应用用于按钮级权限(如 PermissionFilter)
96
105
  button_perms: currentUser?.button_perms ?? [],
106
+ menu_perms: currentUser?.menu_perms ?? [],
107
+ region_perms: currentUser?.region_perms ?? [],
97
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,
98
117
  };
99
- }, [base, env, routePath, currentUser]);
118
+ const { request: _request, ...loggableProps } = props;
119
+ console.log('[MicroAppLoader] buildProps', { appName, ...loggableProps });
120
+ return props;
121
+ }, [base, env, routePath, currentUser, appName]);
100
122
 
101
123
  // ref 持有最新的 buildProps,避免加载 effect 依赖它导致子应用被重载
102
124
  const buildPropsRef = useRef(buildProps);
@@ -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
  };