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.
- package/README.md +7 -20
- package/bin/mico.js +27 -62
- package/generators/micro-react/index.js +25 -1
- package/generators/micro-react/templates/.cursor/rules/always-read-docs.mdc +3 -0
- package/generators/micro-react/templates/.cursor/rules/project-overview.mdc +1 -0
- package/generators/micro-react/templates/CICD/start_dev.sh +11 -0
- package/generators/micro-react/templates/CICD/start_local.sh +9 -0
- package/generators/micro-react/templates/CICD/start_prod.sh +13 -0
- package/generators/micro-react/templates/CICD/start_test.sh +11 -0
- package/generators/micro-react/templates/CLAUDE.md +1 -0
- package/generators/micro-react/templates/README.md +1 -1
- package/generators/micro-react/templates/apps/layout/config/config.dev.ts +13 -5
- package/generators/micro-react/templates/apps/layout/config/config.prod.development.ts +12 -0
- package/generators/micro-react/templates/apps/layout/config/config.prod.testing.ts +12 -0
- package/generators/micro-react/templates/apps/layout/config/config.prod.ts +14 -0
- 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
- package/generators/micro-react/templates/apps/layout/docs/feature-/345/233/275/351/231/205/345/214/226.md +121 -0
- package/generators/micro-react/templates/apps/layout/docs/feature-/345/276/256/345/211/215/347/253/257/346/250/241/345/274/217.md +8 -0
- package/generators/micro-react/templates/apps/layout/docs/feature-/350/217/234/345/215/225/346/235/203/351/231/220/346/216/247/345/210/266.md +83 -77
- 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
- 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
- package/generators/micro-react/templates/apps/layout/mock/api.mock.ts +23 -31
- package/generators/micro-react/templates/apps/layout/mock/menus.ts +14 -0
- package/generators/micro-react/templates/apps/layout/mock/pages.ts +27 -8
- package/generators/micro-react/templates/apps/layout/package.json +2 -0
- package/generators/micro-react/templates/apps/layout/src/app.tsx +85 -4
- package/generators/micro-react/templates/apps/layout/src/common/auth/index.ts +3 -0
- package/generators/micro-react/templates/apps/layout/src/common/auth/tenant.ts +25 -0
- package/generators/micro-react/templates/apps/layout/src/common/auth/type.ts +41 -27
- package/generators/micro-react/templates/apps/layout/src/common/intl/formatLayoutMessage.ts +30 -0
- package/generators/micro-react/templates/apps/layout/src/common/intl/index.ts +6 -0
- package/generators/micro-react/templates/apps/layout/src/common/intl/intlRuntime.ts +14 -0
- package/generators/micro-react/templates/apps/layout/src/common/intl/localeMapping.ts +30 -0
- package/generators/micro-react/templates/apps/layout/src/common/intl/types.ts +14 -0
- package/generators/micro-react/templates/apps/layout/src/common/intl/useLayoutIntl.ts +40 -0
- package/generators/micro-react/templates/apps/layout/src/common/logger.ts +3 -4
- package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +148 -85
- package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +29 -6
- package/generators/micro-react/templates/apps/layout/src/common/micro/types.ts +23 -0
- package/generators/micro-react/templates/apps/layout/src/common/portal-data.ts +46 -2
- package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +74 -15
- package/generators/micro-react/templates/apps/layout/src/common/request/token-refresh.ts +2 -0
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +32 -6
- package/generators/micro-react/templates/apps/layout/src/components/PermissionFilter/index.tsx +51 -0
- package/generators/micro-react/templates/apps/layout/src/components/RightContent/AvatarDropdown.tsx +10 -1
- package/generators/micro-react/templates/apps/layout/src/components/RightContent/TenantDropdown.tsx +76 -0
- package/generators/micro-react/templates/apps/layout/src/components/RightContent/index.ts +1 -0
- package/generators/micro-react/templates/apps/layout/src/components/RightContent/tenant-dropdown.less +48 -0
- package/generators/micro-react/templates/apps/layout/src/constants/index.ts +1 -0
- package/generators/micro-react/templates/apps/layout/src/hooks/index.ts +1 -0
- package/generators/micro-react/templates/apps/layout/src/hooks/useMenuState.ts +18 -0
- package/generators/micro-react/templates/apps/layout/src/hooks/useTenant.ts +41 -0
- package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.tsx +4 -1
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +21 -9
- package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +105 -60
- package/generators/micro-react/templates/apps/layout/src/locales/en-US.ts +28 -0
- package/generators/micro-react/templates/apps/layout/src/locales/zh-CN.ts +26 -0
- package/generators/micro-react/templates/apps/layout/src/pages/404/index.tsx +7 -3
- package/generators/micro-react/templates/apps/layout/src/pages/Home/index.less +32 -0
- package/generators/micro-react/templates/apps/layout/src/pages/Home/index.tsx +148 -4
- package/generators/micro-react/templates/apps/layout/src/requestErrorConfig.ts +2 -1
- package/generators/micro-react/templates/apps/layout/src/services/user.ts +79 -21
- package/generators/micro-react/templates/apps/layout/typings.d.ts +16 -0
- package/generators/micro-react/templates/docs/package-shared.md +189 -0
- package/generators/micro-react/templates/package.json +1 -1
- package/generators/micro-react/templates/packages/common-intl/README.md +78 -368
- package/generators/micro-react/templates/packages/common-intl/package.json +3 -13
- package/generators/micro-react/templates/packages/common-intl/src/index.ts +5 -6
- package/generators/micro-react/templates/packages/common-intl/src/intl.ts +115 -28
- package/generators/micro-react/templates/packages/common-intl/src/umiLocaleBridge.ts +101 -0
- package/generators/micro-react/templates/packages/common-intl/tsconfig.json +2 -4
- package/generators/micro-react/templates/packages/shared/README.md +120 -0
- package/generators/micro-react/templates/packages/shared/package.json +26 -0
- package/generators/micro-react/templates/packages/shared/services/common/index.ts +43 -0
- package/generators/micro-react/templates/packages/shared/services/index.ts +21 -0
- package/generators/micro-react/templates/packages/shared/services/request.ts +43 -0
- package/generators/micro-react/templates/packages/shared/timezone/index.ts +228 -0
- package/generators/micro-react/templates/packages/shared/tsconfig.json +20 -0
- package/generators/micro-react/templates/scripts/apply-sentry-plugin.ts +6 -1
- package/generators/micro-react/templates/turbo.json +9 -1
- package/generators/subapp-react/index.js +28 -22
- package/generators/subapp-react/templates/homepage/README.md +1 -0
- package/generators/subapp-react/templates/homepage/config/config.prod.development.ts +1 -0
- package/generators/subapp-react/templates/homepage/config/config.prod.testing.ts +1 -0
- package/generators/subapp-react/templates/homepage/config/config.prod.ts +1 -0
- package/generators/subapp-react/templates/homepage/config/config.ts +10 -0
- package/generators/subapp-react/templates/homepage/docs/feature-PermissionFilter/346/214/211/351/222/256/346/235/203/351/231/220.md +35 -0
- package/generators/subapp-react/templates/homepage/docs/feature-/345/233/275/351/231/205/345/214/226.md +124 -0
- package/generators/subapp-react/templates/homepage/package.json +3 -1
- package/generators/subapp-react/templates/homepage/src/app.tsx +104 -2
- package/generators/subapp-react/templates/homepage/src/common/intl/index.ts +15 -0
- package/generators/subapp-react/templates/homepage/src/common/intl/intlRuntime.ts +14 -0
- package/generators/subapp-react/templates/homepage/src/common/intl/localeMapping.ts +24 -0
- package/generators/subapp-react/templates/homepage/src/common/intl/subappIntlConfig.ts +28 -0
- package/generators/subapp-react/templates/homepage/src/common/intl/subappLocale.ts +18 -0
- package/generators/subapp-react/templates/homepage/src/common/intl/subappOwnIntl.ts +63 -0
- package/generators/subapp-react/templates/homepage/src/common/intl/types.ts +14 -0
- package/generators/subapp-react/templates/homepage/src/common/intl/useSubappIntl.ts +61 -0
- package/generators/subapp-react/templates/homepage/src/common/locale.ts +80 -0
- package/generators/subapp-react/templates/homepage/src/common/mainApp.ts +41 -2
- package/generators/subapp-react/templates/homepage/src/components/PermissionFilter/index.tsx +48 -0
- package/generators/subapp-react/templates/homepage/src/locales/en-US.ts +6 -0
- package/generators/subapp-react/templates/homepage/src/locales/zh-CN.ts +6 -0
- package/generators/subapp-react/templates/homepage/src/pages/index.less +10 -0
- package/generators/subapp-react/templates/homepage/src/pages/index.tsx +86 -1
- package/generators/subapp-react/templates/homepage/typings.d.ts +12 -0
- package/lib/utils.js +0 -1
- package/package.json +2 -2
- package/generators/micro-react/templates/apps/layout/docs/common-intl.md +0 -372
- package/generators/micro-react/templates/packages/common-intl/src/indexedDBUtils.ts +0 -51
- package/generators/micro-react/templates/packages/common-intl/src/utils.ts +0 -482
- package/generators/micro-react/templates/packages/common-intl/vite.config.ts +0 -25
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getFromStorage, removeStorage, setStorage } from '@/common/helpers';
|
|
2
|
+
import { STORAGE_KEYS } from '@/constants';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 当前租户 code 存储工具
|
|
6
|
+
*
|
|
7
|
+
* 多租户模式下,当前选中的租户会被持久化到 localStorage,
|
|
8
|
+
* 在调用 `fetchUserInfo` 时作为 `tenant` 参数传给后端,
|
|
9
|
+
* 以获取对应租户的 menu_perms / button_perms / region_perms。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const getStoredTenantCode = (): string | null => {
|
|
13
|
+
return getFromStorage(STORAGE_KEYS.CURRENT_TENANT);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const setStoredTenantCode = (code: string): void => {
|
|
17
|
+
if (!code) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
setStorage(STORAGE_KEYS.CURRENT_TENANT, code);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const clearStoredTenantCode = (): void => {
|
|
24
|
+
removeStorage(STORAGE_KEYS.CURRENT_TENANT);
|
|
25
|
+
};
|
|
@@ -8,40 +8,54 @@ 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
|
-
*
|
|
30
|
+
* OpenAPI — GET /user/info/ 响应 `data`
|
|
13
31
|
*/
|
|
14
|
-
export interface
|
|
32
|
+
export interface IUserInfoApiData {
|
|
15
33
|
id: number;
|
|
34
|
+
avatar: string;
|
|
35
|
+
email: string;
|
|
16
36
|
name: string;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
37
|
+
app_perms: IAppPerms[];
|
|
38
|
+
region_perms: string[];
|
|
39
|
+
menu_perms: string[];
|
|
40
|
+
button_perms: string[];
|
|
41
|
+
is_superuser: boolean;
|
|
42
|
+
/** 租户模式:1 单租户,2 多租户 */
|
|
43
|
+
tenant_model: number;
|
|
44
|
+
/** 用户所属租户列表 */
|
|
45
|
+
tenant: ITenantItem[];
|
|
46
|
+
/** 当前租户 code,对应 ITenantItem.code */
|
|
47
|
+
current_tenant: string;
|
|
22
48
|
}
|
|
23
49
|
|
|
24
50
|
/**
|
|
25
|
-
*
|
|
51
|
+
* 应用内用户信息(initialState.currentUser)
|
|
52
|
+
* 在 `IUserInfoApiData` 基础上补充展示用字段(由 fetchUserInfo normalize)
|
|
26
53
|
*/
|
|
27
|
-
export interface IUserInfo {
|
|
28
|
-
|
|
29
|
-
username: string;
|
|
30
|
-
email: string;
|
|
54
|
+
export interface IUserInfo extends IUserInfoApiData {
|
|
55
|
+
/** 展示名,通常来自接口 `name` */
|
|
31
56
|
user_name: string;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
is_superuser: boolean;
|
|
37
|
-
is_staff: boolean;
|
|
38
|
-
is_active: boolean;
|
|
39
|
-
type: number;
|
|
40
|
-
agency_id: number;
|
|
41
|
-
last_login: string;
|
|
42
|
-
permission_tree: IPermissionNode[];
|
|
43
|
-
miss_permissions: string[];
|
|
44
|
-
side_menus: string[];
|
|
45
|
-
app_permissions: string[];
|
|
46
|
-
groups: string[];
|
|
57
|
+
/** 登录名/账号展示,可与 `name` 或 `email` 一致 */
|
|
58
|
+
username: string;
|
|
59
|
+
/** 是否多租户模式(tenant_model === 2 的语义化快捷字段) */
|
|
60
|
+
isMultiTenant: boolean;
|
|
47
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:
|
|
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),
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
import { layoutLogger } from '@/common/logger';
|
|
1
2
|
import { getCurrentLocale, LOCALE } from '@/common/locale';
|
|
2
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
findPageByPath,
|
|
5
|
+
getMenuPage,
|
|
6
|
+
getPages,
|
|
7
|
+
} from '@/common/portal-data';
|
|
8
|
+
|
|
9
|
+
export { findPageByPath };
|
|
3
10
|
import { isAuthDisabled, isNoPermissionRoute } from '@/constants';
|
|
4
11
|
import { getIntl } from '@umijs/max';
|
|
5
12
|
import type {
|
|
@@ -28,6 +35,7 @@ export const extractRoutesFromPages = (
|
|
|
28
35
|
| 'internal'
|
|
29
36
|
| 'microapp',
|
|
30
37
|
entry: page.htmlUrl || page.jsUrls?.[0] || undefined,
|
|
38
|
+
pageConfig: page,
|
|
31
39
|
};
|
|
32
40
|
});
|
|
33
41
|
};
|
|
@@ -39,54 +47,54 @@ export const getDynamicRoutes = (): ParsedRoute[] => {
|
|
|
39
47
|
return extractRoutesFromPages(getPages());
|
|
40
48
|
};
|
|
41
49
|
|
|
42
|
-
/**
|
|
43
|
-
* 根据路径查找对应的页面配置
|
|
44
|
-
* 匹配逻辑与 findRouteByPath 一致(精确匹配 + 通配符匹配)
|
|
45
|
-
*/
|
|
46
50
|
/**
|
|
47
51
|
* 判断指定路径的页面是否为免认证页面
|
|
48
52
|
* 当 accessControlEnabled === false 时,跳过 SSO 认证和权限校验
|
|
53
|
+
* (页面解析与 `findPageByPath` 一致:精确匹配 + `/*` 通配)
|
|
49
54
|
*/
|
|
50
55
|
export const isPageAuthFree = (pathname: string): boolean => {
|
|
51
56
|
const page = findPageByPath(getPages(), pathname);
|
|
52
57
|
return page?.accessControlEnabled === false;
|
|
53
58
|
};
|
|
54
59
|
|
|
55
|
-
export const findPageByPath = (
|
|
56
|
-
pages: PublicPageItem[],
|
|
57
|
-
pathname: string,
|
|
58
|
-
): PublicPageItem | undefined => {
|
|
59
|
-
let exact: PublicPageItem | undefined;
|
|
60
|
-
let bestWildcard: { page: PublicPageItem; basePath: string } | undefined;
|
|
61
|
-
|
|
62
|
-
for (const page of pages) {
|
|
63
|
-
if (!page.enabled) continue;
|
|
64
|
-
|
|
65
|
-
if (page.route === pathname) {
|
|
66
|
-
exact = page;
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (page.route.endsWith('/*')) {
|
|
71
|
-
const basePath = page.route.slice(0, -2);
|
|
72
|
-
if (pathname === basePath || pathname.startsWith(`${basePath}/`)) {
|
|
73
|
-
if (!bestWildcard || basePath.length > bestWildcard.basePath.length) {
|
|
74
|
-
bestWildcard = { page, basePath };
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return exact || bestWildcard?.page;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
60
|
export interface MenuFilterOptions {
|
|
84
61
|
/** 是否是超级用户 */
|
|
85
62
|
isSuperuser?: boolean | number;
|
|
86
|
-
/**
|
|
87
|
-
|
|
63
|
+
/** 用户拥有的菜单权限 code(与页面 `routeKey`、菜单项一致) */
|
|
64
|
+
menuPerms?: string[];
|
|
88
65
|
}
|
|
89
66
|
|
|
67
|
+
/**
|
|
68
|
+
* 菜单关联页是否需要走 `menu_perms` 校验
|
|
69
|
+
* - 未开启访问控制:否(菜单始终可展示,由路由侧处理公开页)
|
|
70
|
+
* - 已开启访问控制但未配置 `routeKey`:否(无 code 可验,菜单展示)
|
|
71
|
+
* - 已开启访问控制且配置了 `routeKey`:是
|
|
72
|
+
*/
|
|
73
|
+
export const isMenuPageRequiringPermCode = (
|
|
74
|
+
page: PublicPageItem | undefined,
|
|
75
|
+
): boolean => {
|
|
76
|
+
if (!page) return false;
|
|
77
|
+
if (page.accessControlEnabled === false) return false;
|
|
78
|
+
const rk = page.routeKey;
|
|
79
|
+
if (rk === null || rk === undefined || rk === '') return false;
|
|
80
|
+
return true;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 用于 `menu_perms` 匹配的权限 code(仅当 `isMenuPageRequiringPermCode(page)` 为真时有效)
|
|
85
|
+
*/
|
|
86
|
+
export const getMenuItemPermCode = (item: MenuItem): string | null => {
|
|
87
|
+
if (item.type === 'page') {
|
|
88
|
+
const page = getMenuPage(item);
|
|
89
|
+
if (!page || !isMenuPageRequiringPermCode(page)) return null;
|
|
90
|
+
return page.routeKey;
|
|
91
|
+
}
|
|
92
|
+
if (item.type === 'link') {
|
|
93
|
+
return item.nameKey || null;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
};
|
|
97
|
+
|
|
90
98
|
export const isSuperuserUser = (value?: boolean | number): boolean => {
|
|
91
99
|
return value === true || value === 1;
|
|
92
100
|
};
|
|
@@ -107,11 +115,9 @@ export const isChineseLocale = (): boolean => {
|
|
|
107
115
|
};
|
|
108
116
|
|
|
109
117
|
/**
|
|
110
|
-
*
|
|
111
|
-
* -
|
|
112
|
-
* -
|
|
113
|
-
* @param item 菜单项
|
|
114
|
-
* @param isChinese 是否中文格式
|
|
118
|
+
* 获取菜单标识符(用于内部路径拼接等,非侧栏展示)
|
|
119
|
+
* - 中文:优先 `name`,兜底 `nameKey`
|
|
120
|
+
* - 英文:优先 `nameEn`,兜底 `nameKey`
|
|
115
121
|
*/
|
|
116
122
|
export const getMenuIdentifier = (
|
|
117
123
|
item: MenuItem,
|
|
@@ -123,45 +129,96 @@ export const getMenuIdentifier = (
|
|
|
123
129
|
return item.nameEn || item.nameKey || '';
|
|
124
130
|
};
|
|
125
131
|
|
|
132
|
+
/** 菜单项被过滤时的原因(用于开发日志) */
|
|
133
|
+
export type MenuFilterDenyReason =
|
|
134
|
+
| 'adminOnly'
|
|
135
|
+
| 'groupContainer'
|
|
136
|
+
| 'missingPermCode'
|
|
137
|
+
| 'menuPermsEmpty'
|
|
138
|
+
| 'permCodeNotInMenuPerms'
|
|
139
|
+
| 'groupNoVisibleChildren'
|
|
140
|
+
| 'allChildrenFilteredOut';
|
|
141
|
+
|
|
142
|
+
export interface IMenuFilterOutcome {
|
|
143
|
+
allowed: boolean;
|
|
144
|
+
/** 当 allowed === false 时,单项校验的拒绝原因(不含容器类、子项被滤空等结构原因) */
|
|
145
|
+
denyReason?: MenuFilterDenyReason;
|
|
146
|
+
}
|
|
147
|
+
|
|
126
148
|
/**
|
|
127
|
-
*
|
|
128
|
-
* -
|
|
129
|
-
* -
|
|
130
|
-
* -
|
|
131
|
-
* -
|
|
149
|
+
* 单项是否允许展示(不含「子项全被过滤」等结构判断)
|
|
150
|
+
* - 免权限校验路由、超管、关闭权限:允许
|
|
151
|
+
* - `group`:自身不作为叶子展示,allowed 恒为 false,`denyReason` 为 `groupContainer`
|
|
152
|
+
* - `page` 且关联页未要求 `menu_perms`(`accessControlEnabled === false`,或已开启但未配置 `routeKey`):允许
|
|
153
|
+
* - `page` / `link` 需 code 时:权限 code 须在 `menu_perms` 中
|
|
132
154
|
*/
|
|
133
|
-
const
|
|
134
|
-
menuPath: string,
|
|
155
|
+
export const getMenuItemFilterOutcome = (
|
|
135
156
|
item: MenuItem,
|
|
136
157
|
options: MenuFilterOptions,
|
|
137
|
-
):
|
|
138
|
-
|
|
139
|
-
if (isAuthDisabled()) return true;
|
|
158
|
+
): IMenuFilterOutcome => {
|
|
159
|
+
if (isAuthDisabled()) return { allowed: true };
|
|
140
160
|
|
|
141
|
-
// 免权限校验路由,始终允许访问
|
|
142
161
|
const itemRoute = item.path ?? getMenuPage(item)?.route;
|
|
143
162
|
if (itemRoute && isNoPermissionRoute(itemRoute)) {
|
|
144
|
-
return true;
|
|
163
|
+
return { allowed: true };
|
|
145
164
|
}
|
|
146
165
|
|
|
147
|
-
|
|
148
|
-
if (isSuperuserUser(options.isSuperuser)) return true;
|
|
166
|
+
if (isSuperuserUser(options.isSuperuser)) return { allowed: true };
|
|
149
167
|
|
|
150
|
-
|
|
151
|
-
|
|
168
|
+
if (item.adminOnly) {
|
|
169
|
+
return { allowed: false, denyReason: 'adminOnly' };
|
|
170
|
+
}
|
|
152
171
|
|
|
153
|
-
|
|
172
|
+
if (item.type === 'group') {
|
|
173
|
+
return { allowed: false, denyReason: 'groupContainer' };
|
|
174
|
+
}
|
|
154
175
|
|
|
155
|
-
|
|
156
|
-
|
|
176
|
+
if (item.type === 'page') {
|
|
177
|
+
const page = getMenuPage(item);
|
|
178
|
+
if (page && !isMenuPageRequiringPermCode(page)) {
|
|
179
|
+
return { allowed: true };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
157
182
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
183
|
+
const menuPerms = options.menuPerms || [];
|
|
184
|
+
const code = getMenuItemPermCode(item);
|
|
185
|
+
|
|
186
|
+
if (!code) {
|
|
187
|
+
return { allowed: false, denyReason: 'missingPermCode' };
|
|
188
|
+
}
|
|
189
|
+
if (menuPerms.length === 0) {
|
|
190
|
+
return { allowed: false, denyReason: 'menuPermsEmpty' };
|
|
191
|
+
}
|
|
192
|
+
if (!menuPerms.includes(code)) {
|
|
193
|
+
return { allowed: false, denyReason: 'permCodeNotInMenuPerms' };
|
|
194
|
+
}
|
|
195
|
+
return { allowed: true };
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const shouldLogMenuFilter = (options: MenuFilterOptions): boolean => {
|
|
199
|
+
if (isAuthDisabled()) return false;
|
|
200
|
+
if (isSuperuserUser(options.isSuperuser)) return false;
|
|
201
|
+
return true;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const logMenuItemHidden = (
|
|
205
|
+
item: MenuItem,
|
|
206
|
+
options: MenuFilterOptions,
|
|
207
|
+
reason: MenuFilterDenyReason,
|
|
208
|
+
): void => {
|
|
209
|
+
if (!shouldLogMenuFilter(options)) return;
|
|
210
|
+
|
|
211
|
+
const permCode = getMenuItemPermCode(item);
|
|
212
|
+
layoutLogger.log('menuFilter', {
|
|
213
|
+
verdict: 'hidden',
|
|
214
|
+
reason,
|
|
215
|
+
menuId: item.id,
|
|
216
|
+
menuType: item.type,
|
|
217
|
+
name: item.name,
|
|
218
|
+
nameKey: item.nameKey,
|
|
219
|
+
permCode,
|
|
220
|
+
menuPermsCount: options.menuPerms?.length ?? 0,
|
|
221
|
+
path: item.path ?? getMenuPage(item)?.route ?? null,
|
|
165
222
|
});
|
|
166
223
|
};
|
|
167
224
|
|
|
@@ -171,26 +228,20 @@ const isMenuAllowed = (
|
|
|
171
228
|
export const filterMenuItems = (
|
|
172
229
|
items: MenuItem[],
|
|
173
230
|
options: MenuFilterOptions = {},
|
|
174
|
-
parentPath = '',
|
|
175
231
|
): MenuItem[] => {
|
|
176
|
-
// 根据当前语言环境判断菜单标识符格式
|
|
177
|
-
const isChinese = isChineseLocale();
|
|
178
232
|
return items
|
|
179
233
|
.filter((item) => item.enabled)
|
|
180
234
|
.map((item) => {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
getMenuIdentifier(item, isChinese),
|
|
184
|
-
);
|
|
185
|
-
const isAllowed = isMenuAllowed(menuPath, item, options);
|
|
235
|
+
const outcome = getMenuItemFilterOutcome(item, options);
|
|
236
|
+
const isAllowed = outcome.allowed;
|
|
186
237
|
|
|
187
|
-
// 递归处理子菜单
|
|
188
238
|
const nextChildren = item.children?.length
|
|
189
|
-
? filterMenuItems(item.children, options
|
|
239
|
+
? filterMenuItems(item.children, options)
|
|
190
240
|
: [];
|
|
191
241
|
|
|
192
242
|
// 分组类型:如果没有子菜单,不显示
|
|
193
243
|
if (item.type === 'group' && nextChildren.length === 0) {
|
|
244
|
+
logMenuItemHidden(item, options, 'groupNoVisibleChildren');
|
|
194
245
|
return null;
|
|
195
246
|
}
|
|
196
247
|
|
|
@@ -200,6 +251,9 @@ export const filterMenuItems = (
|
|
|
200
251
|
if (nextChildren.length > 0) {
|
|
201
252
|
return { ...item, children: nextChildren };
|
|
202
253
|
}
|
|
254
|
+
if (outcome.denyReason) {
|
|
255
|
+
logMenuItemHidden(item, options, outcome.denyReason);
|
|
256
|
+
}
|
|
203
257
|
return null;
|
|
204
258
|
}
|
|
205
259
|
|
|
@@ -211,6 +265,7 @@ export const filterMenuItems = (
|
|
|
211
265
|
|
|
212
266
|
// 有子菜单但过滤后为空,不显示
|
|
213
267
|
if (nextChildren.length === 0) {
|
|
268
|
+
logMenuItemHidden(item, options, 'allChildrenFilteredOut');
|
|
214
269
|
return null;
|
|
215
270
|
}
|
|
216
271
|
|
|
@@ -265,7 +320,7 @@ export const extractRoutes = (
|
|
|
265
320
|
|
|
266
321
|
const menuPath = buildMenuPath(parentPath, getMenuIdentifierDefault(item));
|
|
267
322
|
|
|
268
|
-
if (item.type === 'page'
|
|
323
|
+
if (item.type === 'page') {
|
|
269
324
|
const page = getMenuPage(item);
|
|
270
325
|
if (page && page.enabled) {
|
|
271
326
|
routes.push({
|
|
@@ -273,6 +328,7 @@ export const extractRoutes = (
|
|
|
273
328
|
base: page.base || '/',
|
|
274
329
|
name: item.name,
|
|
275
330
|
nameEn: item.nameEn,
|
|
331
|
+
nameKey: item.nameKey,
|
|
276
332
|
icon: item.icon,
|
|
277
333
|
loadType: getLoadType(page),
|
|
278
334
|
entry: getEntry(page),
|
|
@@ -290,24 +346,28 @@ export const extractRoutes = (
|
|
|
290
346
|
};
|
|
291
347
|
|
|
292
348
|
/**
|
|
293
|
-
*
|
|
294
|
-
*
|
|
349
|
+
* 获取菜单项的显示名称(侧栏、Tab 等)
|
|
350
|
+
* - 有 `nameKey` 时优先走 intl,**`defaultMessage` 使用 `name`(中文)或 `nameEn`/`name`(英文)作为兜底文案**
|
|
351
|
+
* - 无 `nameKey` 时直接使用 `name` / `nameEn`
|
|
295
352
|
*/
|
|
296
353
|
export const getMenuLabel = (
|
|
297
354
|
fields: { name?: string; nameEn?: string; nameKey?: string },
|
|
298
355
|
isChinese: boolean,
|
|
299
356
|
): string => {
|
|
300
|
-
const directName = isChinese ? fields.name : fields.nameEn;
|
|
301
|
-
if (directName) return directName;
|
|
302
|
-
|
|
303
357
|
if (fields.nameKey) {
|
|
304
358
|
const intl = getIntl();
|
|
359
|
+
const defaultMessage = isChinese
|
|
360
|
+
? fields.name || fields.nameKey
|
|
361
|
+
: fields.nameEn || fields.name || fields.nameKey;
|
|
305
362
|
return intl.formatMessage({
|
|
306
363
|
id: fields.nameKey,
|
|
307
|
-
defaultMessage
|
|
364
|
+
defaultMessage,
|
|
308
365
|
});
|
|
309
366
|
}
|
|
310
367
|
|
|
368
|
+
const directName = isChinese ? fields.name : fields.nameEn;
|
|
369
|
+
if (directName) return directName;
|
|
370
|
+
|
|
311
371
|
return fields.name || '';
|
|
312
372
|
};
|
|
313
373
|
|
|
@@ -354,8 +414,8 @@ export const findRouteByPath = (
|
|
|
354
414
|
routes: ParsedRoute[],
|
|
355
415
|
pathname: string,
|
|
356
416
|
): ParsedRoute | undefined => {
|
|
357
|
-
const normalizedPathname = stripTrailingSlash(pathname);
|
|
358
417
|
let exact: ParsedRoute | undefined;
|
|
418
|
+
const normalizedPathname = stripTrailingSlash(pathname);
|
|
359
419
|
let bestWildcard: { route: ParsedRoute; basePath: string } | undefined;
|
|
360
420
|
|
|
361
421
|
for (const route of routes) {
|
|
@@ -366,7 +426,10 @@ export const findRouteByPath = (
|
|
|
366
426
|
|
|
367
427
|
if (route.path.endsWith('/*')) {
|
|
368
428
|
const basePath = route.path.slice(0, -2);
|
|
369
|
-
if (
|
|
429
|
+
if (
|
|
430
|
+
normalizedPathname === basePath ||
|
|
431
|
+
normalizedPathname.startsWith(`${basePath}/`)
|
|
432
|
+
) {
|
|
370
433
|
if (!bestWildcard || basePath.length > bestWildcard.basePath.length) {
|
|
371
434
|
bestWildcard = { route, basePath };
|
|
372
435
|
}
|