generator-mico-cli 0.2.28 → 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 +4 -2
- package/generators/micro-react/templates/apps/layout/config/config.prod.development.ts +2 -0
- package/generators/micro-react/templates/apps/layout/config/config.prod.testing.ts +2 -0
- package/generators/micro-react/templates/apps/layout/config/config.prod.ts +2 -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-/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 +30 -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 -29
- 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
package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx
CHANGED
|
@@ -55,6 +55,7 @@ const MicroAppLoader: React.FC<MicroAppLoaderProps> = ({
|
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
const { initialState } = useModel('@@initialState');
|
|
58
|
+
const currentUser = initialState?.currentUser;
|
|
58
59
|
const location = useLocation();
|
|
59
60
|
const isAuthReady =
|
|
60
61
|
isAuthDisabled() ||
|
|
@@ -91,8 +92,11 @@ const MicroAppLoader: React.FC<MicroAppLoaderProps> = ({
|
|
|
91
92
|
request,
|
|
92
93
|
// 传递当前多语言类型给子应用(zh-CN, en-US)
|
|
93
94
|
locale: getCurrentLocale(),
|
|
95
|
+
// 与 fetchUserInfo 一致:子应用用于按钮级权限(如 PermissionFilter)
|
|
96
|
+
button_perms: currentUser?.button_perms ?? [],
|
|
97
|
+
is_superuser: currentUser?.is_superuser,
|
|
94
98
|
};
|
|
95
|
-
}, [base, env, routePath]);
|
|
99
|
+
}, [base, env, routePath, currentUser]);
|
|
96
100
|
|
|
97
101
|
// ref 持有最新的 buildProps,避免加载 effect 依赖它导致子应用被重载
|
|
98
102
|
const buildPropsRef = useRef(buildProps);
|
package/generators/micro-react/templates/apps/layout/src/components/PermissionFilter/index.tsx
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { isSuperuserUser } from '@/common/menu/parser';
|
|
2
|
+
import { useModel } from '@umijs/max';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
export interface IPermissionFilterProps {
|
|
6
|
+
/** 按钮/操作权限标识,需在用户 `button_perms` 中 */
|
|
7
|
+
permissionKey: string;
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
/** 无权限时渲染内容;不传则渲染 null */
|
|
10
|
+
fallback?: React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hasButtonPermission(
|
|
14
|
+
permissionKey: string,
|
|
15
|
+
buttonPerms: string[] | undefined,
|
|
16
|
+
isSuperuser?: boolean | number,
|
|
17
|
+
): boolean {
|
|
18
|
+
if (!permissionKey?.trim()) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (isSuperuserUser(isSuperuser)) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return Boolean(buttonPerms?.includes(permissionKey));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 按当前用户 `button_perms`(来自 fetchUserInfo)判断是否渲染子节点。
|
|
29
|
+
* 超级用户(is_superuser)视为拥有全部按钮权限。
|
|
30
|
+
*/
|
|
31
|
+
const PermissionFilter: React.FC<IPermissionFilterProps> = ({
|
|
32
|
+
permissionKey,
|
|
33
|
+
children,
|
|
34
|
+
fallback = null,
|
|
35
|
+
}) => {
|
|
36
|
+
const { initialState } = useModel('@@initialState');
|
|
37
|
+
const currentUser = initialState?.currentUser;
|
|
38
|
+
|
|
39
|
+
const allowed = hasButtonPermission(
|
|
40
|
+
permissionKey,
|
|
41
|
+
currentUser?.button_perms,
|
|
42
|
+
currentUser?.is_superuser,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
if (!allowed) {
|
|
46
|
+
return <>{fallback}</>;
|
|
47
|
+
}
|
|
48
|
+
return <>{children}</>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default PermissionFilter;
|
package/generators/micro-react/templates/apps/layout/src/components/RightContent/AvatarDropdown.tsx
CHANGED
|
@@ -42,8 +42,17 @@ interface IMenuItem {
|
|
|
42
42
|
|
|
43
43
|
// TODO: 临时假数据,后续接入真实登录态
|
|
44
44
|
const MOCK_USER: Partial<IUserInfo> = {
|
|
45
|
+
id: 0,
|
|
46
|
+
name: 'Test User',
|
|
45
47
|
user_name: 'Test User',
|
|
46
48
|
username: 'test@micous.com',
|
|
49
|
+
email: 'test@micous.com',
|
|
50
|
+
avatar: '',
|
|
51
|
+
app_perms: [],
|
|
52
|
+
region_perms: [],
|
|
53
|
+
menu_perms: [],
|
|
54
|
+
button_perms: [],
|
|
55
|
+
is_superuser: true,
|
|
47
56
|
};
|
|
48
57
|
|
|
49
58
|
export const AvatarName: React.FC<{ userName?: string }> = ({ userName }) => {
|
|
@@ -371,7 +380,7 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
|
|
|
371
380
|
</Avatar>
|
|
372
381
|
</div>
|
|
373
382
|
)}
|
|
374
|
-
<AvatarName userName={currentUser?.user_name} />
|
|
383
|
+
<AvatarName userName={currentUser?.user_name || currentUser?.email} />
|
|
375
384
|
<IconFont
|
|
376
385
|
type="webcs-outline_down1"
|
|
377
386
|
className="avatar-dropdown-icon"
|
|
@@ -119,7 +119,7 @@ const LayoutMenu: React.FC<LayoutMenuProps> = () => {
|
|
|
119
119
|
const currentUser = initialState?.currentUser;
|
|
120
120
|
|
|
121
121
|
// Parse menu data
|
|
122
|
-
//
|
|
122
|
+
// getMenuItemFilterOutcome 内部已对每个菜单项单独检查 isNoPermissionRoute,
|
|
123
123
|
// 无需在此按当前页面路径全局跳过过滤,避免菜单可见性随页面变化
|
|
124
124
|
const menuItems = useMemo(() => {
|
|
125
125
|
const menus = getMenus();
|
|
@@ -128,10 +128,10 @@ const LayoutMenu: React.FC<LayoutMenuProps> = () => {
|
|
|
128
128
|
}
|
|
129
129
|
const filteredMenus = filterMenuItems(menus, {
|
|
130
130
|
isSuperuser: currentUser?.is_superuser,
|
|
131
|
-
|
|
131
|
+
menuPerms: currentUser?.menu_perms || [],
|
|
132
132
|
});
|
|
133
133
|
return parseMenuItems(filteredMenus);
|
|
134
|
-
}, [currentUser?.is_superuser, currentUser?.
|
|
134
|
+
}, [currentUser?.is_superuser, currentUser?.menu_perms]);
|
|
135
135
|
|
|
136
136
|
// 使用菜单状态 Hook
|
|
137
137
|
const {
|
|
@@ -5,10 +5,11 @@ import {
|
|
|
5
5
|
findPageByPath,
|
|
6
6
|
findRouteByPath,
|
|
7
7
|
getDynamicRoutes,
|
|
8
|
+
isMenuPageRequiringPermCode,
|
|
8
9
|
isSuperuserUser,
|
|
9
10
|
} from '@/common/menu';
|
|
10
|
-
import { getMenus, getPages, hasPages } from '@/common/portal-data';
|
|
11
11
|
import { getAppNameFromEntry } from '@/common/micro';
|
|
12
|
+
import { getMenus, getPages, hasPages } from '@/common/portal-data';
|
|
12
13
|
import AppTabs from '@/components/AppTabs';
|
|
13
14
|
import MicroAppLoader from '@/components/MicroAppLoader';
|
|
14
15
|
import {
|
|
@@ -36,25 +37,20 @@ const BasicLayout: React.FC = () => {
|
|
|
36
37
|
const { initialState } = useModel('@@initialState');
|
|
37
38
|
const currentUser = initialState?.currentUser;
|
|
38
39
|
|
|
39
|
-
// 路由切换时自动刷新用户权限
|
|
40
|
-
// isRefreshing 状态仅用于显示 loading 覆盖层
|
|
41
|
-
// 实际的时序协调由 loadingCoordinator 在 MicroAppManager 层面处理
|
|
42
40
|
const { isRefreshing } = useRoutePermissionRefresh();
|
|
43
41
|
|
|
44
42
|
const filterOptions = useMemo(
|
|
45
43
|
() => ({
|
|
46
44
|
isSuperuser: currentUser?.is_superuser,
|
|
47
|
-
|
|
45
|
+
menuPerms: currentUser?.menu_perms || [],
|
|
48
46
|
}),
|
|
49
|
-
[currentUser?.is_superuser, currentUser?.
|
|
47
|
+
[currentUser?.is_superuser, currentUser?.menu_perms],
|
|
50
48
|
);
|
|
51
49
|
|
|
52
|
-
// 所有页面路由(优先 PAGES,降级 MENUS)— 用于路由匹配和渲染
|
|
53
50
|
const allPageRoutes = useMemo(() => {
|
|
54
51
|
return getDynamicRoutes();
|
|
55
52
|
}, []);
|
|
56
53
|
|
|
57
|
-
// 菜单路由(从 MENUS)— 用于权限交叉引用
|
|
58
54
|
const allMenuRoutes = useMemo(() => {
|
|
59
55
|
return extractRoutes(getMenus());
|
|
60
56
|
}, []);
|
|
@@ -67,58 +63,121 @@ const BasicLayout: React.FC = () => {
|
|
|
67
63
|
return extractRoutes(filteredMenus);
|
|
68
64
|
}, [filterOptions, allMenuRoutes]);
|
|
69
65
|
|
|
70
|
-
// 当前路由配置(从所有页面路由中查找)
|
|
71
66
|
const currentRoute = useMemo(() => {
|
|
72
67
|
return findRouteByPath(allPageRoutes, location.pathname);
|
|
73
68
|
}, [allPageRoutes, location.pathname]);
|
|
74
69
|
|
|
75
|
-
// 权限判断:菜单交叉引用 + 隐藏页面级兜底
|
|
76
70
|
const isForbidden = useMemo(() => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// Tier 1: 菜单权限交叉引用
|
|
84
|
-
const inAllMenu = findRouteByPath(allMenuRoutes, location.pathname);
|
|
85
|
-
if (inAllMenu) {
|
|
86
|
-
const inAllowed = findRouteByPath(allowedMenuRoutes, location.pathname);
|
|
87
|
-
const forbidden = !inAllowed;
|
|
71
|
+
const pathname = location.pathname;
|
|
72
|
+
const permCtx = {
|
|
73
|
+
userId: currentUser?.id,
|
|
74
|
+
isSuperuser: currentUser?.is_superuser,
|
|
75
|
+
menuPermsCount: currentUser?.menu_perms?.length ?? 0,
|
|
76
|
+
};
|
|
88
77
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
78
|
+
if (isAuthDisabled()) {
|
|
79
|
+
layoutLogger.log('routePermission', {
|
|
80
|
+
verdict: 'skip',
|
|
81
|
+
reason: 'disableAuth',
|
|
82
|
+
pathname,
|
|
83
|
+
...permCtx,
|
|
94
84
|
});
|
|
95
|
-
|
|
96
|
-
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (isNoPermissionRoute(pathname)) {
|
|
88
|
+
layoutLogger.log('routePermission', {
|
|
89
|
+
verdict: 'skip',
|
|
90
|
+
reason: 'noPermissionRoute',
|
|
91
|
+
pathname,
|
|
92
|
+
...permCtx,
|
|
93
|
+
});
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (!currentRoute) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
if (isSuperuserUser(currentUser?.is_superuser)) {
|
|
100
|
+
layoutLogger.log('routePermission', {
|
|
101
|
+
verdict: 'skip',
|
|
102
|
+
reason: 'superuser',
|
|
103
|
+
pathname,
|
|
104
|
+
routePath: currentRoute.path,
|
|
105
|
+
...permCtx,
|
|
106
|
+
});
|
|
107
|
+
return false;
|
|
97
108
|
}
|
|
98
109
|
|
|
99
|
-
|
|
100
|
-
|
|
110
|
+
const page =
|
|
111
|
+
currentRoute.pageConfig ??
|
|
112
|
+
(hasPages() ? findPageByPath(getPages(), pathname) : undefined);
|
|
101
113
|
|
|
102
|
-
|
|
103
|
-
|
|
114
|
+
if (page) {
|
|
115
|
+
if (page.adminOnly) {
|
|
116
|
+
layoutLogger.log('routePermission', {
|
|
117
|
+
verdict: 'deny',
|
|
118
|
+
reason: 'adminOnly',
|
|
119
|
+
pathname,
|
|
120
|
+
pageId: page.id,
|
|
121
|
+
routePath: currentRoute.path,
|
|
122
|
+
...permCtx,
|
|
123
|
+
});
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (!page.accessControlEnabled) {
|
|
127
|
+
layoutLogger.log('routePermission', {
|
|
128
|
+
verdict: 'allow',
|
|
129
|
+
reason: 'publicPage',
|
|
130
|
+
pathname,
|
|
131
|
+
pageId: page.id,
|
|
132
|
+
accessControlEnabled: false,
|
|
133
|
+
...permCtx,
|
|
134
|
+
});
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
if (!isMenuPageRequiringPermCode(page)) {
|
|
138
|
+
layoutLogger.log('routePermission', {
|
|
139
|
+
verdict: 'allow',
|
|
140
|
+
reason: 'accessControlNoRouteKey',
|
|
141
|
+
pathname,
|
|
142
|
+
pageId: page.id,
|
|
143
|
+
routePath: currentRoute.path,
|
|
144
|
+
...permCtx,
|
|
145
|
+
});
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
const menuPerms = currentUser?.menu_perms || [];
|
|
149
|
+
const rk = page.routeKey!;
|
|
150
|
+
const keyInMenuPerms = menuPerms.includes(rk);
|
|
151
|
+
const forbidden = !keyInMenuPerms;
|
|
104
152
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
153
|
+
layoutLogger.log('routePermission', {
|
|
154
|
+
verdict: forbidden ? 'deny' : 'allow',
|
|
155
|
+
reason: forbidden ? 'routeKeyNotInMenuPerms' : 'routeKeyInMenuPerms',
|
|
156
|
+
branch: 'pageMeta',
|
|
157
|
+
pathname,
|
|
108
158
|
pageId: page.id,
|
|
159
|
+
routePath: currentRoute.path,
|
|
160
|
+
routeKey: page.routeKey,
|
|
161
|
+
keyInMenuPerms,
|
|
162
|
+
...permCtx,
|
|
109
163
|
});
|
|
110
|
-
|
|
164
|
+
|
|
165
|
+
return forbidden;
|
|
111
166
|
}
|
|
112
167
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
168
|
+
const inAllMenu = findRouteByPath(allMenuRoutes, pathname);
|
|
169
|
+
if (inAllMenu) {
|
|
170
|
+
const inAllowed = findRouteByPath(allowedMenuRoutes, pathname);
|
|
171
|
+
const forbidden = !inAllowed;
|
|
116
172
|
|
|
117
|
-
layoutLogger.log('
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
173
|
+
layoutLogger.log('routePermission', {
|
|
174
|
+
verdict: forbidden ? 'deny' : 'allow',
|
|
175
|
+
reason: forbidden ? 'notInFilteredMenuRoutes' : 'inFilteredMenuRoutes',
|
|
176
|
+
branch: 'menuRouteFallback',
|
|
177
|
+
pathname,
|
|
178
|
+
routePath: currentRoute.path,
|
|
179
|
+
inAllowed: !!inAllowed,
|
|
180
|
+
...permCtx,
|
|
122
181
|
});
|
|
123
182
|
|
|
124
183
|
return forbidden;
|
|
@@ -131,13 +190,11 @@ const BasicLayout: React.FC = () => {
|
|
|
131
190
|
allowedMenuRoutes,
|
|
132
191
|
location.pathname,
|
|
133
192
|
currentUser?.is_superuser,
|
|
134
|
-
currentUser?.
|
|
193
|
+
currentUser?.menu_perms,
|
|
135
194
|
]);
|
|
136
195
|
|
|
137
|
-
// 判断是否需要显示布局
|
|
138
196
|
const showLayout = !isNoLayoutRoute(location.pathname);
|
|
139
197
|
|
|
140
|
-
// 渲染页面内容
|
|
141
198
|
const renderContent = () => {
|
|
142
199
|
layoutLogger.log('renderContent:', {
|
|
143
200
|
pathname: location.pathname,
|
|
@@ -145,22 +202,18 @@ const BasicLayout: React.FC = () => {
|
|
|
145
202
|
isForbidden,
|
|
146
203
|
isRefreshing,
|
|
147
204
|
currentUserChanged: currentUser?.is_superuser,
|
|
148
|
-
|
|
205
|
+
menuPermsCount: currentUser?.menu_perms?.length,
|
|
149
206
|
});
|
|
150
207
|
|
|
151
|
-
// 无权限,显示 403
|
|
152
208
|
if (isForbidden) {
|
|
153
209
|
return <ForbiddenPage />;
|
|
154
210
|
}
|
|
155
211
|
|
|
156
|
-
// 如果有匹配的动态路由配置且需要加载微应用
|
|
157
212
|
if (currentRoute?.loadType === 'microapp' && currentRoute.entry) {
|
|
158
213
|
layoutLogger.log('Loading microapp:', currentRoute);
|
|
159
|
-
// 使用 entry 的 origin 作为微应用标识,同一个 entry 的所有路由共用一个实例
|
|
160
214
|
const appName = getAppNameFromEntry(currentRoute.entry);
|
|
161
215
|
return (
|
|
162
216
|
<>
|
|
163
|
-
{/* 权限刷新中显示覆盖层,但不卸载 MicroAppLoader,避免子应用被中断 */}
|
|
164
217
|
{isRefreshing && (
|
|
165
218
|
<div
|
|
166
219
|
style={{
|
|
@@ -180,23 +233,17 @@ const BasicLayout: React.FC = () => {
|
|
|
180
233
|
</div>
|
|
181
234
|
)}
|
|
182
235
|
<MicroAppLoader
|
|
183
|
-
// 使用 appName 作为 key,确保不同微应用使用不同的组件实例
|
|
184
|
-
// 同一个微应用的不同路由共用同一个实例
|
|
185
236
|
key={appName}
|
|
186
237
|
entry={currentRoute.entry}
|
|
187
238
|
base={currentRoute.base}
|
|
188
|
-
// 使用 entry 生成的标识,而不是 path
|
|
189
239
|
name={appName}
|
|
190
|
-
// 显示名称用于 loading 提示
|
|
191
240
|
displayName={currentRoute.name}
|
|
192
|
-
// 传递当前路由路径,让子应用进行内部路由切换
|
|
193
241
|
routePath={currentRoute.path}
|
|
194
242
|
/>
|
|
195
243
|
</>
|
|
196
244
|
);
|
|
197
245
|
}
|
|
198
246
|
|
|
199
|
-
// 权限刷新中,显示 loading(仅对非微应用路由)
|
|
200
247
|
if (isRefreshing) {
|
|
201
248
|
return (
|
|
202
249
|
<Spin
|
|
@@ -206,11 +253,9 @@ const BasicLayout: React.FC = () => {
|
|
|
206
253
|
);
|
|
207
254
|
}
|
|
208
255
|
|
|
209
|
-
// 默认:使用 Outlet 渲染内部路由
|
|
210
256
|
return <Outlet />;
|
|
211
257
|
};
|
|
212
258
|
|
|
213
|
-
// 不需要布局的页面(仍需检查权限)
|
|
214
259
|
if (!showLayout) {
|
|
215
260
|
return (
|
|
216
261
|
<Suspense
|
|
@@ -20,6 +20,23 @@ export default {
|
|
|
20
20
|
|
|
21
21
|
// Page titles
|
|
22
22
|
'page.home.title': 'Home',
|
|
23
|
+
'page.home.permissionDemo.title': 'PermissionFilter (button permission) demo',
|
|
24
|
+
'page.home.permissionDemo.desc':
|
|
25
|
+
'Uses fetchUserInfo.button_perms and is_superuser; sample permissionKey: {key}',
|
|
26
|
+
'page.home.permissionDemo.noPerm':
|
|
27
|
+
'No permission for {key} (not listed in button_perms).',
|
|
28
|
+
'page.home.permissionDemo.fallbackTitle': 'No permission',
|
|
29
|
+
'page.home.permissionDemo.hasPerm': 'Visible when permitted',
|
|
30
|
+
|
|
31
|
+
// Mock menu nameKeys (aligned with mock/menus.ts)
|
|
32
|
+
'cs_web_menu_home': 'Home',
|
|
33
|
+
'cs_web_menu_example_module': 'Example Module',
|
|
34
|
+
'cs_web_menu_example_page': 'Example Page',
|
|
35
|
+
'cs_web_menu_microapp_example': 'Microapp Example',
|
|
36
|
+
'cs_web_menu_subapp_page': 'Subapp Page',
|
|
37
|
+
'cs_web_menu_external_link': 'External Link',
|
|
38
|
+
'cs_web_menu_permission_management': 'Permission Management',
|
|
39
|
+
'cs_web_menu_group_management': 'Group Management',
|
|
23
40
|
|
|
24
41
|
// AvatarDropdown
|
|
25
42
|
'avatar.language': 'Language',
|
|
@@ -19,6 +19,22 @@ export default {
|
|
|
19
19
|
|
|
20
20
|
// Page titles
|
|
21
21
|
'page.home.title': '首页',
|
|
22
|
+
'page.home.permissionDemo.title': 'PermissionFilter 按钮权限示例',
|
|
23
|
+
'page.home.permissionDemo.desc':
|
|
24
|
+
'依据当前用户 fetchUserInfo.button_perms 与 is_superuser;示例 permissionKey:{key}',
|
|
25
|
+
'page.home.permissionDemo.noPerm': '无 {key} 权限(button_perms 未包含该标识)。',
|
|
26
|
+
'page.home.permissionDemo.fallbackTitle': '无权限',
|
|
27
|
+
'page.home.permissionDemo.hasPerm': '有权限时可见',
|
|
28
|
+
|
|
29
|
+
// Mock 菜单 nameKey(与 mock/menus.ts 一致)
|
|
30
|
+
'cs_web_menu_home': '首页',
|
|
31
|
+
'cs_web_menu_example_module': '示例模块',
|
|
32
|
+
'cs_web_menu_example_page': '示例页面',
|
|
33
|
+
'cs_web_menu_microapp_example': '微应用示例',
|
|
34
|
+
'cs_web_menu_subapp_page': '子应用页面',
|
|
35
|
+
'cs_web_menu_external_link': '外部链接',
|
|
36
|
+
'cs_web_menu_permission_management': '权限管理',
|
|
37
|
+
'cs_web_menu_group_management': '小组管理',
|
|
22
38
|
|
|
23
39
|
// AvatarDropdown
|
|
24
40
|
'avatar.language': '语言',
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
extractRoutes,
|
|
3
|
+
filterMenuItems,
|
|
4
|
+
type MenuFilterOptions,
|
|
5
|
+
} from '@/common/menu';
|
|
2
6
|
import { getMenus } from '@/common/portal-data';
|
|
3
7
|
import { isAuthDisabled } from '@/constants';
|
|
4
8
|
import { Button, Result, Space } from '@mico-platform/ui';
|
|
@@ -30,9 +34,9 @@ const NotFoundPage: React.FC = () => {
|
|
|
30
34
|
const filterOptions = useMemo<MenuFilterOptions>(
|
|
31
35
|
() => ({
|
|
32
36
|
isSuperuser: currentUser?.is_superuser,
|
|
33
|
-
|
|
37
|
+
menuPerms: currentUser?.menu_perms || [],
|
|
34
38
|
}),
|
|
35
|
-
[currentUser?.is_superuser, currentUser?.
|
|
39
|
+
[currentUser?.is_superuser, currentUser?.menu_perms],
|
|
36
40
|
);
|
|
37
41
|
|
|
38
42
|
const firstAvailablePath = useMemo(
|
|
@@ -1,11 +1,59 @@
|
|
|
1
|
+
import PermissionFilter from '@/components/PermissionFilter';
|
|
2
|
+
import { Alert, Button, Card, Space, Typography } from '@mico-platform/ui';
|
|
1
3
|
import { useIntl } from '@umijs/max';
|
|
4
|
+
import React from 'react';
|
|
2
5
|
import styles from './index.less';
|
|
3
6
|
|
|
7
|
+
const { Paragraph, Text } = Typography;
|
|
8
|
+
|
|
9
|
+
/** 与 mock GET /user/info/ button_perms 中的示例 key 对齐 */
|
|
10
|
+
const HOME_BUTTON_PERM_KEY = 'cs_web_btn_home_demo';
|
|
11
|
+
|
|
4
12
|
const HomePage: React.FC = () => {
|
|
5
13
|
const intl = useIntl();
|
|
14
|
+
|
|
6
15
|
return (
|
|
7
16
|
<div className={styles.container}>
|
|
8
|
-
|
|
17
|
+
<Typography.Title heading={4}>
|
|
18
|
+
{intl.formatMessage({ id: 'page.home.title' })}
|
|
19
|
+
</Typography.Title>
|
|
20
|
+
|
|
21
|
+
<Card
|
|
22
|
+
className={styles.demoCard}
|
|
23
|
+
title={intl.formatMessage({ id: 'page.home.permissionDemo.title' })}
|
|
24
|
+
>
|
|
25
|
+
<Paragraph type="secondary">
|
|
26
|
+
{intl.formatMessage(
|
|
27
|
+
{ id: 'page.home.permissionDemo.desc' },
|
|
28
|
+
{ key: HOME_BUTTON_PERM_KEY },
|
|
29
|
+
)}
|
|
30
|
+
</Paragraph>
|
|
31
|
+
|
|
32
|
+
<PermissionFilter
|
|
33
|
+
permissionKey={HOME_BUTTON_PERM_KEY}
|
|
34
|
+
fallback={
|
|
35
|
+
<Alert
|
|
36
|
+
type="warning"
|
|
37
|
+
title={intl.formatMessage({
|
|
38
|
+
id: 'page.home.permissionDemo.fallbackTitle',
|
|
39
|
+
})}
|
|
40
|
+
content={intl.formatMessage(
|
|
41
|
+
{ id: 'page.home.permissionDemo.noPerm' },
|
|
42
|
+
{ key: HOME_BUTTON_PERM_KEY },
|
|
43
|
+
)}
|
|
44
|
+
/>
|
|
45
|
+
}
|
|
46
|
+
>
|
|
47
|
+
<Space>
|
|
48
|
+
<Button type="primary" status="success">
|
|
49
|
+
{intl.formatMessage({ id: 'page.home.permissionDemo.hasPerm' })}
|
|
50
|
+
</Button>
|
|
51
|
+
<Text type="success">
|
|
52
|
+
({HOME_BUTTON_PERM_KEY})
|
|
53
|
+
</Text>
|
|
54
|
+
</Space>
|
|
55
|
+
</PermissionFilter>
|
|
56
|
+
</Card>
|
|
9
57
|
</div>
|
|
10
58
|
);
|
|
11
59
|
};
|
|
@@ -1,30 +1,42 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { IUserInfo } from '@/common/auth/type';
|
|
1
|
+
import type { IUserInfo, IUserInfoApiData } from '@/common/auth/type';
|
|
3
2
|
import { getCurrentLocale, type SupportedLocale } from '@/common/locale';
|
|
3
|
+
import { request } from '@/common/request';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/** 新用户信息接口(与 OpenAPI `GET /api/user/info/` 一致,非旧 `/api/user/info`) */
|
|
6
|
+
const USER_INFO_API = '/api/user/info/';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* 语言代码到接口 lang 参数的映射
|
|
9
10
|
* - zh_CN: 1 (中文)
|
|
10
11
|
* - en: 2 (英文)
|
|
11
12
|
*/
|
|
12
|
-
const LOCALE_TO_LANG: Partial<Record<SupportedLocale, number>> = {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
};
|
|
13
|
+
// const LOCALE_TO_LANG: Partial<Record<SupportedLocale, number>> = {
|
|
14
|
+
// 'zh-CN': 1,
|
|
15
|
+
// 'en-US': 2,
|
|
16
|
+
// };
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
// function getApiLangParam(): number {
|
|
19
|
+
// const locale = getCurrentLocale();
|
|
20
|
+
// return LOCALE_TO_LANG[locale] ?? 1;
|
|
21
|
+
// }
|
|
22
|
+
|
|
23
|
+
function normalizeUserInfo(data: IUserInfoApiData): IUserInfo {
|
|
24
|
+
const name = data.name || '';
|
|
25
|
+
return {
|
|
26
|
+
...data,
|
|
27
|
+
app_perms: Array.isArray(data.app_perms) ? data.app_perms : [],
|
|
28
|
+
region_perms: Array.isArray(data.region_perms) ? data.region_perms : [],
|
|
29
|
+
menu_perms: Array.isArray(data.menu_perms) ? data.menu_perms : [],
|
|
30
|
+
button_perms: Array.isArray(data.button_perms) ? data.button_perms : [],
|
|
31
|
+
is_superuser: Boolean(data.is_superuser),
|
|
32
|
+
user_name: name,
|
|
33
|
+
username: name || data.email || '',
|
|
34
|
+
};
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
export interface IUserInfoResponse {
|
|
26
38
|
code: number;
|
|
27
|
-
data:
|
|
39
|
+
data: IUserInfoApiData;
|
|
28
40
|
msg: string;
|
|
29
41
|
}
|
|
30
42
|
|
|
@@ -34,19 +46,14 @@ export interface IUserInfoResponse {
|
|
|
34
46
|
* @throws 当接口返回非 200 code 时抛出错误
|
|
35
47
|
*/
|
|
36
48
|
export async function fetchUserInfo(): Promise<IUserInfo> {
|
|
37
|
-
const
|
|
38
|
-
const url = `${USER_INFO_API}?lang=${lang}`;
|
|
49
|
+
const url = USER_INFO_API;
|
|
39
50
|
|
|
40
51
|
const response = await request<IUserInfoResponse>(url, {
|
|
41
52
|
method: 'GET',
|
|
42
53
|
skipProxy: true,
|
|
43
54
|
});
|
|
44
55
|
if (response.code === 200 && response.data) {
|
|
45
|
-
|
|
46
|
-
if (!data.user_name) {
|
|
47
|
-
data.user_name = data.username || '';
|
|
48
|
-
}
|
|
49
|
-
return data;
|
|
56
|
+
return normalizeUserInfo(response.data);
|
|
50
57
|
}
|
|
51
58
|
throw new Error(response.msg || '获取用户信息失败');
|
|
52
59
|
}
|