generator-mico-cli 0.2.28 → 0.2.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -20
- package/bin/mico.js +27 -62
- package/generators/micro-react/index.js +25 -1
- package/generators/micro-react/templates/.cursor/rules/always-read-docs.mdc +3 -0
- package/generators/micro-react/templates/.cursor/rules/project-overview.mdc +1 -0
- package/generators/micro-react/templates/CICD/start_dev.sh +11 -0
- package/generators/micro-react/templates/CICD/start_local.sh +9 -0
- package/generators/micro-react/templates/CICD/start_prod.sh +13 -0
- package/generators/micro-react/templates/CICD/start_test.sh +11 -0
- package/generators/micro-react/templates/CLAUDE.md +1 -0
- package/generators/micro-react/templates/README.md +1 -1
- package/generators/micro-react/templates/apps/layout/config/config.dev.ts +13 -5
- package/generators/micro-react/templates/apps/layout/config/config.prod.development.ts +12 -0
- package/generators/micro-react/templates/apps/layout/config/config.prod.testing.ts +12 -0
- package/generators/micro-react/templates/apps/layout/config/config.prod.ts +14 -0
- package/generators/micro-react/templates/apps/layout/docs/feature-PermissionFilter/346/214/211/351/222/256/346/235/203/351/231/220.md +116 -0
- package/generators/micro-react/templates/apps/layout/docs/feature-/345/233/275/351/231/205/345/214/226.md +121 -0
- package/generators/micro-react/templates/apps/layout/docs/feature-/345/276/256/345/211/215/347/253/257/346/250/241/345/274/217.md +8 -0
- package/generators/micro-react/templates/apps/layout/docs/feature-/350/217/234/345/215/225/346/235/203/351/231/220/346/216/247/345/210/266.md +83 -77
- package/generators/micro-react/templates/apps/layout/docs/feature-/350/267/257/347/224/261/344/270/216/350/217/234/345/215/225/350/247/243/350/200/246.md +50 -35
- package/generators/micro-react/templates/apps/layout/docs/feature-/350/267/257/347/224/261/346/235/203/351/231/220/346/227/245/345/277/227.md +162 -0
- package/generators/micro-react/templates/apps/layout/mock/api.mock.ts +23 -31
- package/generators/micro-react/templates/apps/layout/mock/menus.ts +14 -0
- package/generators/micro-react/templates/apps/layout/mock/pages.ts +27 -8
- package/generators/micro-react/templates/apps/layout/package.json +2 -0
- package/generators/micro-react/templates/apps/layout/src/app.tsx +85 -4
- package/generators/micro-react/templates/apps/layout/src/common/auth/index.ts +3 -0
- package/generators/micro-react/templates/apps/layout/src/common/auth/tenant.ts +25 -0
- package/generators/micro-react/templates/apps/layout/src/common/auth/type.ts +41 -27
- package/generators/micro-react/templates/apps/layout/src/common/intl/formatLayoutMessage.ts +30 -0
- package/generators/micro-react/templates/apps/layout/src/common/intl/index.ts +6 -0
- package/generators/micro-react/templates/apps/layout/src/common/intl/intlRuntime.ts +14 -0
- package/generators/micro-react/templates/apps/layout/src/common/intl/localeMapping.ts +30 -0
- package/generators/micro-react/templates/apps/layout/src/common/intl/types.ts +14 -0
- package/generators/micro-react/templates/apps/layout/src/common/intl/useLayoutIntl.ts +40 -0
- package/generators/micro-react/templates/apps/layout/src/common/logger.ts +3 -4
- package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +148 -85
- package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +29 -6
- package/generators/micro-react/templates/apps/layout/src/common/micro/types.ts +23 -0
- package/generators/micro-react/templates/apps/layout/src/common/portal-data.ts +46 -2
- package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +74 -15
- package/generators/micro-react/templates/apps/layout/src/common/request/token-refresh.ts +2 -0
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +32 -6
- package/generators/micro-react/templates/apps/layout/src/components/PermissionFilter/index.tsx +51 -0
- package/generators/micro-react/templates/apps/layout/src/components/RightContent/AvatarDropdown.tsx +10 -1
- package/generators/micro-react/templates/apps/layout/src/components/RightContent/TenantDropdown.tsx +76 -0
- package/generators/micro-react/templates/apps/layout/src/components/RightContent/index.ts +1 -0
- package/generators/micro-react/templates/apps/layout/src/components/RightContent/tenant-dropdown.less +48 -0
- package/generators/micro-react/templates/apps/layout/src/constants/index.ts +1 -0
- package/generators/micro-react/templates/apps/layout/src/hooks/index.ts +1 -0
- package/generators/micro-react/templates/apps/layout/src/hooks/useMenuState.ts +18 -0
- package/generators/micro-react/templates/apps/layout/src/hooks/useTenant.ts +41 -0
- package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.tsx +4 -1
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +21 -9
- package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +105 -60
- package/generators/micro-react/templates/apps/layout/src/locales/en-US.ts +28 -0
- package/generators/micro-react/templates/apps/layout/src/locales/zh-CN.ts +26 -0
- package/generators/micro-react/templates/apps/layout/src/pages/404/index.tsx +7 -3
- package/generators/micro-react/templates/apps/layout/src/pages/Home/index.less +32 -0
- package/generators/micro-react/templates/apps/layout/src/pages/Home/index.tsx +148 -4
- package/generators/micro-react/templates/apps/layout/src/requestErrorConfig.ts +2 -1
- package/generators/micro-react/templates/apps/layout/src/services/user.ts +79 -21
- package/generators/micro-react/templates/apps/layout/typings.d.ts +16 -0
- package/generators/micro-react/templates/docs/package-shared.md +189 -0
- package/generators/micro-react/templates/package.json +1 -1
- package/generators/micro-react/templates/packages/common-intl/README.md +78 -368
- package/generators/micro-react/templates/packages/common-intl/package.json +3 -13
- package/generators/micro-react/templates/packages/common-intl/src/index.ts +5 -6
- package/generators/micro-react/templates/packages/common-intl/src/intl.ts +115 -28
- package/generators/micro-react/templates/packages/common-intl/src/umiLocaleBridge.ts +101 -0
- package/generators/micro-react/templates/packages/common-intl/tsconfig.json +2 -4
- package/generators/micro-react/templates/packages/shared/README.md +120 -0
- package/generators/micro-react/templates/packages/shared/package.json +26 -0
- package/generators/micro-react/templates/packages/shared/services/common/index.ts +43 -0
- package/generators/micro-react/templates/packages/shared/services/index.ts +21 -0
- package/generators/micro-react/templates/packages/shared/services/request.ts +43 -0
- package/generators/micro-react/templates/packages/shared/timezone/index.ts +228 -0
- package/generators/micro-react/templates/packages/shared/tsconfig.json +20 -0
- package/generators/micro-react/templates/scripts/apply-sentry-plugin.ts +6 -1
- package/generators/micro-react/templates/turbo.json +9 -1
- package/generators/subapp-react/index.js +28 -22
- package/generators/subapp-react/templates/homepage/README.md +1 -0
- package/generators/subapp-react/templates/homepage/config/config.prod.development.ts +1 -0
- package/generators/subapp-react/templates/homepage/config/config.prod.testing.ts +1 -0
- package/generators/subapp-react/templates/homepage/config/config.prod.ts +1 -0
- package/generators/subapp-react/templates/homepage/config/config.ts +10 -0
- package/generators/subapp-react/templates/homepage/docs/feature-PermissionFilter/346/214/211/351/222/256/346/235/203/351/231/220.md +35 -0
- package/generators/subapp-react/templates/homepage/docs/feature-/345/233/275/351/231/205/345/214/226.md +124 -0
- package/generators/subapp-react/templates/homepage/package.json +3 -1
- package/generators/subapp-react/templates/homepage/src/app.tsx +104 -2
- package/generators/subapp-react/templates/homepage/src/common/intl/index.ts +15 -0
- package/generators/subapp-react/templates/homepage/src/common/intl/intlRuntime.ts +14 -0
- package/generators/subapp-react/templates/homepage/src/common/intl/localeMapping.ts +24 -0
- package/generators/subapp-react/templates/homepage/src/common/intl/subappIntlConfig.ts +28 -0
- package/generators/subapp-react/templates/homepage/src/common/intl/subappLocale.ts +18 -0
- package/generators/subapp-react/templates/homepage/src/common/intl/subappOwnIntl.ts +63 -0
- package/generators/subapp-react/templates/homepage/src/common/intl/types.ts +14 -0
- package/generators/subapp-react/templates/homepage/src/common/intl/useSubappIntl.ts +61 -0
- package/generators/subapp-react/templates/homepage/src/common/locale.ts +80 -0
- package/generators/subapp-react/templates/homepage/src/common/mainApp.ts +41 -2
- package/generators/subapp-react/templates/homepage/src/components/PermissionFilter/index.tsx +48 -0
- package/generators/subapp-react/templates/homepage/src/locales/en-US.ts +6 -0
- package/generators/subapp-react/templates/homepage/src/locales/zh-CN.ts +6 -0
- package/generators/subapp-react/templates/homepage/src/pages/index.less +10 -0
- package/generators/subapp-react/templates/homepage/src/pages/index.tsx +86 -1
- package/generators/subapp-react/templates/homepage/typings.d.ts +12 -0
- package/lib/utils.js +0 -1
- package/package.json +2 -2
- package/generators/micro-react/templates/apps/layout/docs/common-intl.md +0 -372
- package/generators/micro-react/templates/packages/common-intl/src/indexedDBUtils.ts +0 -51
- package/generators/micro-react/templates/packages/common-intl/src/utils.ts +0 -482
- package/generators/micro-react/templates/packages/common-intl/vite.config.ts +0 -25
|
@@ -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 {
|
|
@@ -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.
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
164
|
+
sider.removeEventListener('mouseenter', handleMouseEnter);
|
|
165
|
+
sider.removeEventListener('mouseleave', handleMouseLeave);
|
|
154
166
|
};
|
|
155
|
-
}, [
|
|
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
|
-
|
|
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,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 {
|
|
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(
|
|
@@ -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
|
|
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
|
|
5
|
-
|
|
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
|
-
{
|
|
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("{DEMO_DATE_STR}"):</Text>{' '}
|
|
137
|
+
{tzDemo.parsed}
|
|
138
|
+
</Text>
|
|
139
|
+
<Text>
|
|
140
|
+
<Text bold>getShortcutDateRange("week"):</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(
|
|
136
|
+
notifyHostError(createAuthError(notifyInfo, status, 'http'));
|
|
136
137
|
} else {
|
|
137
138
|
// 其他业务错误
|
|
138
139
|
notifyHostError(
|