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.
Files changed (56) hide show
  1. package/README.md +5 -20
  2. package/bin/mico.js +27 -62
  3. package/generators/micro-react/index.js +8 -0
  4. package/generators/micro-react/templates/.cursor/rules/always-read-docs.mdc +3 -0
  5. package/generators/micro-react/templates/.cursor/rules/project-overview.mdc +1 -0
  6. package/generators/micro-react/templates/CLAUDE.md +1 -0
  7. package/generators/micro-react/templates/README.md +1 -1
  8. package/generators/micro-react/templates/apps/layout/config/config.dev.ts +4 -2
  9. package/generators/micro-react/templates/apps/layout/config/config.prod.development.ts +2 -0
  10. package/generators/micro-react/templates/apps/layout/config/config.prod.testing.ts +2 -0
  11. package/generators/micro-react/templates/apps/layout/config/config.prod.ts +2 -0
  12. 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
  13. 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
  14. 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
  15. 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
  16. package/generators/micro-react/templates/apps/layout/mock/api.mock.ts +23 -31
  17. package/generators/micro-react/templates/apps/layout/mock/pages.ts +5 -6
  18. package/generators/micro-react/templates/apps/layout/package.json +2 -1
  19. package/generators/micro-react/templates/apps/layout/src/app.tsx +30 -2
  20. package/generators/micro-react/templates/apps/layout/src/common/auth/type.ts +15 -27
  21. package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +148 -85
  22. package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +2 -6
  23. package/generators/micro-react/templates/apps/layout/src/common/portal-data.ts +46 -2
  24. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +5 -1
  25. package/generators/micro-react/templates/apps/layout/src/components/PermissionFilter/index.tsx +51 -0
  26. package/generators/micro-react/templates/apps/layout/src/components/RightContent/AvatarDropdown.tsx +10 -1
  27. package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +3 -3
  28. package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +105 -60
  29. package/generators/micro-react/templates/apps/layout/src/locales/en-US.ts +17 -0
  30. package/generators/micro-react/templates/apps/layout/src/locales/zh-CN.ts +16 -0
  31. package/generators/micro-react/templates/apps/layout/src/pages/404/index.tsx +7 -3
  32. package/generators/micro-react/templates/apps/layout/src/pages/Home/index.less +5 -0
  33. package/generators/micro-react/templates/apps/layout/src/pages/Home/index.tsx +49 -1
  34. package/generators/micro-react/templates/apps/layout/src/services/user.ts +28 -21
  35. package/generators/micro-react/templates/packages/common-intl/README.md +77 -369
  36. package/generators/micro-react/templates/packages/common-intl/package.json +3 -13
  37. package/generators/micro-react/templates/packages/common-intl/src/index.ts +3 -6
  38. package/generators/micro-react/templates/packages/common-intl/src/intl.ts +20 -29
  39. package/generators/micro-react/templates/packages/common-intl/tsconfig.json +2 -4
  40. package/generators/subapp-react/index.js +28 -22
  41. package/generators/subapp-react/templates/homepage/README.md +1 -0
  42. package/generators/subapp-react/templates/homepage/config/config.prod.development.ts +1 -0
  43. package/generators/subapp-react/templates/homepage/config/config.prod.testing.ts +1 -0
  44. package/generators/subapp-react/templates/homepage/config/config.prod.ts +1 -0
  45. package/generators/subapp-react/templates/homepage/docs/feature-PermissionFilter/346/214/211/351/222/256/346/235/203/351/231/220.md +35 -0
  46. package/generators/subapp-react/templates/homepage/package.json +2 -1
  47. package/generators/subapp-react/templates/homepage/src/app.tsx +7 -0
  48. package/generators/subapp-react/templates/homepage/src/common/mainApp.ts +39 -2
  49. package/generators/subapp-react/templates/homepage/src/components/PermissionFilter/index.tsx +48 -0
  50. package/generators/subapp-react/templates/homepage/src/pages/index.tsx +35 -1
  51. package/lib/utils.js +0 -1
  52. package/package.json +2 -2
  53. package/generators/micro-react/templates/apps/layout/docs/common-intl.md +0 -372
  54. package/generators/micro-react/templates/packages/common-intl/src/indexedDBUtils.ts +0 -51
  55. package/generators/micro-react/templates/packages/common-intl/src/utils.ts +0 -482
  56. package/generators/micro-react/templates/packages/common-intl/vite.config.ts +0 -25
@@ -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);
@@ -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;
@@ -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
- // isMenuAllowed 内部已对每个菜单项单独检查 isNoPermissionRoute,
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
- sideMenus: (currentUser?.side_menus || []) as string[],
131
+ menuPerms: currentUser?.menu_perms || [],
132
132
  });
133
133
  return parseMenuItems(filteredMenus);
134
- }, [currentUser?.is_superuser, currentUser?.side_menus]);
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
- sideMenus: (currentUser?.side_menus || []) as string[],
45
+ menuPerms: currentUser?.menu_perms || [],
48
46
  }),
49
- [currentUser?.is_superuser, currentUser?.side_menus],
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
- if (isAuthDisabled()) return false;
78
- if (isNoPermissionRoute(location.pathname)) return false;
79
- // 非动态路由,交给 Umi 处理(404 等)
80
- if (!currentRoute) return false;
81
- if (isSuperuserUser(currentUser?.is_superuser)) return false;
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
- layoutLogger.log('isForbidden (menu check):', {
90
- pathname: location.pathname,
91
- inAllMenu: true,
92
- inAllowed: !!inAllowed,
93
- forbidden,
78
+ if (isAuthDisabled()) {
79
+ layoutLogger.log('routePermission', {
80
+ verdict: 'skip',
81
+ reason: 'disableAuth',
82
+ pathname,
83
+ ...permCtx,
94
84
  });
95
-
96
- return forbidden;
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
- // Tier 2: 隐藏页面级权限(仅在 PAGES 数据可用时生效)
100
- if (!hasPages()) return false;
110
+ const page =
111
+ currentRoute.pageConfig ??
112
+ (hasPages() ? findPageByPath(getPages(), pathname) : undefined);
101
113
 
102
- const page = findPageByPath(getPages(), location.pathname);
103
- if (!page) return false;
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
- if (page.adminOnly) {
106
- layoutLogger.log('isForbidden (hidden page adminOnly):', {
107
- pathname: location.pathname,
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
- return true;
164
+
165
+ return forbidden;
111
166
  }
112
167
 
113
- if (page.accessControlEnabled) {
114
- const sideMenus = (currentUser?.side_menus || []) as string[];
115
- const forbidden = !page.routeKey || !sideMenus.includes(page.routeKey);
168
+ const inAllMenu = findRouteByPath(allMenuRoutes, pathname);
169
+ if (inAllMenu) {
170
+ const inAllowed = findRouteByPath(allowedMenuRoutes, pathname);
171
+ const forbidden = !inAllowed;
116
172
 
117
- layoutLogger.log('isForbidden (hidden page accessControl):', {
118
- pathname: location.pathname,
119
- pageId: page.id,
120
- routeKey: page.routeKey,
121
- hasSideMenuMatch: !forbidden,
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?.side_menus,
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
- sideMenusCount: currentUser?.side_menus?.length,
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 { extractRoutes, filterMenuItems, type MenuFilterOptions } from '@/common/menu';
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
- sideMenus: (currentUser?.side_menus || []) as string[],
37
+ menuPerms: currentUser?.menu_perms || [],
34
38
  }),
35
- [currentUser?.is_superuser, currentUser?.side_menus],
39
+ [currentUser?.is_superuser, currentUser?.menu_perms],
36
40
  );
37
41
 
38
42
  const firstAvailablePath = useMemo(
@@ -4,3 +4,8 @@
4
4
  padding-top: 30px;
5
5
  color: @color-text-1;
6
6
  }
7
+
8
+ .demoCard {
9
+ margin-top: 16px;
10
+ max-width: 720px;
11
+ }
@@ -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
- {intl.formatMessage({ id: 'page.home.title' })}
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 { request } from '@/common/request';
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
- const USER_INFO_API = '/api/user/info';
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
- 'zh-CN': 1,
14
- 'en-US': 2,
15
- };
13
+ // const LOCALE_TO_LANG: Partial<Record<SupportedLocale, number>> = {
14
+ // 'zh-CN': 1,
15
+ // 'en-US': 2,
16
+ // };
16
17
 
17
- /**
18
- * 获取当前语言对应的 API lang 参数
19
- */
20
- function getApiLangParam(): number {
21
- const locale = getCurrentLocale();
22
- return LOCALE_TO_LANG[locale] ?? 1;
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: IUserInfo;
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 lang = getApiLangParam();
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
- const data = response.data;
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
  }