generator-mico-cli 0.2.20 → 0.2.22
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 +29 -0
- package/bin/mico.js +124 -5
- package/generators/micro-react/index.js +76 -17
- package/generators/micro-react/templates/.cursor/rules/always-read-docs.mdc +14 -4
- package/generators/micro-react/templates/.cursor/rules/layout-app.mdc +36 -26
- package/generators/micro-react/templates/.cursor/rules/project-overview.mdc +5 -2
- package/generators/micro-react/templates/CLAUDE.md +15 -7
- package/generators/micro-react/templates/_gitignore +2 -0
- package/generators/micro-react/templates/apps/layout/config/config.dev.ts +7 -3
- package/generators/micro-react/templates/apps/layout/config/config.ts +21 -0
- package/generators/micro-react/templates/apps/layout/config/routes.ts +0 -5
- package/generators/micro-react/templates/apps/layout/docs/common-intl.md +8 -6
- 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 +65 -37
- 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 +112 -48
- 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 +179 -0
- package/generators/micro-react/templates/apps/layout/docs/utils-timezone.md +4 -2
- package/generators/micro-react/templates/apps/layout/mock/menus.ts +89 -139
- package/generators/micro-react/templates/apps/layout/mock/pages.ts +83 -0
- package/generators/micro-react/templates/apps/layout/package.json +3 -2
- package/generators/micro-react/templates/apps/layout/src/app.tsx +10 -8
- package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +121 -58
- package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +35 -4
- package/generators/micro-react/templates/apps/layout/src/common/micro-prefetch.ts +3 -2
- package/generators/micro-react/templates/apps/layout/src/common/portal-data.ts +45 -0
- package/generators/micro-react/templates/apps/layout/src/common/request/config.ts +49 -10
- package/generators/micro-react/templates/apps/layout/src/common/request/interceptors.ts +1 -1
- package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +6 -0
- package/generators/micro-react/templates/apps/layout/src/common/theme.ts +0 -2
- package/generators/micro-react/templates/apps/layout/src/components/AppTabs/index.less +0 -1
- package/generators/micro-react/templates/apps/layout/src/components/AppTabs/index.tsx +4 -4
- package/generators/micro-react/templates/apps/layout/src/components/IconFont/index.tsx +4 -5
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.less +20 -1
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +4 -3
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/micro-app-manager.ts +7 -1
- package/generators/micro-react/templates/apps/layout/src/global.less +15 -3
- package/generators/micro-react/templates/apps/layout/src/hooks/useMenu.ts +3 -2
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.less +30 -3
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +15 -4
- package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +75 -38
- package/generators/micro-react/templates/apps/layout/src/pages/404/index.tsx +3 -7
- package/generators/micro-react/templates/apps/layout/src/services/user.ts +2 -2
- package/generators/micro-react/templates/dev.preset.json +1 -1
- package/generators/micro-react/templates/package.json +2 -1
- package/generators/subapp-react/index.js +240 -14
- package/generators/subapp-react/templates/homepage/.env +2 -1
- package/generators/subapp-react/templates/homepage/config/config.dev.ts +9 -1
- package/generators/subapp-react/templates/homepage/config/config.prod.development.ts +2 -1
- package/generators/subapp-react/templates/homepage/config/config.prod.testing.ts +2 -1
- package/generators/subapp-react/templates/homepage/config/config.prod.ts +2 -1
- package/generators/subapp-react/templates/homepage/config/config.ts +21 -0
- package/generators/subapp-react/templates/homepage/config/routes.ts +1 -1
- package/generators/subapp-react/templates/homepage/mock/api.mock.ts +2 -2
- package/generators/subapp-react/templates/homepage/package.json +3 -2
- package/generators/subapp-react/templates/homepage/src/app.tsx +1 -1
- package/generators/subapp-react/templates/homepage/src/common/request.ts +2 -2
- package/generators/subapp-react/templates/homepage/src/global.less +2 -1
- package/generators/subapp-react/templates/homepage/src/pages/index.less +1 -1
- package/generators/subapp-react/templates/homepage/src/pages/index.tsx +27 -27
- package/lib/utils.js +200 -2
- package/package.json +1 -1
|
@@ -1,15 +1,82 @@
|
|
|
1
|
-
import { getIntl } from '@umijs/max';
|
|
2
|
-
import { isAuthDisabled, isNoPermissionRoute } from '@/constants';
|
|
3
|
-
import type { MenuItem, ParsedMenuItem, ParsedRoute } from './types';
|
|
4
1
|
import { getCurrentLocale, LOCALE } from '@/common/locale';
|
|
2
|
+
import { getMenuPage, getPages } from '@/common/portal-data';
|
|
3
|
+
import { isAuthDisabled, isNoPermissionRoute } from '@/constants';
|
|
4
|
+
import { getIntl } from '@umijs/max';
|
|
5
|
+
import type {
|
|
6
|
+
MenuItem,
|
|
7
|
+
ParsedMenuItem,
|
|
8
|
+
ParsedRoute,
|
|
9
|
+
PublicPageItem,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 从页面数据提取路由配置
|
|
14
|
+
*/
|
|
15
|
+
export const extractRoutesFromPages = (
|
|
16
|
+
pages: PublicPageItem[],
|
|
17
|
+
): ParsedRoute[] => {
|
|
18
|
+
return pages
|
|
19
|
+
.filter((page) => page.enabled)
|
|
20
|
+
.map((page) => {
|
|
21
|
+
const hasEntry = page.htmlUrl || (page.jsUrls && page.jsUrls.length > 0);
|
|
22
|
+
return {
|
|
23
|
+
path: page.route,
|
|
24
|
+
name: page.name,
|
|
25
|
+
icon: '',
|
|
26
|
+
loadType: (hasEntry ? 'microapp' : 'internal') as
|
|
27
|
+
| 'internal'
|
|
28
|
+
| 'microapp',
|
|
29
|
+
entry: page.htmlUrl || page.jsUrls?.[0] || undefined,
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 获取动态路由(从 __MICO_PAGES__)
|
|
36
|
+
*/
|
|
37
|
+
export const getDynamicRoutes = (): ParsedRoute[] => {
|
|
38
|
+
return extractRoutesFromPages(getPages());
|
|
39
|
+
};
|
|
5
40
|
|
|
6
41
|
/**
|
|
7
|
-
*
|
|
42
|
+
* 根据路径查找对应的页面配置
|
|
43
|
+
* 匹配逻辑与 findRouteByPath 一致(精确匹配 + 通配符匹配)
|
|
8
44
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
45
|
+
/**
|
|
46
|
+
* 判断指定路径的页面是否为免认证页面
|
|
47
|
+
* 当 accessControlEnabled === false 时,跳过 SSO 认证和权限校验
|
|
48
|
+
*/
|
|
49
|
+
export const isPageAuthFree = (pathname: string): boolean => {
|
|
50
|
+
const page = findPageByPath(getPages(), pathname);
|
|
51
|
+
return page?.accessControlEnabled === false;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const findPageByPath = (
|
|
55
|
+
pages: PublicPageItem[],
|
|
56
|
+
pathname: string,
|
|
57
|
+
): PublicPageItem | undefined => {
|
|
58
|
+
let exact: PublicPageItem | undefined;
|
|
59
|
+
let bestWildcard: { page: PublicPageItem; basePath: string } | undefined;
|
|
60
|
+
|
|
61
|
+
for (const page of pages) {
|
|
62
|
+
if (!page.enabled) continue;
|
|
63
|
+
|
|
64
|
+
if (page.route === pathname) {
|
|
65
|
+
exact = page;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (page.route.endsWith('/*')) {
|
|
70
|
+
const basePath = page.route.slice(0, -2);
|
|
71
|
+
if (pathname === basePath || pathname.startsWith(`${basePath}/`)) {
|
|
72
|
+
if (!bestWildcard || basePath.length > bestWildcard.basePath.length) {
|
|
73
|
+
bestWildcard = { page, basePath };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return exact || bestWildcard?.page;
|
|
13
80
|
};
|
|
14
81
|
|
|
15
82
|
export interface MenuFilterOptions {
|
|
@@ -19,7 +86,7 @@ export interface MenuFilterOptions {
|
|
|
19
86
|
sideMenus?: string[];
|
|
20
87
|
}
|
|
21
88
|
|
|
22
|
-
const isSuperuserUser = (value?: boolean | number): boolean => {
|
|
89
|
+
export const isSuperuserUser = (value?: boolean | number): boolean => {
|
|
23
90
|
return value === true || value === 1;
|
|
24
91
|
};
|
|
25
92
|
|
|
@@ -55,14 +122,11 @@ export const getMenuIdentifier = (
|
|
|
55
122
|
return item.nameEn || item.nameKey || '';
|
|
56
123
|
};
|
|
57
124
|
|
|
58
|
-
// 权限管理菜单的多格式标识符(用于硬编码权限判断)
|
|
59
|
-
const PERMISSION_MENU_IDENTIFIERS = ['权限管理', 'Permission', 'permission'];
|
|
60
|
-
|
|
61
125
|
/**
|
|
62
126
|
* 检查菜单路径是否允许访问(白名单逻辑)
|
|
63
127
|
* - 免权限校验路由,始终允许访问
|
|
64
128
|
* - 超级用户可以访问所有菜单
|
|
65
|
-
* - 非超级用户不能访问
|
|
129
|
+
* - 非超级用户不能访问 adminOnly 菜单
|
|
66
130
|
* - 菜单路径在 sideMenus 中,或是 sideMenus 中某项的前缀(父级菜单)
|
|
67
131
|
*/
|
|
68
132
|
const isMenuAllowed = (
|
|
@@ -70,23 +134,20 @@ const isMenuAllowed = (
|
|
|
70
134
|
item: MenuItem,
|
|
71
135
|
options: MenuFilterOptions,
|
|
72
136
|
): boolean => {
|
|
73
|
-
|
|
74
137
|
// 关闭权限控制时,所有菜单都允许访问
|
|
75
138
|
if (isAuthDisabled()) return true;
|
|
76
139
|
|
|
77
140
|
// 免权限校验路由,始终允许访问
|
|
78
|
-
|
|
141
|
+
const itemRoute = item.path ?? getMenuPage(item)?.route;
|
|
142
|
+
if (itemRoute && isNoPermissionRoute(itemRoute)) {
|
|
79
143
|
return true;
|
|
80
144
|
}
|
|
81
145
|
|
|
82
146
|
// 超级用户可以访问所有菜单
|
|
83
147
|
if (isSuperuserUser(options.isSuperuser)) return true;
|
|
84
148
|
|
|
85
|
-
// 非超级用户不能访问
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
// 非超级用户不能访问"权限管理"
|
|
89
|
-
if (menuPath === '权限管理') return false;
|
|
149
|
+
// 非超级用户不能访问 adminOnly 菜单
|
|
150
|
+
if (item.adminOnly) return false;
|
|
90
151
|
|
|
91
152
|
const sideMenus = options.sideMenus || [];
|
|
92
153
|
|
|
@@ -103,12 +164,6 @@ const isMenuAllowed = (
|
|
|
103
164
|
});
|
|
104
165
|
};
|
|
105
166
|
|
|
106
|
-
export const isRouteAllowed = (
|
|
107
|
-
menuPath: string,
|
|
108
|
-
options: MenuFilterOptions = {},
|
|
109
|
-
): boolean => {
|
|
110
|
-
return isMenuAllowed(menuPath, options);
|
|
111
|
-
};
|
|
112
167
|
/**
|
|
113
168
|
* 根据权限过滤菜单项(白名单逻辑)
|
|
114
169
|
*/
|
|
@@ -167,15 +222,13 @@ export const filterMenuItems = (
|
|
|
167
222
|
* 判断页面的加载类型
|
|
168
223
|
* 有 htmlUrl 或 jsUrls 时使用 qiankun 微应用加载
|
|
169
224
|
*/
|
|
170
|
-
const getLoadType = (
|
|
225
|
+
const getLoadType = (
|
|
226
|
+
page: PublicPageItem | null | undefined,
|
|
227
|
+
): 'internal' | 'microapp' => {
|
|
171
228
|
if (!page) return 'internal';
|
|
172
|
-
|
|
173
|
-
// 有 htmlUrl 或 jsUrls 使用 qiankun 微应用加载
|
|
174
229
|
if (page.htmlUrl || (page.jsUrls && page.jsUrls.length > 0)) {
|
|
175
230
|
return 'microapp';
|
|
176
231
|
}
|
|
177
|
-
|
|
178
|
-
// 否则使用内部路由
|
|
179
232
|
return 'internal';
|
|
180
233
|
};
|
|
181
234
|
|
|
@@ -183,7 +236,9 @@ const getLoadType = (page: MenuItem['page']): 'internal' | 'microapp' => {
|
|
|
183
236
|
* 获取微应用入口 URL
|
|
184
237
|
* 优先使用 htmlUrl,其次使用 jsUrls[0]
|
|
185
238
|
*/
|
|
186
|
-
const getEntry = (
|
|
239
|
+
const getEntry = (
|
|
240
|
+
page: PublicPageItem | null | undefined,
|
|
241
|
+
): string | undefined => {
|
|
187
242
|
if (!page) return undefined;
|
|
188
243
|
return page.htmlUrl || page.jsUrls?.[0] || undefined;
|
|
189
244
|
};
|
|
@@ -209,17 +264,19 @@ export const extractRoutes = (
|
|
|
209
264
|
|
|
210
265
|
const menuPath = buildMenuPath(parentPath, getMenuIdentifierDefault(item));
|
|
211
266
|
|
|
212
|
-
if (item.type === 'page' && item.
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
267
|
+
if (item.type === 'page' && item.pageId) {
|
|
268
|
+
const page = getMenuPage(item);
|
|
269
|
+
if (page && page.enabled) {
|
|
270
|
+
routes.push({
|
|
271
|
+
path: page.route,
|
|
272
|
+
name: item.name,
|
|
273
|
+
nameEn: item.nameEn,
|
|
274
|
+
icon: item.icon,
|
|
275
|
+
loadType: getLoadType(page),
|
|
276
|
+
entry: getEntry(page),
|
|
277
|
+
pageConfig: page,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
223
280
|
}
|
|
224
281
|
|
|
225
282
|
if (item.children && item.children.length > 0) {
|
|
@@ -231,18 +288,25 @@ export const extractRoutes = (
|
|
|
231
288
|
};
|
|
232
289
|
|
|
233
290
|
/**
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* - 如果 intl 中没有对应的 key,则使用 fallbackName 作为兜底
|
|
237
|
-
* - 如果没有传入 fallbackName,则使用 key 本身作为兜底
|
|
238
|
-
*
|
|
239
|
-
* @param key 国际化 key
|
|
240
|
-
* @param fallbackName 保底名称(可选,默认使用 key)
|
|
241
|
-
* @returns 翻译后的菜单名称
|
|
291
|
+
* 获取菜单项的显示名称
|
|
292
|
+
* 优先级:name/nameEn > nameKey(走 intl 国际化) > 兜底 name
|
|
242
293
|
*/
|
|
243
|
-
export const getMenuLabel = (
|
|
244
|
-
|
|
245
|
-
|
|
294
|
+
export const getMenuLabel = (
|
|
295
|
+
fields: { name?: string; nameEn?: string; nameKey?: string },
|
|
296
|
+
isChinese: boolean,
|
|
297
|
+
): string => {
|
|
298
|
+
const directName = isChinese ? fields.name : fields.nameEn;
|
|
299
|
+
if (directName) return directName;
|
|
300
|
+
|
|
301
|
+
if (fields.nameKey) {
|
|
302
|
+
const intl = getIntl();
|
|
303
|
+
return intl.formatMessage({
|
|
304
|
+
id: fields.nameKey,
|
|
305
|
+
defaultMessage: fields.nameKey,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return fields.name || '';
|
|
246
310
|
};
|
|
247
311
|
|
|
248
312
|
/**
|
|
@@ -256,18 +320,17 @@ export const parseMenuItems = (items: MenuItem[]): ParsedMenuItem[] => {
|
|
|
256
320
|
.filter((item) => item.enabled)
|
|
257
321
|
.sort((a, b) => a.sortOrder - b.sortOrder)
|
|
258
322
|
.map((item) => {
|
|
259
|
-
const name = getMenuIdentifier(item, isChinese);
|
|
260
323
|
const parsed: ParsedMenuItem = {
|
|
261
324
|
key: String(item.id),
|
|
262
|
-
label:
|
|
325
|
+
label: getMenuLabel(item, isChinese),
|
|
263
326
|
icon: item.icon,
|
|
264
327
|
type: item.type,
|
|
265
328
|
};
|
|
266
329
|
|
|
267
330
|
if (item.type === 'link' && item.path) {
|
|
268
331
|
parsed.path = item.path;
|
|
269
|
-
} else if (item.type === 'page'
|
|
270
|
-
parsed.path = item.
|
|
332
|
+
} else if (item.type === 'page') {
|
|
333
|
+
parsed.path = item.path ?? getMenuPage(item)?.route;
|
|
271
334
|
}
|
|
272
335
|
|
|
273
336
|
if (item.children && item.children.length > 0) {
|
|
@@ -28,6 +28,12 @@ export interface PageConfig {
|
|
|
28
28
|
prefixPath: string;
|
|
29
29
|
/** 路由匹配模式 */
|
|
30
30
|
routeMode: PageRouteMode;
|
|
31
|
+
/** 是否仅超级管理员可见 */
|
|
32
|
+
adminOnly?: boolean;
|
|
33
|
+
/** 是否开启权限控制 */
|
|
34
|
+
accessControlEnabled: boolean;
|
|
35
|
+
/** 路由权限标识(用于匹配 sideMenus) */
|
|
36
|
+
routeKey: string | null;
|
|
31
37
|
/** 关联的主文档 ID */
|
|
32
38
|
mainDocumentId: number;
|
|
33
39
|
/** 所属工作空间子域名 */
|
|
@@ -44,6 +50,15 @@ export interface PageConfig {
|
|
|
44
50
|
updatedAt: string;
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
/**
|
|
54
|
+
* 公共页面配置 - 从 window.__MICO_PAGES__ 获取
|
|
55
|
+
* PageConfig 的子集,用于动态路由注册和页面级权限控制
|
|
56
|
+
*/
|
|
57
|
+
export type PublicPageItem = Omit<
|
|
58
|
+
PageConfig,
|
|
59
|
+
'workspaceSubdomain' | 'publishedBy' | 'versions' | 'createdAt' | 'updatedAt'
|
|
60
|
+
>;
|
|
61
|
+
|
|
47
62
|
/**
|
|
48
63
|
* 菜单项类型
|
|
49
64
|
*/
|
|
@@ -65,8 +80,8 @@ export interface MenuItem {
|
|
|
65
80
|
enabled: boolean;
|
|
66
81
|
sortOrder: number;
|
|
67
82
|
pageId: number | null;
|
|
68
|
-
/**
|
|
69
|
-
|
|
83
|
+
/** 是否仅超级管理员可见 */
|
|
84
|
+
adminOnly?: boolean;
|
|
70
85
|
children: MenuItem[];
|
|
71
86
|
}
|
|
72
87
|
|
|
@@ -87,7 +102,7 @@ export interface ParsedRoute {
|
|
|
87
102
|
/** 微应用入口 URL (htmlUrl 或 jsUrls[0]) */
|
|
88
103
|
entry?: string;
|
|
89
104
|
/** 原始页面配置 */
|
|
90
|
-
pageConfig?:
|
|
105
|
+
pageConfig?: PublicPageItem;
|
|
91
106
|
}
|
|
92
107
|
|
|
93
108
|
/**
|
|
@@ -107,7 +122,7 @@ export interface ParsedMenuItem {
|
|
|
107
122
|
*/
|
|
108
123
|
export interface MicroAppProps {
|
|
109
124
|
/** 主应用标识 */
|
|
110
|
-
mainApp: '
|
|
125
|
+
mainApp: 'common-web';
|
|
111
126
|
/** 共享的 request 实例,子应用可直接使用 */
|
|
112
127
|
request: <T = any>(url: string, options?: any) => Promise<T>;
|
|
113
128
|
}
|
|
@@ -128,6 +143,8 @@ export type WorkspaceStatus = 'active' | 'inactive' | 'pending';
|
|
|
128
143
|
export interface WorkspaceConfig {
|
|
129
144
|
/** 关联的应用 ID(可能为空) */
|
|
130
145
|
appId: number | null;
|
|
146
|
+
/** CAS 服务端登录 URL,用于 SSO 外部登录跳转 */
|
|
147
|
+
casServerLoginUrl?: string;
|
|
131
148
|
/** 创建时间 */
|
|
132
149
|
createdAt: string;
|
|
133
150
|
/** 创建者 ID */
|
|
@@ -161,10 +178,24 @@ export interface WorkspaceConfig {
|
|
|
161
178
|
*/
|
|
162
179
|
declare global {
|
|
163
180
|
interface Window {
|
|
181
|
+
__MICO_PAGES__?: PublicPageItem[];
|
|
164
182
|
__MICO_MENUS__?: MenuItem[];
|
|
165
183
|
__MICO_CONFIG__?: {
|
|
166
184
|
appName?: string;
|
|
185
|
+
/** 站点/产品标题,用于登录页标题文案、logo alt 等 */
|
|
186
|
+
title?: string;
|
|
187
|
+
/** 登录页 logo 图片地址,不配置时使用应用内置 logo */
|
|
188
|
+
logo?: string;
|
|
189
|
+
appId?: string;
|
|
167
190
|
apiBaseUrl?: string;
|
|
191
|
+
proxySuffix?: string;
|
|
192
|
+
loginEndpoint?: string;
|
|
193
|
+
refreshEndpoint?: string;
|
|
194
|
+
externalLoginPath?: string;
|
|
195
|
+
/** 请求客户端配置覆盖,与 config 中 defaultClientOptions 同名字段 */
|
|
196
|
+
defaultClientOptions?: Partial<
|
|
197
|
+
import('../request/types').RequestClientOptions
|
|
198
|
+
>;
|
|
168
199
|
/** 默认重定向路径,访问 "/" 时自动跳转到此路径 */
|
|
169
200
|
defaultPath?: string;
|
|
170
201
|
/**
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { prefetchApps } from 'qiankun';
|
|
9
|
-
import { extractRoutes
|
|
9
|
+
import { extractRoutes } from './menu';
|
|
10
|
+
import { getMenus } from './portal-data';
|
|
10
11
|
import { getAppNameFromEntry } from './micro';
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -45,7 +46,7 @@ export const getMicroAppsForPrefetch = (
|
|
|
45
46
|
excludeEntry?: string,
|
|
46
47
|
): Array<{ name: string; entry: string }> => {
|
|
47
48
|
try {
|
|
48
|
-
const menus =
|
|
49
|
+
const menus = getMenus();
|
|
49
50
|
const routes = extractRoutes(menus);
|
|
50
51
|
|
|
51
52
|
return routes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 全局数据源管理
|
|
3
|
+
*
|
|
4
|
+
* 统一管理 window.__MICO_PAGES__ 和 window.__MICO_MENUS__ 的访问与索引。
|
|
5
|
+
* 数据由中台注入,每次直接从 window 读取(避免缓存导致时序问题)。
|
|
6
|
+
* pageId 索引惰性构建,数据就绪后缓存。
|
|
7
|
+
*/
|
|
8
|
+
import type { MenuItem, PublicPageItem } from './menu/types';
|
|
9
|
+
|
|
10
|
+
/** 获取页面列表 (window.__MICO_PAGES__) */
|
|
11
|
+
export const getPages = (): PublicPageItem[] => {
|
|
12
|
+
if (typeof window === 'undefined') return [];
|
|
13
|
+
return Array.isArray(window.__MICO_PAGES__) ? window.__MICO_PAGES__ : [];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** 获取菜单树 (window.__MICO_MENUS__) */
|
|
17
|
+
export const getMenus = (): MenuItem[] => {
|
|
18
|
+
if (typeof window === 'undefined') return [];
|
|
19
|
+
return Array.isArray(window.__MICO_MENUS__) ? window.__MICO_MENUS__ : [];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** 是否有页面数据可用 */
|
|
23
|
+
export const hasPages = (): boolean => {
|
|
24
|
+
return getPages().length > 0;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
let _pageIdIndex: Map<number, PublicPageItem> | null = null;
|
|
28
|
+
|
|
29
|
+
const getPageIdIndex = (): Map<number, PublicPageItem> => {
|
|
30
|
+
const pages = getPages();
|
|
31
|
+
if (!_pageIdIndex || (_pageIdIndex.size === 0 && pages.length > 0)) {
|
|
32
|
+
_pageIdIndex = new Map(pages.map((p) => [p.id, p]));
|
|
33
|
+
}
|
|
34
|
+
return _pageIdIndex;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** 通过 pageId 查找页面(O(1)) */
|
|
38
|
+
export const getPageById = (pageId: number): PublicPageItem | undefined => {
|
|
39
|
+
return getPageIdIndex().get(pageId);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** 获取菜单项关联的页面 */
|
|
43
|
+
export const getMenuPage = (item: MenuItem): PublicPageItem | undefined => {
|
|
44
|
+
return item.pageId ? getPageById(item.pageId) : undefined;
|
|
45
|
+
};
|
|
@@ -1,20 +1,59 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 请求客户端配置管理
|
|
3
|
+
* appId 优先从 window.__MICO_WORKSPACE__ 获取;
|
|
4
|
+
* externalLoginPath 优先从 window.__MICO_WORKSPACE__.casServerLoginUrl 获取;
|
|
5
|
+
* 其余 defaultClientOptions / apiBaseUrl / proxySuffix / loginEndpoint / refreshEndpoint 优先从 window.__MICO_CONFIG__ 同名字段获取。
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
8
|
import { getStoredAuthToken } from '../auth/auth-manager';
|
|
6
9
|
import type { RequestClientOptions, TokenResolver } from './types';
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
function getMicoConfig(): Window['__MICO_CONFIG__'] {
|
|
12
|
+
if (typeof window === 'undefined') return undefined;
|
|
13
|
+
return window.__MICO_CONFIG__;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getMicoWorkspace(): Window['__MICO_WORKSPACE__'] {
|
|
17
|
+
if (typeof window === 'undefined') return undefined;
|
|
18
|
+
return window.__MICO_WORKSPACE__ ?? undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildDefaultClientOptions(): RequestClientOptions {
|
|
22
|
+
const envDefaults: RequestClientOptions = {
|
|
23
|
+
appId: process.env.APP_ID ?? '',
|
|
24
|
+
ticketParam: 'ticket',
|
|
25
|
+
logoutPath: '/logout',
|
|
26
|
+
apiBaseUrl: process.env.API_BASE_URL ?? '',
|
|
27
|
+
proxySuffix: process.env.PROXY_SUFFIX ?? '',
|
|
28
|
+
loginEndpoint: process.env.LOGIN_ENDPOINT ?? '',
|
|
29
|
+
refreshEndpoint: process.env.REFRESH_ENDPOINT ?? '',
|
|
30
|
+
externalLoginPath: process.env.EXTERNAL_LOGIN_PATH ?? '',
|
|
31
|
+
};
|
|
32
|
+
const mico = getMicoConfig();
|
|
33
|
+
const workspace = getMicoWorkspace();
|
|
34
|
+
const appIdFromWorkspace =
|
|
35
|
+
workspace?.appId !== undefined && workspace?.appId !== null
|
|
36
|
+
? String(workspace.appId)
|
|
37
|
+
: undefined;
|
|
38
|
+
const externalLoginFromWorkspace = workspace?.casServerLoginUrl;
|
|
39
|
+
if (!mico && !appIdFromWorkspace && !externalLoginFromWorkspace)
|
|
40
|
+
return envDefaults;
|
|
41
|
+
return {
|
|
42
|
+
...envDefaults,
|
|
43
|
+
...(mico?.defaultClientOptions ?? {}),
|
|
44
|
+
appId: appIdFromWorkspace ?? mico?.appId ?? envDefaults.appId,
|
|
45
|
+
apiBaseUrl: mico?.apiBaseUrl ?? envDefaults.apiBaseUrl,
|
|
46
|
+
proxySuffix: mico?.proxySuffix ?? envDefaults.proxySuffix,
|
|
47
|
+
loginEndpoint: mico?.loginEndpoint ?? envDefaults.loginEndpoint,
|
|
48
|
+
refreshEndpoint: mico?.refreshEndpoint ?? envDefaults.refreshEndpoint,
|
|
49
|
+
externalLoginPath:
|
|
50
|
+
externalLoginFromWorkspace ??
|
|
51
|
+
mico?.externalLoginPath ??
|
|
52
|
+
envDefaults.externalLoginPath,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const defaultClientOptions: RequestClientOptions = buildDefaultClientOptions();
|
|
18
57
|
|
|
19
58
|
let clientOptions: RequestClientOptions = { ...defaultClientOptions };
|
|
20
59
|
|
|
@@ -68,7 +68,7 @@ export const runResponseInterceptors = async <T>(
|
|
|
68
68
|
): Promise<T> => {
|
|
69
69
|
let current = response;
|
|
70
70
|
for (const { handler } of responseInterceptors) {
|
|
71
|
-
current = await handler(current, ctx);
|
|
71
|
+
current = await handler(current, ctx) as T;
|
|
72
72
|
}
|
|
73
73
|
return current;
|
|
74
74
|
};
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
maybePersistTokens,
|
|
8
8
|
setStoredAuthToken,
|
|
9
9
|
} from '@/common/auth/auth-manager';
|
|
10
|
+
import { isPageAuthFree } from '@/common/menu';
|
|
10
11
|
import { isNoAuthRoute, ROUTES } from '@/constants';
|
|
11
12
|
import {
|
|
12
13
|
getTicketParam,
|
|
@@ -37,6 +38,11 @@ export const handleAuthFailureRedirect = (): void => {
|
|
|
37
38
|
return;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
if (isPageAuthFree(window.location.pathname)) {
|
|
42
|
+
console.log('[SSO] 页面 accessControlEnabled=false,跳过 SSO 重定向');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
40
46
|
// 从 URL 中获取当前 redirect 登录的次数
|
|
41
47
|
const currentUrl = new URL(window.location.href);
|
|
42
48
|
const redirectCountParam = currentUrl.searchParams.get('redirect_count');
|
|
@@ -50,10 +50,8 @@ export function applyTheme(theme: ThemeMode): void {
|
|
|
50
50
|
if (typeof document === 'undefined') return;
|
|
51
51
|
|
|
52
52
|
if (theme === 'dark') {
|
|
53
|
-
document.body.setAttribute('arco-theme', 'dark');
|
|
54
53
|
document.body.setAttribute('data-theme', 'dark');
|
|
55
54
|
} else {
|
|
56
|
-
document.body.removeAttribute('arco-theme');
|
|
57
55
|
document.body.setAttribute('data-theme', 'normal');
|
|
58
56
|
}
|
|
59
57
|
}
|
|
@@ -188,10 +188,10 @@ function AppTabs() {
|
|
|
188
188
|
>
|
|
189
189
|
{tabs.map((tab) => {
|
|
190
190
|
const isChinese = isChineseLocale();
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
191
|
+
const label = getMenuLabel(
|
|
192
|
+
{ name: tab.title, nameEn: tab.titleEn, nameKey: tab.nameKey },
|
|
193
|
+
isChinese,
|
|
194
|
+
);
|
|
195
195
|
return <TabPane destroyOnHide key={tab.key} title={label} />;
|
|
196
196
|
})}
|
|
197
197
|
</Tabs>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getIconFontPath } from '@/common/locale';
|
|
2
2
|
import { Icon } from '@mico-platform/ui';
|
|
3
3
|
|
|
4
4
|
// 该组件用于自定义的图标,用法见https://arco.design/react/components/icon#%E6%B7%BB%E5%8A%A0-iconbox-%E9%A1%B9%E7%9B%AE
|
|
@@ -7,13 +7,12 @@ import { Icon } from '@mico-platform/ui';
|
|
|
7
7
|
// 字体库地址:https://arco.design/iconbox/mime/lib/1945/0
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* 根据当前语言加载对应的图标库
|
|
10
|
+
* 根据当前语言加载对应的图标库(当前只有中文)
|
|
11
11
|
* 语言切换时会重新加载页面,所以这里只需要在初始化时根据当前语言加载
|
|
12
12
|
*/
|
|
13
13
|
function createIconFont() {
|
|
14
|
-
//
|
|
15
|
-
const
|
|
16
|
-
const iconPath = getIconFontPath(currentLocale);
|
|
14
|
+
// TODO: 后续支持多语言时,需要根据当前语言加载对应的图标库
|
|
15
|
+
const iconPath = getIconFontPath();
|
|
17
16
|
|
|
18
17
|
// 创建 IconFont 实例
|
|
19
18
|
return Icon.addFromIconFontCn({ src: iconPath });
|
package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.less
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
.micro-app-container {
|
|
6
6
|
width: 100%;
|
|
7
|
-
height: 100%;
|
|
8
7
|
position: relative;
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
9
10
|
overflow: auto;
|
|
10
11
|
}
|
|
11
12
|
|
|
@@ -48,10 +49,28 @@
|
|
|
48
49
|
// 激活时在文档流中,继承占位元素的尺寸
|
|
49
50
|
width: 100%;
|
|
50
51
|
height: 100%;
|
|
52
|
+
display: flex;
|
|
53
|
+
flex: 1;
|
|
54
|
+
flex-direction: column;
|
|
51
55
|
overflow: auto;
|
|
56
|
+
min-height: calc(100vh - 134px);
|
|
57
|
+
|
|
52
58
|
|
|
53
59
|
> div {
|
|
60
|
+
flex: 1;
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
> div > div {
|
|
54
66
|
height: 100%;
|
|
67
|
+
display: flex;
|
|
68
|
+
flex: 1 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
> div > div > div {
|
|
72
|
+
width: 100%;
|
|
73
|
+
flex: 1 1;
|
|
55
74
|
}
|
|
56
75
|
|
|
57
76
|
// 隐藏状态(在 body 中)
|
package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx
CHANGED
|
@@ -2,7 +2,8 @@ import { getAuthInfo } from '@/common/auth/auth-manager';
|
|
|
2
2
|
import { EEnv, getEnv } from '@/common/env';
|
|
3
3
|
import { getCurrentLocale } from '@/common/locale';
|
|
4
4
|
import { request } from '@/common/request';
|
|
5
|
-
import {
|
|
5
|
+
import { isPageAuthFree } from '@/common/menu/parser';
|
|
6
|
+
import { isAuthDisabled, isNoAuthRoute, isNoPermissionRoute } from '@/constants';
|
|
6
7
|
import { Spin } from '@mico-platform/ui';
|
|
7
8
|
import { useLocation, useModel } from '@umijs/max';
|
|
8
9
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
@@ -46,12 +47,12 @@ const MicroAppLoader: React.FC<MicroAppLoaderProps> = ({
|
|
|
46
47
|
mounted: false,
|
|
47
48
|
});
|
|
48
49
|
|
|
49
|
-
// 获取 initialState,等待用户信息准备好再加载子应用
|
|
50
|
-
// disableAuth 模式或免权限校验路由下跳过等待
|
|
51
50
|
const { initialState } = useModel('@@initialState');
|
|
52
51
|
const location = useLocation();
|
|
53
52
|
const isAuthReady =
|
|
54
53
|
isAuthDisabled() ||
|
|
54
|
+
isPageAuthFree(location.pathname) ||
|
|
55
|
+
isNoAuthRoute(location.pathname) ||
|
|
55
56
|
isNoPermissionRoute(location.pathname) ||
|
|
56
57
|
!!initialState?.currentUser;
|
|
57
58
|
|
|
@@ -391,6 +391,12 @@ class MicroAppManager {
|
|
|
391
391
|
activateContainer(container, request.target);
|
|
392
392
|
|
|
393
393
|
console.log('🔍[路由调试] 调用 loadMicroApp', { name: request.name, qiankunName, entry: request.entry });
|
|
394
|
+
|
|
395
|
+
if (process.env.NODE_ENV === 'development') {
|
|
396
|
+
// 重置标志,使子应用能初始化自己的 react-refresh 运行时以支持 HMR
|
|
397
|
+
(window as any).__reactRefreshInjected = false;
|
|
398
|
+
}
|
|
399
|
+
|
|
394
400
|
const microApp = loadMicroApp(
|
|
395
401
|
{
|
|
396
402
|
name: qiankunName,
|
|
@@ -399,7 +405,7 @@ class MicroAppManager {
|
|
|
399
405
|
props: request.props,
|
|
400
406
|
},
|
|
401
407
|
{
|
|
402
|
-
sandbox: { strictStyleIsolation: false, experimentalStyleIsolation: true },
|
|
408
|
+
sandbox: { strictStyleIsolation: false, experimentalStyleIsolation: false, loose: true },
|
|
403
409
|
},
|
|
404
410
|
);
|
|
405
411
|
|