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.
Files changed (112) hide show
  1. package/README.md +7 -20
  2. package/bin/mico.js +27 -62
  3. package/generators/micro-react/index.js +25 -1
  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/CICD/start_dev.sh +11 -0
  7. package/generators/micro-react/templates/CICD/start_local.sh +9 -0
  8. package/generators/micro-react/templates/CICD/start_prod.sh +13 -0
  9. package/generators/micro-react/templates/CICD/start_test.sh +11 -0
  10. package/generators/micro-react/templates/CLAUDE.md +1 -0
  11. package/generators/micro-react/templates/README.md +1 -1
  12. package/generators/micro-react/templates/apps/layout/config/config.dev.ts +13 -5
  13. package/generators/micro-react/templates/apps/layout/config/config.prod.development.ts +12 -0
  14. package/generators/micro-react/templates/apps/layout/config/config.prod.testing.ts +12 -0
  15. package/generators/micro-react/templates/apps/layout/config/config.prod.ts +14 -0
  16. 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
  17. package/generators/micro-react/templates/apps/layout/docs/feature-/345/233/275/351/231/205/345/214/226.md +121 -0
  18. 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
  19. 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
  20. 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
  21. 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
  22. package/generators/micro-react/templates/apps/layout/mock/api.mock.ts +23 -31
  23. package/generators/micro-react/templates/apps/layout/mock/menus.ts +14 -0
  24. package/generators/micro-react/templates/apps/layout/mock/pages.ts +27 -8
  25. package/generators/micro-react/templates/apps/layout/package.json +2 -0
  26. package/generators/micro-react/templates/apps/layout/src/app.tsx +85 -4
  27. package/generators/micro-react/templates/apps/layout/src/common/auth/index.ts +3 -0
  28. package/generators/micro-react/templates/apps/layout/src/common/auth/tenant.ts +25 -0
  29. package/generators/micro-react/templates/apps/layout/src/common/auth/type.ts +41 -27
  30. package/generators/micro-react/templates/apps/layout/src/common/intl/formatLayoutMessage.ts +30 -0
  31. package/generators/micro-react/templates/apps/layout/src/common/intl/index.ts +6 -0
  32. package/generators/micro-react/templates/apps/layout/src/common/intl/intlRuntime.ts +14 -0
  33. package/generators/micro-react/templates/apps/layout/src/common/intl/localeMapping.ts +30 -0
  34. package/generators/micro-react/templates/apps/layout/src/common/intl/types.ts +14 -0
  35. package/generators/micro-react/templates/apps/layout/src/common/intl/useLayoutIntl.ts +40 -0
  36. package/generators/micro-react/templates/apps/layout/src/common/logger.ts +3 -4
  37. package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +148 -85
  38. package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +29 -6
  39. package/generators/micro-react/templates/apps/layout/src/common/micro/types.ts +23 -0
  40. package/generators/micro-react/templates/apps/layout/src/common/portal-data.ts +46 -2
  41. package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +74 -15
  42. package/generators/micro-react/templates/apps/layout/src/common/request/token-refresh.ts +2 -0
  43. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +32 -6
  44. package/generators/micro-react/templates/apps/layout/src/components/PermissionFilter/index.tsx +51 -0
  45. package/generators/micro-react/templates/apps/layout/src/components/RightContent/AvatarDropdown.tsx +10 -1
  46. package/generators/micro-react/templates/apps/layout/src/components/RightContent/TenantDropdown.tsx +76 -0
  47. package/generators/micro-react/templates/apps/layout/src/components/RightContent/index.ts +1 -0
  48. package/generators/micro-react/templates/apps/layout/src/components/RightContent/tenant-dropdown.less +48 -0
  49. package/generators/micro-react/templates/apps/layout/src/constants/index.ts +1 -0
  50. package/generators/micro-react/templates/apps/layout/src/hooks/index.ts +1 -0
  51. package/generators/micro-react/templates/apps/layout/src/hooks/useMenuState.ts +18 -0
  52. package/generators/micro-react/templates/apps/layout/src/hooks/useTenant.ts +41 -0
  53. package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.tsx +4 -1
  54. package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +21 -9
  55. package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +105 -60
  56. package/generators/micro-react/templates/apps/layout/src/locales/en-US.ts +28 -0
  57. package/generators/micro-react/templates/apps/layout/src/locales/zh-CN.ts +26 -0
  58. package/generators/micro-react/templates/apps/layout/src/pages/404/index.tsx +7 -3
  59. package/generators/micro-react/templates/apps/layout/src/pages/Home/index.less +32 -0
  60. package/generators/micro-react/templates/apps/layout/src/pages/Home/index.tsx +148 -4
  61. package/generators/micro-react/templates/apps/layout/src/requestErrorConfig.ts +2 -1
  62. package/generators/micro-react/templates/apps/layout/src/services/user.ts +79 -21
  63. package/generators/micro-react/templates/apps/layout/typings.d.ts +16 -0
  64. package/generators/micro-react/templates/docs/package-shared.md +189 -0
  65. package/generators/micro-react/templates/package.json +1 -1
  66. package/generators/micro-react/templates/packages/common-intl/README.md +78 -368
  67. package/generators/micro-react/templates/packages/common-intl/package.json +3 -13
  68. package/generators/micro-react/templates/packages/common-intl/src/index.ts +5 -6
  69. package/generators/micro-react/templates/packages/common-intl/src/intl.ts +115 -28
  70. package/generators/micro-react/templates/packages/common-intl/src/umiLocaleBridge.ts +101 -0
  71. package/generators/micro-react/templates/packages/common-intl/tsconfig.json +2 -4
  72. package/generators/micro-react/templates/packages/shared/README.md +120 -0
  73. package/generators/micro-react/templates/packages/shared/package.json +26 -0
  74. package/generators/micro-react/templates/packages/shared/services/common/index.ts +43 -0
  75. package/generators/micro-react/templates/packages/shared/services/index.ts +21 -0
  76. package/generators/micro-react/templates/packages/shared/services/request.ts +43 -0
  77. package/generators/micro-react/templates/packages/shared/timezone/index.ts +228 -0
  78. package/generators/micro-react/templates/packages/shared/tsconfig.json +20 -0
  79. package/generators/micro-react/templates/scripts/apply-sentry-plugin.ts +6 -1
  80. package/generators/micro-react/templates/turbo.json +9 -1
  81. package/generators/subapp-react/index.js +28 -22
  82. package/generators/subapp-react/templates/homepage/README.md +1 -0
  83. package/generators/subapp-react/templates/homepage/config/config.prod.development.ts +1 -0
  84. package/generators/subapp-react/templates/homepage/config/config.prod.testing.ts +1 -0
  85. package/generators/subapp-react/templates/homepage/config/config.prod.ts +1 -0
  86. package/generators/subapp-react/templates/homepage/config/config.ts +10 -0
  87. package/generators/subapp-react/templates/homepage/docs/feature-PermissionFilter/346/214/211/351/222/256/346/235/203/351/231/220.md +35 -0
  88. package/generators/subapp-react/templates/homepage/docs/feature-/345/233/275/351/231/205/345/214/226.md +124 -0
  89. package/generators/subapp-react/templates/homepage/package.json +3 -1
  90. package/generators/subapp-react/templates/homepage/src/app.tsx +104 -2
  91. package/generators/subapp-react/templates/homepage/src/common/intl/index.ts +15 -0
  92. package/generators/subapp-react/templates/homepage/src/common/intl/intlRuntime.ts +14 -0
  93. package/generators/subapp-react/templates/homepage/src/common/intl/localeMapping.ts +24 -0
  94. package/generators/subapp-react/templates/homepage/src/common/intl/subappIntlConfig.ts +28 -0
  95. package/generators/subapp-react/templates/homepage/src/common/intl/subappLocale.ts +18 -0
  96. package/generators/subapp-react/templates/homepage/src/common/intl/subappOwnIntl.ts +63 -0
  97. package/generators/subapp-react/templates/homepage/src/common/intl/types.ts +14 -0
  98. package/generators/subapp-react/templates/homepage/src/common/intl/useSubappIntl.ts +61 -0
  99. package/generators/subapp-react/templates/homepage/src/common/locale.ts +80 -0
  100. package/generators/subapp-react/templates/homepage/src/common/mainApp.ts +41 -2
  101. package/generators/subapp-react/templates/homepage/src/components/PermissionFilter/index.tsx +48 -0
  102. package/generators/subapp-react/templates/homepage/src/locales/en-US.ts +6 -0
  103. package/generators/subapp-react/templates/homepage/src/locales/zh-CN.ts +6 -0
  104. package/generators/subapp-react/templates/homepage/src/pages/index.less +10 -0
  105. package/generators/subapp-react/templates/homepage/src/pages/index.tsx +86 -1
  106. package/generators/subapp-react/templates/homepage/typings.d.ts +12 -0
  107. package/lib/utils.js +0 -1
  108. package/package.json +2 -2
  109. package/generators/micro-react/templates/apps/layout/docs/common-intl.md +0 -372
  110. package/generators/micro-react/templates/packages/common-intl/src/indexedDBUtils.ts +0 -51
  111. package/generators/micro-react/templates/packages/common-intl/src/utils.ts +0 -482
  112. package/generators/micro-react/templates/packages/common-intl/vite.config.ts +0 -25
@@ -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 {
@@ -140,19 +140,31 @@ const LayoutMenu: React.FC<LayoutMenuProps> = () => {
140
140
  collapsed,
141
141
  handleClickMenuItem,
142
142
  handleCollapsed,
143
+ handleMouseEnter,
144
+ handleMouseLeave,
143
145
  setOpenKeys,
144
146
  } = useMenuState({ menuItems });
145
147
 
146
148
  useEffect(() => {
147
- document.documentElement.style.setProperty(
148
- '--sider-width',
149
- collapsed ? '48px' : '200px',
150
- );
149
+ const layoutContent = document.querySelector('.layout-content');
150
+ if (layoutContent) {
151
+ (layoutContent as HTMLElement).style.setProperty(
152
+ '--sider-width',
153
+ collapsed ? '48px' : '200px',
154
+ );
155
+ }
156
+ }, [collapsed]);
151
157
 
158
+ useEffect(() => {
159
+ const sider = siderRef.current;
160
+ if (!sider) return;
161
+ sider.addEventListener('mouseenter', handleMouseEnter);
162
+ sider.addEventListener('mouseleave', handleMouseLeave);
152
163
  return () => {
153
- document.documentElement.style.removeProperty('--sider-width');
164
+ sider.removeEventListener('mouseenter', handleMouseEnter);
165
+ sider.removeEventListener('mouseleave', handleMouseLeave);
154
166
  };
155
- }, [collapsed]);
167
+ }, [handleMouseEnter, handleMouseLeave]);
156
168
 
157
169
  // 点击触发按钮图标
158
170
  const clickTriggerBtnIcon = collapsed
@@ -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,34 @@ 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',
40
+
41
+ // SSO auth failure modal
42
+ 'sso.auth.failure.modal.title': 'Login Required',
43
+ 'sso.auth.failure.modal.content':
44
+ 'Auto login failed. Would you like to try logging in again?',
45
+ 'sso.auth.failure.modal.ok': 'Re-login',
46
+ 'sso.auth.failure.modal.cancel': 'Cancel',
47
+ sso_auth_failure_modal_title: 'Login Required',
48
+ sso_auth_failure_modal_content: 'Auto login failed. Would you like to try logging in again?',
49
+ sso_auth_failure_modal_ok: 'Re-login',
50
+ sso_auth_failure_modal_cancel: 'Cancel',
23
51
 
24
52
  // AvatarDropdown
25
53
  'avatar.language': 'Language',
@@ -19,6 +19,32 @@ 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': '小组管理',
38
+
39
+ // SSO 认证失败弹框
40
+ 'sso.auth.failure.modal.title': '登录提示',
41
+ 'sso.auth.failure.modal.content': '自动登录失败,是否重新尝试登录?',
42
+ 'sso.auth.failure.modal.ok': '重新登录',
43
+ 'sso.auth.failure.modal.cancel': '取消',
44
+ sso_auth_failure_modal_title: '登录提示',
45
+ sso_auth_failure_modal_content: '自动登录失败,是否重新尝试登录?',
46
+ sso_auth_failure_modal_ok: '重新登录',
47
+ sso_auth_failure_modal_cancel: '取消',
22
48
 
23
49
  // AvatarDropdown
24
50
  '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,35 @@
4
4
  padding-top: 30px;
5
5
  color: @color-text-1;
6
6
  }
7
+
8
+ .title {
9
+ margin: 0 0 12px;
10
+ font-size: 22px;
11
+ font-weight: 600;
12
+ }
13
+
14
+ .hint {
15
+ margin: 0 0 8px;
16
+ font-size: 14px;
17
+ line-height: 1.6;
18
+ color: @color-text-2;
19
+ }
20
+
21
+ .meta {
22
+ margin: 0;
23
+ font-size: 12px;
24
+ font-family: ui-monospace, monospace;
25
+ color: @color-text-3;
26
+ }
27
+
28
+ .demoCard {
29
+ margin-top: 16px;
30
+ max-width: 720px;
31
+ }
32
+
33
+ .tzDemoList {
34
+ width: 100%;
35
+ font-family: ui-monospace, monospace;
36
+ font-size: 12px;
37
+ line-height: 1.8;
38
+ }
@@ -1,11 +1,155 @@
1
- import { useIntl } from '@umijs/max';
1
+ import PermissionFilter from '@/components/PermissionFilter';
2
+ import { useLayoutIntl } from '@/common/intl';
3
+ import { Alert, Button, Card, Space, Typography } from '@mico-platform/ui';
4
+ import type { FC } from 'react';
5
+ import {
6
+ TIMEZONE,
7
+ formatTimestampToDateTime,
8
+ formatTimestampToDateTimeWithTimezone,
9
+ getShortcutDateRange,
10
+ parseDateInTimezone,
11
+ utcOffsetToTimeZoneStr,
12
+ utcOffsetToTimezone,
13
+ } from '<%= packageScope %>/shared/timezone';
2
14
  import styles from './index.less';
3
15
 
4
- const HomePage: React.FC = () => {
5
- const intl = useIntl();
16
+ const { Paragraph, Text } = Typography;
17
+
18
+ /**
19
+ * shared/timezone 示例:所有函数依赖 app.tsx 顶层的 `configureTimezone({ getTimezone })` 注入。
20
+ * 这里用一个固定时间戳做确定性展示;业务实际使用时通常传 `Date.now()` 或后端下发值。
21
+ */
22
+ const DEMO_TIMESTAMP = 1730721621000;
23
+ const DEMO_DATE_STR = '2025-11-04 16:20:21';
24
+
25
+ /** 与 mock GET /user/info/ button_perms 中的示例 key 对齐 */
26
+ const HOME_BUTTON_PERM_KEY = 'cs_web_btn_home_demo';
27
+
28
+ /**
29
+ * 国际化示例:通过 `useLayoutIntl` 适配层统一调用。
30
+ * - 未配置 `__MICO_CONFIG__.commonIntl`:等同 `useIntl().formatMessage`,读本地 `locales/*`
31
+ * - 已配置:优先 <%= packageScope %>/common-intl 的 `i18n()`,无命中再 Umi
32
+ */
33
+ const HomePage: FC = () => {
34
+ const { formatMessage, commonIntlEnabled, locale } = useLayoutIntl();
35
+
36
+ // shared/timezone 示例输出
37
+ const tzDemo = {
38
+ utcOffset8: utcOffsetToTimezone(8),
39
+ utcOffsetMinus5: utcOffsetToTimezone(-5),
40
+ iana8: utcOffsetToTimeZoneStr(8),
41
+ parsed: parseDateInTimezone(DEMO_DATE_STR).format('YYYY-MM-DD HH:mm:ss Z'),
42
+ thisWeek: (() => {
43
+ const [start, end] = getShortcutDateRange('week');
44
+ return `${start.format('MM-DD')} ~ ${end.format('MM-DD')}`;
45
+ })(),
46
+ formattedDemo: formatTimestampToDateTime(DEMO_TIMESTAMP),
47
+ formattedWithTz: formatTimestampToDateTimeWithTimezone(
48
+ DEMO_TIMESTAMP,
49
+ TIMEZONE['UTC+8'],
50
+ ),
51
+ };
52
+
6
53
  return (
7
54
  <div className={styles.container}>
8
- {intl.formatMessage({ id: 'page.home.title' })}
55
+ <h1 className={styles.title}>
56
+ {
57
+ formatMessage({
58
+ id: 'page.home.title',
59
+ defaultMessage: '首页',
60
+ })
61
+ }
62
+ </h1>
63
+ <p className={styles.hint}>
64
+ common-intl 优先样例:{formatMessage({
65
+ id: 'cs_web_workbench_userlist_chat',
66
+ defaultMessage: '看到该文案代表未启用 commonIntl 或中台无该 key',
67
+ })}
68
+ </p>
69
+ <p className={styles.hint}>
70
+ Umi 兜底样例:{formatMessage({
71
+ id: 'page.home.title',
72
+ defaultMessage: '看到该文案代表仅 Umi 或兜底路径',
73
+ })}
74
+ </p>
75
+ <p className={styles.meta}>
76
+ commonIntlEnabled: {String(commonIntlEnabled)} · locale: {locale}
77
+ </p>
78
+
79
+ <Card
80
+ className={styles.demoCard}
81
+ title={formatMessage({ id: 'page.home.permissionDemo.title' })}
82
+ >
83
+ <Paragraph type="secondary">
84
+ {formatMessage(
85
+ { id: 'page.home.permissionDemo.desc' },
86
+ { key: HOME_BUTTON_PERM_KEY },
87
+ )}
88
+ </Paragraph>
89
+
90
+ <PermissionFilter
91
+ permissionKey={HOME_BUTTON_PERM_KEY}
92
+ fallback={
93
+ <Alert
94
+ type="warning"
95
+ title={formatMessage({
96
+ id: 'page.home.permissionDemo.fallbackTitle',
97
+ })}
98
+ content={formatMessage(
99
+ { id: 'page.home.permissionDemo.noPerm' },
100
+ { key: HOME_BUTTON_PERM_KEY },
101
+ )}
102
+ />
103
+ }
104
+ >
105
+ <Space>
106
+ <Button type="primary" status="success">
107
+ {formatMessage({ id: 'page.home.permissionDemo.hasPerm' })}
108
+ </Button>
109
+ <Text type="success">
110
+ ({HOME_BUTTON_PERM_KEY})
111
+ </Text>
112
+ </Space>
113
+ </PermissionFilter>
114
+ </Card>
115
+
116
+ <Card
117
+ className={styles.demoCard}
118
+ title={<>shared/timezone 示例</>}
119
+ >
120
+ <Paragraph type="secondary">
121
+ 所有函数依赖 app.tsx 顶层的 <Text code>configureTimezone</Text>{' '}
122
+ 注入;时区实时取自 <Text code>@/common/helpers#getTimezone</Text>。
123
+ </Paragraph>
124
+
125
+ <Space direction="vertical" size="mini" className={styles.tzDemoList}>
126
+ <Text>
127
+ <Text bold>utcOffsetToTimezone(8):</Text> {tzDemo.utcOffset8}
128
+ </Text>
129
+ <Text>
130
+ <Text bold>utcOffsetToTimezone(-5):</Text> {tzDemo.utcOffsetMinus5}
131
+ </Text>
132
+ <Text>
133
+ <Text bold>utcOffsetToTimeZoneStr(8):</Text> {tzDemo.iana8}
134
+ </Text>
135
+ <Text>
136
+ <Text bold>parseDateInTimezone(&quot;{DEMO_DATE_STR}&quot;):</Text>{' '}
137
+ {tzDemo.parsed}
138
+ </Text>
139
+ <Text>
140
+ <Text bold>getShortcutDateRange(&quot;week&quot;):</Text>{' '}
141
+ {tzDemo.thisWeek}
142
+ </Text>
143
+ <Text>
144
+ <Text bold>formatTimestampToDateTime(demo):</Text>{' '}
145
+ {String(tzDemo.formattedDemo)}
146
+ </Text>
147
+ <Text>
148
+ <Text bold>formatTimestampToDateTimeWithTimezone(demo, UTC+8):</Text>{' '}
149
+ {String(tzDemo.formattedWithTz)}
150
+ </Text>
151
+ </Space>
152
+ </Card>
9
153
  </div>
10
154
  );
11
155
  };
@@ -131,8 +131,9 @@ export const errorConfig: RequestConfig = {
131
131
 
132
132
  // 根据 HTTP 状态码区分鉴权错误和业务错误
133
133
  if (status === 401 || status === 403) {
134
+ const notifyInfo = `鉴权失败,状态码:${status},可能登录态过期,请重新登录`;
134
135
  // 鉴权失败
135
- notifyHostError(createAuthError(errorMessage, status, 'http'));
136
+ notifyHostError(createAuthError(notifyInfo, status, 'http'));
136
137
  } else {
137
138
  // 其他业务错误
138
139
  notifyHostError(