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
@@ -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 */}
@@ -140,19 +140,31 @@ const LayoutMenu: React.FC<LayoutMenuProps> = () => {
140
140
  collapsed,
141
141
  handleClickMenuItem,
142
142
  handleCollapsed,
143
+ handleMouseEnter,
144
+ handleMouseLeave,
143
145
  setOpenKeys,
144
146
  } = useMenuState({ menuItems });
145
147
 
146
148
  useEffect(() => {
147
- document.documentElement.style.setProperty(
148
- '--sider-width',
149
- collapsed ? '48px' : '200px',
150
- );
149
+ const layoutContent = document.querySelector('.layout-content');
150
+ if (layoutContent) {
151
+ (layoutContent as HTMLElement).style.setProperty(
152
+ '--sider-width',
153
+ collapsed ? '48px' : '200px',
154
+ );
155
+ }
156
+ }, [collapsed]);
151
157
 
158
+ useEffect(() => {
159
+ const sider = siderRef.current;
160
+ if (!sider) return;
161
+ sider.addEventListener('mouseenter', handleMouseEnter);
162
+ sider.addEventListener('mouseleave', handleMouseLeave);
152
163
  return () => {
153
- document.documentElement.style.removeProperty('--sider-width');
164
+ sider.removeEventListener('mouseenter', handleMouseEnter);
165
+ sider.removeEventListener('mouseleave', handleMouseLeave);
154
166
  };
155
- }, [collapsed]);
167
+ }, [handleMouseEnter, handleMouseLeave]);
156
168
 
157
169
  // 点击触发按钮图标
158
170
  const clickTriggerBtnIcon = collapsed
@@ -38,6 +38,17 @@ export default {
38
38
  'cs_web_menu_permission_management': 'Permission Management',
39
39
  'cs_web_menu_group_management': 'Group Management',
40
40
 
41
+ // SSO auth failure modal
42
+ 'sso.auth.failure.modal.title': 'Login Required',
43
+ 'sso.auth.failure.modal.content':
44
+ 'Auto login failed. Would you like to try logging in again?',
45
+ 'sso.auth.failure.modal.ok': 'Re-login',
46
+ 'sso.auth.failure.modal.cancel': 'Cancel',
47
+ sso_auth_failure_modal_title: 'Login Required',
48
+ sso_auth_failure_modal_content: 'Auto login failed. Would you like to try logging in again?',
49
+ sso_auth_failure_modal_ok: 'Re-login',
50
+ sso_auth_failure_modal_cancel: 'Cancel',
51
+
41
52
  // AvatarDropdown
42
53
  'avatar.language': 'Language',
43
54
  'avatar.language.zh_CN': 'Simplified Chinese',
@@ -36,6 +36,16 @@ export default {
36
36
  'cs_web_menu_permission_management': '权限管理',
37
37
  'cs_web_menu_group_management': '小组管理',
38
38
 
39
+ // SSO 认证失败弹框
40
+ 'sso.auth.failure.modal.title': '登录提示',
41
+ 'sso.auth.failure.modal.content': '自动登录失败,是否重新尝试登录?',
42
+ 'sso.auth.failure.modal.ok': '重新登录',
43
+ 'sso.auth.failure.modal.cancel': '取消',
44
+ sso_auth_failure_modal_title: '登录提示',
45
+ sso_auth_failure_modal_content: '自动登录失败,是否重新尝试登录?',
46
+ sso_auth_failure_modal_ok: '重新登录',
47
+ sso_auth_failure_modal_cancel: '取消',
48
+
39
49
  // AvatarDropdown
40
50
  'avatar.language': '语言',
41
51
  'avatar.language.zh_CN': '简体中文',
@@ -5,7 +5,34 @@
5
5
  color: @color-text-1;
6
6
  }
7
7
 
8
+ .title {
9
+ margin: 0 0 12px;
10
+ font-size: 22px;
11
+ font-weight: 600;
12
+ }
13
+
14
+ .hint {
15
+ margin: 0 0 8px;
16
+ font-size: 14px;
17
+ line-height: 1.6;
18
+ color: @color-text-2;
19
+ }
20
+
21
+ .meta {
22
+ margin: 0;
23
+ font-size: 12px;
24
+ font-family: ui-monospace, monospace;
25
+ color: @color-text-3;
26
+ }
27
+
8
28
  .demoCard {
9
29
  margin-top: 16px;
10
30
  max-width: 720px;
11
31
  }
32
+
33
+ .tzDemoList {
34
+ width: 100%;
35
+ font-family: ui-monospace, monospace;
36
+ font-size: 12px;
37
+ line-height: 1.8;
38
+ }
@@ -1,29 +1,87 @@
1
1
  import PermissionFilter from '@/components/PermissionFilter';
2
+ import { useLayoutIntl } from '@/common/intl';
2
3
  import { Alert, Button, Card, Space, Typography } from '@mico-platform/ui';
3
- import { useIntl } from '@umijs/max';
4
- import React from 'react';
4
+ import type { FC } from 'react';
5
+ import {
6
+ TIMEZONE,
7
+ formatTimestampToDateTime,
8
+ formatTimestampToDateTimeWithTimezone,
9
+ getShortcutDateRange,
10
+ parseDateInTimezone,
11
+ utcOffsetToTimeZoneStr,
12
+ utcOffsetToTimezone,
13
+ } from '<%= packageScope %>/shared/timezone';
5
14
  import styles from './index.less';
6
15
 
7
16
  const { Paragraph, Text } = Typography;
8
17
 
18
+ /**
19
+ * shared/timezone 示例:所有函数依赖 app.tsx 顶层的 `configureTimezone({ getTimezone })` 注入。
20
+ * 这里用一个固定时间戳做确定性展示;业务实际使用时通常传 `Date.now()` 或后端下发值。
21
+ */
22
+ const DEMO_TIMESTAMP = 1730721621000;
23
+ const DEMO_DATE_STR = '2025-11-04 16:20:21';
24
+
9
25
  /** 与 mock GET /user/info/ button_perms 中的示例 key 对齐 */
10
26
  const HOME_BUTTON_PERM_KEY = 'cs_web_btn_home_demo';
11
27
 
12
- const HomePage: React.FC = () => {
13
- const intl = useIntl();
28
+ /**
29
+ * 国际化示例:通过 `useLayoutIntl` 适配层统一调用。
30
+ * - 未配置 `__MICO_CONFIG__.commonIntl`:等同 `useIntl().formatMessage`,读本地 `locales/*`
31
+ * - 已配置:优先 <%= packageScope %>/common-intl 的 `i18n()`,无命中再 Umi
32
+ */
33
+ const HomePage: FC = () => {
34
+ const { formatMessage, commonIntlEnabled, locale } = useLayoutIntl();
35
+
36
+ // shared/timezone 示例输出
37
+ const tzDemo = {
38
+ utcOffset8: utcOffsetToTimezone(8),
39
+ utcOffsetMinus5: utcOffsetToTimezone(-5),
40
+ iana8: utcOffsetToTimeZoneStr(8),
41
+ parsed: parseDateInTimezone(DEMO_DATE_STR).format('YYYY-MM-DD HH:mm:ss Z'),
42
+ thisWeek: (() => {
43
+ const [start, end] = getShortcutDateRange('week');
44
+ return `${start.format('MM-DD')} ~ ${end.format('MM-DD')}`;
45
+ })(),
46
+ formattedDemo: formatTimestampToDateTime(DEMO_TIMESTAMP),
47
+ formattedWithTz: formatTimestampToDateTimeWithTimezone(
48
+ DEMO_TIMESTAMP,
49
+ TIMEZONE['UTC+8'],
50
+ ),
51
+ };
14
52
 
15
53
  return (
16
54
  <div className={styles.container}>
17
- <Typography.Title heading={4}>
18
- {intl.formatMessage({ id: 'page.home.title' })}
19
- </Typography.Title>
55
+ <h1 className={styles.title}>
56
+ {
57
+ formatMessage({
58
+ id: 'page.home.title',
59
+ defaultMessage: '首页',
60
+ })
61
+ }
62
+ </h1>
63
+ <p className={styles.hint}>
64
+ common-intl 优先样例:{formatMessage({
65
+ id: 'cs_web_workbench_userlist_chat',
66
+ defaultMessage: '看到该文案代表未启用 commonIntl 或中台无该 key',
67
+ })}
68
+ </p>
69
+ <p className={styles.hint}>
70
+ Umi 兜底样例:{formatMessage({
71
+ id: 'page.home.title',
72
+ defaultMessage: '看到该文案代表仅 Umi 或兜底路径',
73
+ })}
74
+ </p>
75
+ <p className={styles.meta}>
76
+ commonIntlEnabled: {String(commonIntlEnabled)} · locale: {locale}
77
+ </p>
20
78
 
21
79
  <Card
22
80
  className={styles.demoCard}
23
- title={intl.formatMessage({ id: 'page.home.permissionDemo.title' })}
81
+ title={formatMessage({ id: 'page.home.permissionDemo.title' })}
24
82
  >
25
83
  <Paragraph type="secondary">
26
- {intl.formatMessage(
84
+ {formatMessage(
27
85
  { id: 'page.home.permissionDemo.desc' },
28
86
  { key: HOME_BUTTON_PERM_KEY },
29
87
  )}
@@ -34,10 +92,10 @@ const HomePage: React.FC = () => {
34
92
  fallback={
35
93
  <Alert
36
94
  type="warning"
37
- title={intl.formatMessage({
95
+ title={formatMessage({
38
96
  id: 'page.home.permissionDemo.fallbackTitle',
39
97
  })}
40
- content={intl.formatMessage(
98
+ content={formatMessage(
41
99
  { id: 'page.home.permissionDemo.noPerm' },
42
100
  { key: HOME_BUTTON_PERM_KEY },
43
101
  )}
@@ -46,7 +104,7 @@ const HomePage: React.FC = () => {
46
104
  >
47
105
  <Space>
48
106
  <Button type="primary" status="success">
49
- {intl.formatMessage({ id: 'page.home.permissionDemo.hasPerm' })}
107
+ {formatMessage({ id: 'page.home.permissionDemo.hasPerm' })}
50
108
  </Button>
51
109
  <Text type="success">
52
110
  ({HOME_BUTTON_PERM_KEY})
@@ -54,6 +112,44 @@ const HomePage: React.FC = () => {
54
112
  </Space>
55
113
  </PermissionFilter>
56
114
  </Card>
115
+
116
+ <Card
117
+ className={styles.demoCard}
118
+ title={<>shared/timezone 示例</>}
119
+ >
120
+ <Paragraph type="secondary">
121
+ 所有函数依赖 app.tsx 顶层的 <Text code>configureTimezone</Text>{' '}
122
+ 注入;时区实时取自 <Text code>@/common/helpers#getTimezone</Text>。
123
+ </Paragraph>
124
+
125
+ <Space direction="vertical" size="mini" className={styles.tzDemoList}>
126
+ <Text>
127
+ <Text bold>utcOffsetToTimezone(8):</Text> {tzDemo.utcOffset8}
128
+ </Text>
129
+ <Text>
130
+ <Text bold>utcOffsetToTimezone(-5):</Text> {tzDemo.utcOffsetMinus5}
131
+ </Text>
132
+ <Text>
133
+ <Text bold>utcOffsetToTimeZoneStr(8):</Text> {tzDemo.iana8}
134
+ </Text>
135
+ <Text>
136
+ <Text bold>parseDateInTimezone(&quot;{DEMO_DATE_STR}&quot;):</Text>{' '}
137
+ {tzDemo.parsed}
138
+ </Text>
139
+ <Text>
140
+ <Text bold>getShortcutDateRange(&quot;week&quot;):</Text>{' '}
141
+ {tzDemo.thisWeek}
142
+ </Text>
143
+ <Text>
144
+ <Text bold>formatTimestampToDateTime(demo):</Text>{' '}
145
+ {String(tzDemo.formattedDemo)}
146
+ </Text>
147
+ <Text>
148
+ <Text bold>formatTimestampToDateTimeWithTimezone(demo, UTC+8):</Text>{' '}
149
+ {String(tzDemo.formattedWithTz)}
150
+ </Text>
151
+ </Space>
152
+ </Card>
57
153
  </div>
58
154
  );
59
155
  };
@@ -131,8 +131,9 @@ export const errorConfig: RequestConfig = {
131
131
 
132
132
  // 根据 HTTP 状态码区分鉴权错误和业务错误
133
133
  if (status === 401 || status === 403) {
134
+ const notifyInfo = `鉴权失败,状态码:${status},可能登录态过期,请重新登录`;
134
135
  // 鉴权失败
135
- notifyHostError(createAuthError(errorMessage, status, 'http'));
136
+ notifyHostError(createAuthError(notifyInfo, status, 'http'));
136
137
  } else {
137
138
  // 其他业务错误
138
139
  notifyHostError(
@@ -1,4 +1,6 @@
1
+ import { getStoredTenantCode, setStoredTenantCode } from '@/common/auth/tenant';
1
2
  import type { IUserInfo, IUserInfoApiData } from '@/common/auth/type';
3
+ import { ETenantModel } from '@/common/auth/type';
2
4
  import { getCurrentLocale, type SupportedLocale } from '@/common/locale';
3
5
  import { request } from '@/common/request';
4
6
 
@@ -22,6 +24,7 @@ const USER_INFO_API = '/api/user/info/';
22
24
 
23
25
  function normalizeUserInfo(data: IUserInfoApiData): IUserInfo {
24
26
  const name = data.name || '';
27
+ const tenant_model = data.tenant_model ?? ETenantModel.Single;
25
28
  return {
26
29
  ...data,
27
30
  app_perms: Array.isArray(data.app_perms) ? data.app_perms : [],
@@ -29,6 +32,9 @@ function normalizeUserInfo(data: IUserInfoApiData): IUserInfo {
29
32
  menu_perms: Array.isArray(data.menu_perms) ? data.menu_perms : [],
30
33
  button_perms: Array.isArray(data.button_perms) ? data.button_perms : [],
31
34
  is_superuser: Boolean(data.is_superuser),
35
+ tenant_model,
36
+ tenant: Array.isArray(data.tenant) ? data.tenant : [],
37
+ isMultiTenant: tenant_model === ETenantModel.Multi,
32
38
  user_name: name,
33
39
  username: name || data.email || '',
34
40
  };
@@ -40,20 +46,65 @@ export interface IUserInfoResponse {
40
46
  msg: string;
41
47
  }
42
48
 
49
+ export interface ISimpleUser {
50
+ id: number;
51
+ email: string;
52
+ is_superuser: number;
53
+ is_active: number;
54
+ user_name: string;
55
+ }
56
+
57
+ export interface IAllUsersResponse {
58
+ code: number;
59
+ msg: string;
60
+ data: {
61
+ count: number;
62
+ results: ISimpleUser[];
63
+ };
64
+ }
65
+
66
+ export async function fetchAllUsers(params?: {
67
+ email?: string;
68
+ }): Promise<IAllUsersResponse> {
69
+ return request<IAllUsersResponse>('/api/user/all/', {
70
+ method: 'GET',
71
+ skipProxy: true,
72
+ params,
73
+ });
74
+ }
75
+
43
76
  /**
44
77
  * 获取用户信息
45
78
  * @returns 用户信息对象
46
79
  * @throws 当接口返回非 200 code 时抛出错误
47
80
  */
48
- export async function fetchUserInfo(): Promise<IUserInfo> {
81
+ export async function fetchUserInfo(params?: {
82
+ lang?: number;
83
+ tenant_code?: string;
84
+ }): Promise<IUserInfo> {
49
85
  const url = USER_INFO_API;
50
86
 
87
+ // 若未显式指定 tenant_code,则自动注入 localStorage 中的当前租户
88
+ // 单租户或首次进入时无值,后端会取默认租户并通过 current_tenant 回包
89
+ const tenantCode = params?.tenant_code ?? getStoredTenantCode() ?? undefined;
90
+ const requestParams = {
91
+ ...params,
92
+ ...(tenantCode ? { tenant_code: tenantCode } : {}),
93
+ };
94
+
51
95
  const response = await request<IUserInfoResponse>(url, {
52
96
  method: 'GET',
53
97
  skipProxy: true,
98
+ params: requestParams,
54
99
  });
55
100
  if (response.code === 200 && response.data) {
56
- return normalizeUserInfo(response.data);
101
+ const userInfo = normalizeUserInfo(response.data);
102
+ // 回写当前租户到本地存储,保证后续请求与 UI 展示一致
103
+ const currentTenantCode = userInfo.current_tenant;
104
+ if (currentTenantCode) {
105
+ setStoredTenantCode(currentTenantCode);
106
+ }
107
+ return userInfo;
57
108
  }
58
109
  throw new Error(response.msg || '获取用户信息失败');
59
110
  }
@@ -1 +1,17 @@
1
1
  import '@umijs/max/typings';
2
+
3
+ /**
4
+ * Window 上挂载的构建信息
5
+ */
6
+ declare global {
7
+ interface Window {
8
+ __MICO_BUILD__?: {
9
+ branchOrTag?: string;
10
+ gitCommit?: string;
11
+ version?: string;
12
+ buildTime?: string;
13
+ /** Jenkins 等 CI 的构建号 */
14
+ buildNumber?: string;
15
+ };
16
+ }
17
+ }
@@ -0,0 +1,189 @@
1
+ # shared 共享包使用说明
2
+
3
+ > 创建时间:2026-04-23
4
+
5
+ ## 功能概述
6
+
7
+ `<%= packageScope %>/shared` 是平台跨应用(主应用 + qiankun 子应用)复用的业务服务层骨架,当前对外暴露两个独立模块:
8
+
9
+ - **`./services`**
10
+ - `configureRequest` / `getRequest`:统一 request 注入器
11
+ - `common`:通用类型(分页、响应包装)与 `isSuccess`
12
+ - **`./timezone`**
13
+ - `TIMEZONE` / `TIMEZONE_HASH` / `TTimezone`:UTC 与 IANA 映射表
14
+ - `utcOffsetToTimezone` / `utcOffsetToTimeZoneStr`:纯转换
15
+ - `configureTimezone({ getTimezone })`:注入「当前 IANA 时区」获取器
16
+ - `parseDateInTimezone` / `getShortcutDateRange` / `formatTimestampToDateTime` / `formatTimestampToDateTimeWithTimezone`
17
+
18
+ 业务接口、业务常量、业务工具等由各项目按需新增,本包只定义骨架与扩展规范。
19
+
20
+ ## 包结构
21
+
22
+ ```
23
+ packages/shared/
24
+ ├── package.json # 仅导出 ./services 与 ./timezone
25
+ ├── tsconfig.json
26
+ ├── README.md
27
+ ├── services/
28
+ │ ├── index.ts # services 域唯一对外 barrel
29
+ │ ├── request.ts # request 注入器
30
+ │ └── common/
31
+ │ └── index.ts # 通用类型与工具
32
+ └── timezone/
33
+ └── index.ts # 时区工具 + 时区获取器
34
+ ```
35
+
36
+ ## 导出约定
37
+
38
+ | 约定 | 说明 |
39
+ |---|---|
40
+ | 一目录一入口 | 每个根目录(`services/`、`timezone/` 等)在 `exports` 中只对应一条入口 |
41
+ | 无根入口 | 不导出 `.`、不设置 `main`/`types`,消费方**不能** `import from '<%= packageScope %>/shared'` |
42
+ | 禁止深层路径 | 不允许 `./services/user`、`./services/common`、`./timezone/sub` 等子路径 |
43
+ | 同级扩展 | 新增共享模块时新建与 `services/`、`timezone/` 同级的目录 |
44
+
45
+ ## 消费方使用
46
+
47
+ ### 1. 初始化(必须,仅一次)
48
+
49
+ 主应用与子应用都在 `app.tsx` 顶层注入本应用的 `request`,模板已默认接好:
50
+
51
+ ```ts
52
+ // apps/layout/src/app.tsx
53
+ import { configureRequest } from '<%= packageScope %>/shared/services';
54
+ import { request as commonRequest } from './common/request';
55
+
56
+ configureRequest(commonRequest);
57
+ ```
58
+
59
+ ```ts
60
+ // apps/<subapp>/src/app.tsx
61
+ import { configureRequest } from '<%= packageScope %>/shared/services';
62
+ import { request } from './common/request';
63
+
64
+ // 子应用 request 内部已自动桥接主应用 / 独立 umi,
65
+ // 顶层注入即可,无需在 mount 里反复设置
66
+ configureRequest(request);
67
+ ```
68
+
69
+ > 子应用 `./common/request` 的实现会在 qiankun 模式下走 `getMainAppRequest()`,独立运行时走 `umiRequest`,因此顶层注入的是同一个稳定函数引用,运行期才决定具体走哪条链路。
70
+
71
+ 未初始化即调用 `getRequest()` 会抛出 `[shared/services] request 未初始化,请先调用 configureRequest()`,提示检查初始化时序。
72
+
73
+ ### 2. 业务代码调用
74
+
75
+ ```ts
76
+ import {
77
+ getRequest,
78
+ isSuccess,
79
+ type IPagedResponse,
80
+ } from '<%= packageScope %>/shared/services';
81
+
82
+ async function fetchUsers(): Promise<IPagedResponse<IUser>> {
83
+ const res = await getRequest()<{ code: number; data: IPagedResponse<IUser> }>(
84
+ '/api/user/',
85
+ { method: 'GET' },
86
+ );
87
+ if (!isSuccess(res)) throw new Error('fetch users failed');
88
+ return res.data;
89
+ }
90
+ ```
91
+
92
+ ### 3. 时区模块初始化
93
+
94
+ `./timezone` 不内置时区存储,由消费方注入「当前 IANA 时区获取器」(通常代理到主应用的时区管理模块):
95
+
96
+ ```ts
97
+ // apps/layout/src/app.tsx
98
+ import { configureTimezone } from '<%= packageScope %>/shared/timezone';
99
+ import { getTimezone } from '@/common/helpers';
100
+
101
+ configureTimezone({ getTimezone });
102
+ ```
103
+
104
+ 依赖前置:本模块以 `peerDependencies` 声明 dayjs。**消费方需在入口扩展三个插件**(`utc / timezone / customParseFormat`):
105
+
106
+ ```ts
107
+ import dayjs from 'dayjs';
108
+ import utc from 'dayjs/plugin/utc';
109
+ import timezone from 'dayjs/plugin/timezone';
110
+ import customParseFormat from 'dayjs/plugin/customParseFormat';
111
+ dayjs.extend(utc);
112
+ dayjs.extend(timezone);
113
+ dayjs.extend(customParseFormat);
114
+ ```
115
+
116
+ > 模板的 `apps/layout/src/common/helpers.ts` 已扩展 `utc / timezone`,仅需补 `customParseFormat`。
117
+
118
+ 业务调用示例:
119
+
120
+ ```ts
121
+ import {
122
+ TIMEZONE,
123
+ parseDateInTimezone,
124
+ getShortcutDateRange,
125
+ formatTimestampToDateTime,
126
+ formatTimestampToDateTimeWithTimezone,
127
+ } from '<%= packageScope %>/shared/timezone';
128
+
129
+ parseDateInTimezone('2025-11-04 16:20:21');
130
+ const [start, end] = getShortcutDateRange('week');
131
+ formatTimestampToDateTime(Date.now());
132
+ formatTimestampToDateTimeWithTimezone(Date.now(), TIMEZONE['UTC+8']);
133
+ ```
134
+
135
+ ## 扩展规范
136
+
137
+ ### 场景 A:新增业务接口(归属 services 域)
138
+
139
+ 1. 新建子目录 `services/<module>/`
140
+ 2. 在其中创建唯一对外入口 `index.ts`;如需细分,新增 `type.ts` 等文件但只由 `index.ts` re-export
141
+ 3. 在 `services/<module>/index.ts` 中:
142
+ - 用 `export type {...}` 对外暴露类型
143
+ - 用 `getRequest()` 调后端,导出业务函数
144
+ 4. 在 `services/index.ts` 追加一行聚合:`export * from './<module>';`
145
+ 5. **不要**改动 `package.json` 的 `exports`
146
+
147
+ ### 场景 B:新增非 services 共享模块(如 constants / utils)
148
+
149
+ 当一类共享内容明显**不属于** services 域,新建与 `services/` 同级的目录,作为全新独立对外模块:
150
+
151
+ 1. 在包根新建目录,例如 `constants/`
152
+ 2. 创建唯一对外入口 `constants/index.ts`
153
+ 3. 在 `package.json` 的 `exports` 中追加且仅追加一条:
154
+
155
+ ```json
156
+ "./constants": "./constants/index.ts"
157
+ ```
158
+
159
+ 4. 在 `package.json` 的 `files` 数组追加目录名 `"constants"`
160
+ 5. **禁止**新增任何 `./constants/<sub>` 深层路径
161
+
162
+ ## 设计决策
163
+
164
+ | 决策点 | 选择 | 理由 |
165
+ |---|---|---|
166
+ | 包定位 | 仅骨架,无业务实现 | 模板场景下各项目业务差异大,留空可避免强耦合业务假设 |
167
+ | request 注入器 | 运行时注入而非直接依赖某 request 包 | 主应用 / 子应用在 qiankun 模式下共享同一请求实例,注入模式可一次定义、多处复用 |
168
+ | 仅 `./services` 入口 | 不开放 `.` 根入口与深层路径 | 强约束消费方统一从命名空间入口导入,内部重构不影响外部契约 |
169
+ | 一目录一模块 | 同级目录都只暴露 `index.ts` | 保持"外部可见面 = exports 列表",依赖图清晰,重构自由度高 |
170
+ | 保留 `common` 内联于 services | 不独立成 `common/` 同级目录 | 当前只有分页和响应结果等通用类型,体量小;后续若通用工具增多再拆 |
171
+
172
+ ## 注意事项
173
+
174
+ - `configureRequest` 必须在任何业务调用**之前**执行。主应用与子应用模板均在 `app.tsx` 顶层注入;子应用注入的是本地的 `./common/request` 函数(运行期由它自动桥接主应用 / 独立 umi),所以无需在 `mount` 里重复注入 `props.request`。
175
+ - `RequestFn` 签名为 `<T = unknown>(url: string, options?: Record<string, unknown>) => Promise<T>`,各应用的 request 实例需兼容这一签名。
176
+ - 新增子目录时,如果消费方只依赖类型,也要通过 `services/index.ts` re-export,不要引导外部直接 import 内部 `type.ts`。
177
+ - 若后续需要支持多实例 request(例如不同域名的后端),可以在本包扩展成 `configureRequest(name, req)` + `getRequest(name)` 的多命名空间形式,当前版本按单实例设计。
178
+
179
+ ## 相关文件
180
+
181
+ | 文件 | 说明 |
182
+ |---|---|
183
+ | [packages/shared/package.json](../packages/shared/package.json) | 导出声明,约束外部可见面 |
184
+ | [packages/shared/services/index.ts](../packages/shared/services/index.ts) | 对外 barrel 与扩展规范注释 |
185
+ | [packages/shared/services/request.ts](../packages/shared/services/request.ts) | request 注入器实现 |
186
+ | [packages/shared/services/common/index.ts](../packages/shared/services/common/index.ts) | 通用类型与 `isSuccess` |
187
+ | [packages/shared/timezone/index.ts](../packages/shared/timezone/index.ts) | 时区常量、纯转换、注入器与格式化函数 |
188
+ | [packages/shared/README.md](../packages/shared/README.md) | 包内使用文档 |
189
+ | [apps/layout/src/common/helpers.ts](../apps/layout/src/common/helpers.ts) | 主应用时区管理实现(被 `configureTimezone` 注入) |
@@ -32,7 +32,7 @@
32
32
  "devDependencies": {
33
33
  "@commitlint/cli": "^19.5.0",
34
34
  "@commitlint/config-conventional": "^19.5.0",
35
- "@common-web/sentry": "^0.0.4",
35
+ "@common-web/sentry": "0.0.7",
36
36
  "@sentry/webpack-plugin": "^4.9.1",
37
37
  "@typescript-eslint/eslint-plugin": "^8.54.0",
38
38
  "@typescript-eslint/parser": "^8.54.0",
@@ -112,13 +112,15 @@ const intl = addIntl({
112
112
  export default intl;
113
113
  ```
114
114
 
115
+ > **注意:** 子应用使用独立 tag 模式时,必须移除子应用 config 文件中 `externals` 里的 `'<%= packageScope %>/common-intl': 'CommonIntl'` 配置,确保子应用打包自己的 `@common-web/common-intl` 副本,避免与主应用共享模块级状态。
116
+
115
117
  > `initIntl` 只管身份配置(tag、app_name、存储),`fetchMultilingualData` 管运行时依赖(请求实例、消息提示、语言、API 地址)。`i18n` 函数与 `tag` 绑定,不同 tag 的翻译数据互相隔离。
116
118
 
117
119
  ## 常见问题
118
120
 
119
121
  ### Q: 子应用需要调用 initIntl 吗?
120
122
 
121
- 默认不需要。主应用负责初始化和获取文案,子应用直接使用共享的 `intl` 对象。仅当子应用需要独立 tag 时才自行调用。
123
+ 默认不需要。主应用负责初始化和获取文案,子应用直接使用共享的 `intl` 对象。**仅当子应用需要与主应用不同的 tag** 时,再在子应用内**额外**调用一次 `initIntl`(可从本包 `import { initIntl } from '<%= packageScope %>/common-intl'`),并自行 `render` 拉取。
122
124
 
123
125
  ### Q: 翻译数据什么时候加载?
124
126