generator-mico-cli 0.2.27 → 0.2.29
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 +5 -20
- package/bin/mico.js +27 -62
- package/generators/micro-react/index.js +8 -0
- 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/CLAUDE.md +1 -0
- package/generators/micro-react/templates/README.md +1 -1
- package/generators/micro-react/templates/apps/layout/config/config.dev.ts +7 -4
- package/generators/micro-react/templates/apps/layout/config/config.prod.development.ts +2 -2
- package/generators/micro-react/templates/apps/layout/config/config.prod.testing.ts +2 -2
- package/generators/micro-react/templates/apps/layout/config/config.prod.ts +3 -3
- package/generators/micro-react/templates/apps/layout/docs/feat-/346/236/204/345/273/272define/344/270/216/345/205/215/350/256/244/350/257/201/345/210/235/345/247/213/346/200/201.md +44 -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/276/256/345/211/215/347/253/257/346/250/241/345/274/217.md +11 -6
- 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/pages.ts +5 -6
- package/generators/micro-react/templates/apps/layout/package.json +2 -1
- package/generators/micro-react/templates/apps/layout/src/app.tsx +31 -2
- package/generators/micro-react/templates/apps/layout/src/common/auth/type.ts +15 -27
- 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 +2 -6
- package/generators/micro-react/templates/apps/layout/src/common/portal-data.ts +46 -2
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +5 -1
- 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/layouts/components/menu/index.tsx +3 -3
- 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 +17 -0
- package/generators/micro-react/templates/apps/layout/src/locales/zh-CN.ts +16 -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 +5 -0
- package/generators/micro-react/templates/apps/layout/src/pages/Home/index.tsx +49 -1
- package/generators/micro-react/templates/apps/layout/src/services/user.ts +28 -21
- package/generators/micro-react/templates/packages/common-intl/README.md +77 -369
- package/generators/micro-react/templates/packages/common-intl/package.json +3 -13
- package/generators/micro-react/templates/packages/common-intl/src/index.ts +3 -6
- package/generators/micro-react/templates/packages/common-intl/src/intl.ts +20 -23
- package/generators/micro-react/templates/packages/common-intl/tsconfig.json +2 -4
- 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/docs/feature-PermissionFilter/346/214/211/351/222/256/346/235/203/351/231/220.md +35 -0
- package/generators/subapp-react/templates/homepage/package.json +2 -1
- package/generators/subapp-react/templates/homepage/src/app.tsx +7 -0
- package/generators/subapp-react/templates/homepage/src/common/mainApp.ts +39 -2
- package/generators/subapp-react/templates/homepage/src/components/PermissionFilter/index.tsx +48 -0
- package/generators/subapp-react/templates/homepage/src/pages/index.tsx +35 -1
- 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
|
@@ -5,38 +5,30 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export default {
|
|
8
|
-
//
|
|
9
|
-
'GET /
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"side_menus": [
|
|
30
|
-
'首页',
|
|
31
|
-
'示例模块.示例页面',
|
|
32
|
-
'微应用示例.子应用页面',
|
|
33
|
-
'外部链接',
|
|
34
|
-
'权限管理',
|
|
35
|
-
],
|
|
36
|
-
"region_permissions": []
|
|
8
|
+
// 获取用户信息(OpenAPI 8 GET /user/info/,与 fetchUserInfo 一致)
|
|
9
|
+
'GET /user/info/': {
|
|
10
|
+
code: 200,
|
|
11
|
+
data: {
|
|
12
|
+
id: 381,
|
|
13
|
+
avatar:
|
|
14
|
+
'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
|
|
15
|
+
email: '本地测试mock用户@micous.com',
|
|
16
|
+
name: 'Easton',
|
|
17
|
+
app_perms: [],
|
|
18
|
+
region_perms: [],
|
|
19
|
+
menu_perms: [
|
|
20
|
+
'cs_web_menu_home',
|
|
21
|
+
'cs_web_menu_example_page',
|
|
22
|
+
'cs_web_menu_subapp_page',
|
|
23
|
+
'cs_web_menu_external_link',
|
|
24
|
+
'cs_web_menu_permission_management',
|
|
25
|
+
'cs_web_menu_group_management',
|
|
26
|
+
],
|
|
27
|
+
button_perms: ['cs_web_btn_subapp_demo', 'cs_web_btn_home_demo'],
|
|
28
|
+
is_superuser: false,
|
|
37
29
|
},
|
|
38
|
-
|
|
39
|
-
},
|
|
30
|
+
msg: 'ok',
|
|
31
|
+
},
|
|
40
32
|
|
|
41
33
|
// 获取统计数据
|
|
42
34
|
// 'GET /api/dashboard/stats': {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - id: 页面唯一标识
|
|
7
7
|
* - name: 页面名称
|
|
8
8
|
* - route: 路由路径
|
|
9
|
+
* - base: 微应用挂载前缀(可与 route 不同)
|
|
9
10
|
* - htmlUrl: 微应用 HTML 入口 URL
|
|
10
11
|
* - jsUrls: 额外 JS 资源
|
|
11
12
|
* - cssUrls: 额外 CSS 资源
|
|
@@ -25,10 +26,10 @@ const mockPages: PublicPageItem[] = [
|
|
|
25
26
|
{
|
|
26
27
|
id: 125,
|
|
27
28
|
name: '登录页',
|
|
28
|
-
nameEn: 'Login',
|
|
29
|
-
nameKey: 'page.user.login',
|
|
30
29
|
route: '/user/login',
|
|
31
30
|
base: '/user/login',
|
|
31
|
+
// htmlUrl:
|
|
32
|
+
// 'https://cdn-portal.micoplatform.com/portal-center/common-web/0.0.2/login/index.html',
|
|
32
33
|
htmlUrl:
|
|
33
34
|
'https://cdn-portal.micoplatform.com/portal-center/common-web/0.0.4/login/index.html',
|
|
34
35
|
jsUrls: [],
|
|
@@ -45,10 +46,10 @@ const mockPages: PublicPageItem[] = [
|
|
|
45
46
|
{
|
|
46
47
|
id: 124,
|
|
47
48
|
name: '权限管理',
|
|
48
|
-
nameEn: 'Permission Management',
|
|
49
|
-
nameKey: 'page.permission',
|
|
50
49
|
route: '/permission',
|
|
51
50
|
base: '/',
|
|
51
|
+
// htmlUrl:
|
|
52
|
+
// 'https://cdn-portal.micoplatform.com/portal-center/common-web/0.0.3/permission/index.html',
|
|
52
53
|
htmlUrl:
|
|
53
54
|
'https://cdn-portal.micoplatform.com/portal-center/common-web/0.0.4/permission/index.html',
|
|
54
55
|
jsUrls: [],
|
|
@@ -65,8 +66,6 @@ const mockPages: PublicPageItem[] = [
|
|
|
65
66
|
{
|
|
66
67
|
id: 115,
|
|
67
68
|
name: '兜底',
|
|
68
|
-
nameEn: 'Fallback',
|
|
69
|
-
nameKey: 'page.fallback',
|
|
70
69
|
route: '/*',
|
|
71
70
|
base: '/',
|
|
72
71
|
htmlUrl: '',
|
|
@@ -7,6 +7,13 @@ import * as micoUI from '@mico-platform/ui';
|
|
|
7
7
|
import React from 'react';
|
|
8
8
|
import ReactDOM from 'react-dom';
|
|
9
9
|
|
|
10
|
+
import { request as commonRequest } from './common/request';
|
|
11
|
+
import {
|
|
12
|
+
fetchMultilingualData,
|
|
13
|
+
getCurrentLocale as getIntlLocale,
|
|
14
|
+
type ILang,
|
|
15
|
+
} from '<%= packageScope %>/common-intl';
|
|
16
|
+
import * as CommonIntl from '<%= packageScope %>/common-intl';
|
|
10
17
|
import { getStoredAuthToken } from './common/auth/auth-manager';
|
|
11
18
|
import type { IUserInfo } from './common/auth/type';
|
|
12
19
|
import { fetchUserInfo } from './services/user';
|
|
@@ -90,6 +97,7 @@ if (typeof window !== 'undefined') {
|
|
|
90
97
|
win.React = React;
|
|
91
98
|
win.ReactDOM = ReactDOM;
|
|
92
99
|
win.micoUI = micoUI;
|
|
100
|
+
win.CommonIntl = CommonIntl;
|
|
93
101
|
}
|
|
94
102
|
|
|
95
103
|
// 初始化主题(在页面加载时立即执行,避免闪烁)
|
|
@@ -107,6 +115,26 @@ export const locale = {
|
|
|
107
115
|
},
|
|
108
116
|
};
|
|
109
117
|
|
|
118
|
+
// ==================== 国际化数据预加载 ====================
|
|
119
|
+
|
|
120
|
+
/** @see https://umijs.org/docs/api/runtime-config#render */
|
|
121
|
+
export function render(oldRender: () => void): void {
|
|
122
|
+
fetchMultilingualData({
|
|
123
|
+
requestInstance: commonRequest,
|
|
124
|
+
messageInstance: {
|
|
125
|
+
error: micoUI.Message.error,
|
|
126
|
+
warning: micoUI.Message.warning,
|
|
127
|
+
},
|
|
128
|
+
lang: getIntlLocale() as ILang,
|
|
129
|
+
localeRequestUrl: process.env.LOCALE_REQUEST_URL,
|
|
130
|
+
})
|
|
131
|
+
.then(oldRender)
|
|
132
|
+
.catch((error: Error) => {
|
|
133
|
+
console.error('获取多语言文案失败', error);
|
|
134
|
+
oldRender();
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
110
138
|
/**
|
|
111
139
|
* @see https://umijs.org/docs/api/runtime-config#getinitialstate
|
|
112
140
|
*/
|
|
@@ -137,8 +165,9 @@ export async function getInitialState(): Promise<{
|
|
|
137
165
|
await ensureSsoSession();
|
|
138
166
|
}
|
|
139
167
|
|
|
140
|
-
// 有 token
|
|
141
|
-
|
|
168
|
+
// 有 token 就获取用户信息(登录后 refresh、免认证页同样需要 currentUser:
|
|
169
|
+
// 如 PermissionFilter / MicroAppLoader 注入子应用的 button_perms)
|
|
170
|
+
if (getStoredAuthToken() && !skipAuth) {
|
|
142
171
|
const userInfo = await fetchUserInfoFn();
|
|
143
172
|
if (userInfo) {
|
|
144
173
|
clearRedirectCount();
|
|
@@ -9,39 +9,27 @@ export const UID = 'uid';
|
|
|
9
9
|
export const MICRO_ENV_KEY = 'micro_env';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* OpenAPI 8 — GET /user/info/ 响应 `data`(与 Apifox 导出 8 一致)
|
|
13
13
|
*/
|
|
14
|
-
export interface
|
|
14
|
+
export interface IUserInfoApiData {
|
|
15
15
|
id: number;
|
|
16
|
+
avatar: string;
|
|
17
|
+
email: string;
|
|
16
18
|
name: string;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
app_perms: string[];
|
|
20
|
+
region_perms: string[];
|
|
21
|
+
menu_perms: string[];
|
|
22
|
+
button_perms: string[];
|
|
23
|
+
is_superuser: boolean;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
|
-
*
|
|
27
|
+
* 应用内用户信息(initialState.currentUser)
|
|
28
|
+
* 在 `IUserInfoApiData` 基础上补充展示用字段(由 fetchUserInfo normalize)
|
|
26
29
|
*/
|
|
27
|
-
export interface IUserInfo {
|
|
28
|
-
|
|
29
|
-
username: string;
|
|
30
|
-
email: string;
|
|
30
|
+
export interface IUserInfo extends IUserInfoApiData {
|
|
31
|
+
/** 展示名,通常来自接口 `name` */
|
|
31
32
|
user_name: string;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
first_name: string;
|
|
35
|
-
last_name: string;
|
|
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[];
|
|
33
|
+
/** 登录名/账号展示,可与 `name` 或 `email` 一致 */
|
|
34
|
+
username: string;
|
|
47
35
|
}
|
|
@@ -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
|
}
|
|
@@ -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
|
-
/**
|
|
33
|
+
/** 路由权限标识(与用户信息 `menu_perms`、菜单项 code 一致) */
|
|
38
34
|
routeKey: string | null;
|
|
39
35
|
/** 关联的主文档 ID */
|
|
40
36
|
mainDocumentId: number;
|
|
@@ -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
|
-
|
|
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
|
};
|