generator-mico-cli 0.2.1 → 0.2.2-8.beta.1

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 (159) hide show
  1. package/README.md +199 -15
  2. package/bin/mico.js +232 -27
  3. package/generators/micro-react/index.js +200 -18
  4. package/generators/micro-react/meta.json +13 -0
  5. package/generators/micro-react/templates/.commitlintrc.js +1 -0
  6. package/generators/micro-react/templates/.cursor/rules/always-read-docs.mdc +14 -4
  7. package/generators/micro-react/templates/.cursor/rules/cicd-deploy.mdc +10 -8
  8. package/generators/micro-react/templates/.cursor/rules/coding-conventions.mdc +1 -1
  9. package/generators/micro-react/templates/.cursor/rules/development-guide.mdc +3 -4
  10. package/generators/micro-react/templates/.cursor/rules/layout-app.mdc +38 -31
  11. package/generators/micro-react/templates/.cursor/rules/project-overview.mdc +7 -4
  12. package/generators/micro-react/templates/.cursor/rules/theme-system.mdc +10 -12
  13. package/generators/micro-react/templates/.eslintrc.js +25 -1
  14. package/generators/micro-react/templates/AGENTS.md +5 -2
  15. package/generators/micro-react/templates/CICD/before_build.sh +76 -0
  16. package/generators/micro-react/templates/CICD/start_dev.sh +27 -3
  17. package/generators/micro-react/templates/CICD/start_prod.sh +26 -3
  18. package/generators/micro-react/templates/CICD/start_test.sh +28 -3
  19. package/generators/micro-react/templates/CICD/wangsu_fresh_dev.sh +4 -4
  20. package/generators/micro-react/templates/CICD/wangsu_fresh_prod.sh +4 -4
  21. package/generators/micro-react/templates/CICD/wangsu_fresh_test.sh +4 -4
  22. package/generators/micro-react/templates/CLAUDE.md +16 -9
  23. package/generators/micro-react/templates/README.md +42 -4
  24. package/generators/micro-react/templates/_gitignore +4 -0
  25. package/generators/micro-react/templates/_npmrc +4 -0
  26. package/generators/micro-react/templates/apps/layout/config/config.dev.ts +32 -16
  27. package/generators/micro-react/templates/apps/layout/config/config.prod.development.ts +24 -29
  28. package/generators/micro-react/templates/apps/layout/config/config.prod.testing.ts +25 -6
  29. package/generators/micro-react/templates/apps/layout/config/config.prod.ts +16 -7
  30. package/generators/micro-react/templates/apps/layout/config/config.ts +27 -4
  31. package/generators/micro-react/templates/apps/layout/config/routes.ts +5 -5
  32. package/generators/micro-react/templates/apps/layout/docs/arch-/346/227/245/345/277/227/344/270/216/345/270/270/351/207/217.md +2 -2
  33. package/generators/micro-react/templates/apps/layout/docs/common-intl.md +372 -0
  34. package/generators/micro-react/templates/apps/layout/docs/feat-/346/236/204/345/273/272define/344/270/216/345/205/215/350/256/244/350/257/201/345/210/235/345/247/213/346/200/201.md +44 -0
  35. package/generators/micro-react/templates/apps/layout/docs/feature-404/351/241/265/351/235/242.md +103 -0
  36. package/generators/micro-react/templates/apps/layout/docs/feature-/344/270/273/351/242/230/350/211/262/345/210/207/346/215/242.md +22 -26
  37. 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 +185 -28
  38. 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 +308 -63
  39. 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
  40. package/generators/micro-react/templates/apps/layout/docs/fix-SSO/346/227/240/351/231/220/351/207/215/345/256/232/345/220/221.md +88 -0
  41. package/generators/micro-react/templates/apps/layout/docs/utils-timezone.md +324 -0
  42. package/generators/micro-react/templates/apps/layout/mock/api.mock.ts +81 -61
  43. package/generators/micro-react/templates/apps/layout/mock/menus.ts +114 -4
  44. package/generators/micro-react/templates/apps/layout/mock/pages.ts +86 -0
  45. package/generators/micro-react/templates/apps/layout/package.json +7 -4
  46. package/generators/micro-react/templates/apps/layout/src/app.tsx +111 -76
  47. package/generators/micro-react/templates/apps/layout/src/common/auth/index.ts +3 -0
  48. package/generators/micro-react/templates/apps/layout/src/common/helpers.ts +177 -0
  49. package/generators/micro-react/templates/apps/layout/src/common/locale.ts +22 -17
  50. package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +192 -42
  51. package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +69 -5
  52. package/generators/micro-react/templates/apps/layout/src/common/micro/index.ts +34 -0
  53. package/generators/micro-react/templates/apps/layout/src/common/micro-prefetch.ts +109 -0
  54. package/generators/micro-react/templates/apps/layout/src/common/portal-data.ts +45 -0
  55. package/generators/micro-react/templates/apps/layout/src/common/request/config.ts +72 -10
  56. package/generators/micro-react/templates/apps/layout/src/common/request/index.ts +2 -2
  57. package/generators/micro-react/templates/apps/layout/src/common/request/interceptors.ts +31 -3
  58. package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +29 -11
  59. package/generators/micro-react/templates/apps/layout/src/common/request/url-resolver.ts +1 -1
  60. package/generators/micro-react/templates/apps/layout/src/common/route-guard.ts +345 -0
  61. package/generators/micro-react/templates/apps/layout/src/common/theme.ts +2 -4
  62. package/generators/micro-react/templates/apps/layout/src/common/upload/oss.ts +3 -4
  63. package/generators/micro-react/templates/apps/layout/src/common/upload/types.ts +1 -1
  64. package/generators/micro-react/templates/apps/layout/src/common/uploadFiles.ts +1 -1
  65. package/generators/micro-react/templates/apps/layout/src/components/AppTabs/index.less +8 -3
  66. package/generators/micro-react/templates/apps/layout/src/components/AppTabs/index.tsx +25 -8
  67. package/generators/micro-react/templates/apps/layout/src/components/HeaderDropdown/index.tsx +20 -0
  68. package/generators/micro-react/templates/apps/layout/src/components/IconFont/index.tsx +5 -6
  69. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.less +21 -6
  70. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +83 -149
  71. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/micro-app-manager.ts +569 -0
  72. package/generators/micro-react/templates/apps/layout/src/components/RightContent/AvatarDropdown.tsx +383 -0
  73. package/generators/micro-react/templates/apps/layout/src/components/RightContent/avatar-dropdown.less +35 -0
  74. package/generators/micro-react/templates/apps/layout/src/components/RightContent/index.ts +2 -0
  75. package/generators/micro-react/templates/apps/layout/src/constants/index.ts +170 -6
  76. package/generators/micro-react/templates/apps/layout/src/global.less +18 -9
  77. package/generators/micro-react/templates/apps/layout/src/hooks/useMenu.ts +3 -2
  78. package/generators/micro-react/templates/apps/layout/src/hooks/useRoutePermissionRefresh.ts +72 -0
  79. package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.less +3 -1
  80. package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.tsx +10 -55
  81. package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.less +34 -4
  82. package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +24 -8
  83. package/generators/micro-react/templates/apps/layout/src/layouts/index.less +84 -13
  84. package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +156 -69
  85. package/generators/micro-react/templates/apps/layout/src/locales/en-US.ts +12 -0
  86. package/generators/micro-react/templates/apps/layout/src/locales/zh-CN.ts +12 -0
  87. package/generators/micro-react/templates/apps/layout/src/pages/403/index.tsx +8 -2
  88. package/generators/micro-react/templates/apps/layout/src/pages/404/index.tsx +78 -0
  89. package/generators/micro-react/templates/apps/layout/src/pages/Home/index.less +3 -0
  90. package/generators/micro-react/templates/apps/layout/src/pages/Home/index.tsx +7 -1
  91. package/generators/micro-react/templates/apps/layout/src/pages/User/Login/index.less +1 -1
  92. package/generators/micro-react/templates/apps/layout/src/pages/User/Login/index.tsx +3 -3
  93. package/generators/micro-react/templates/apps/layout/src/requestErrorConfig.ts +1 -1
  94. package/generators/micro-react/templates/apps/layout/src/services/config/index.ts +63 -0
  95. package/generators/micro-react/templates/apps/layout/src/services/config/type.ts +30 -0
  96. package/generators/micro-react/templates/apps/layout/src/services/user.ts +29 -2
  97. package/generators/micro-react/templates/apps/layout/tailwind.config.js +3 -0
  98. package/generators/micro-react/templates/deployDesc.md +3 -3
  99. package/generators/micro-react/templates/dev.preset.json +14 -0
  100. package/generators/micro-react/templates/docs/dev-preset.md +130 -0
  101. package/generators/micro-react/templates/package.json +21 -6
  102. package/generators/micro-react/templates/packages/common-intl/README.md +427 -0
  103. package/generators/micro-react/templates/packages/common-intl/package.json +34 -0
  104. package/generators/micro-react/templates/packages/common-intl/src/index.ts +7 -0
  105. package/generators/micro-react/templates/packages/common-intl/src/indexedDBUtils.ts +51 -0
  106. package/generators/micro-react/templates/packages/common-intl/src/intl.ts +50 -0
  107. package/generators/micro-react/templates/packages/common-intl/src/utils.ts +482 -0
  108. package/generators/micro-react/templates/packages/common-intl/tsconfig.json +22 -0
  109. package/generators/micro-react/templates/packages/common-intl/vite.config.ts +25 -0
  110. package/generators/micro-react/templates/scripts/apply-sentry-plugin.ts +45 -0
  111. package/generators/micro-react/templates/scripts/collect-dist.js +10 -0
  112. package/generators/micro-react/templates/scripts/dev-preset.js +265 -0
  113. package/generators/micro-react/templates/scripts/dev-preset.schema.json +39 -0
  114. package/generators/micro-react/templates/turbo.json +4 -1
  115. package/generators/subapp-react/index.js +326 -40
  116. package/generators/subapp-react/meta.json +10 -0
  117. package/generators/subapp-react/templates/homepage/.env +2 -1
  118. package/generators/subapp-react/templates/homepage/README.md +3 -3
  119. package/generators/subapp-react/templates/homepage/config/config.dev.ts +14 -7
  120. package/generators/subapp-react/templates/homepage/config/config.prod.development.ts +16 -5
  121. package/generators/subapp-react/templates/homepage/config/config.prod.testing.ts +16 -5
  122. package/generators/subapp-react/templates/homepage/config/config.prod.ts +14 -5
  123. package/generators/subapp-react/templates/homepage/config/config.ts +21 -0
  124. package/generators/subapp-react/templates/homepage/config/routes.ts +2 -2
  125. package/generators/subapp-react/templates/homepage/mock/api.mock.ts +2 -2
  126. package/generators/subapp-react/templates/homepage/package.json +7 -4
  127. package/generators/subapp-react/templates/homepage/src/app.tsx +18 -27
  128. package/generators/subapp-react/templates/homepage/src/common/request.ts +29 -2
  129. package/generators/subapp-react/templates/homepage/src/global.less +6 -5
  130. package/generators/subapp-react/templates/homepage/src/pages/index.less +3 -3
  131. package/generators/subapp-react/templates/homepage/src/pages/index.tsx +99 -60
  132. package/generators/subapp-react/templates/homepage/src/styles/theme.less +1 -1
  133. package/generators/subapp-umd/ignore-list.json +5 -0
  134. package/generators/subapp-umd/index.js +309 -0
  135. package/generators/subapp-umd/meta.json +11 -0
  136. package/generators/subapp-umd/templates/README.md +94 -0
  137. package/generators/subapp-umd/templates/package.json +35 -0
  138. package/generators/subapp-umd/templates/public/index.html +34 -0
  139. package/generators/subapp-umd/templates/src/App.less +15 -0
  140. package/generators/subapp-umd/templates/src/App.tsx +13 -0
  141. package/generators/subapp-umd/templates/src/index.ts +2 -0
  142. package/generators/subapp-umd/templates/tsconfig.json +27 -0
  143. package/generators/subapp-umd/templates/webpack.config.js +70 -0
  144. package/lib/utils.js +332 -2
  145. package/package.json +15 -2
  146. package/generators/micro-react/templates/apps/layout/mock/menus.json +0 -100
  147. package/generators/micro-react/templates/apps/layout/src/common/constants.ts +0 -38
  148. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/container-manager.ts +0 -202
  149. package/generators/micro-react/templates/packages/shared-styles/README.md +0 -124
  150. package/generators/micro-react/templates/packages/shared-styles/arco-design-mobile-override.less +0 -91
  151. package/generators/micro-react/templates/packages/shared-styles/arco-override.less +0 -119
  152. package/generators/micro-react/templates/packages/shared-styles/index.d.ts +0 -44
  153. package/generators/micro-react/templates/packages/shared-styles/index.less +0 -13
  154. package/generators/micro-react/templates/packages/shared-styles/package.json +0 -30
  155. package/generators/micro-react/templates/packages/shared-styles/theme-inject.less +0 -10
  156. package/generators/micro-react/templates/packages/shared-styles/themes/dark/custom-var.less +0 -290
  157. package/generators/micro-react/templates/packages/shared-styles/themes/normal/custom-var.less +0 -269
  158. package/generators/micro-react/templates/packages/shared-styles/variables-only.less +0 -433
  159. package/generators/micro-react/templates/packages/shared-styles/variables.less +0 -452
@@ -1,12 +1,83 @@
1
- import type { MenuItem, ParsedMenuItem, ParsedRoute } from './types';
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';
2
11
 
3
12
  /**
4
- * 获取 window 上挂载的菜单数据
13
+ * 从页面数据提取路由配置
5
14
  */
6
- export const getWindowMenus = (): MenuItem[] => {
7
- if (typeof window === 'undefined') return [];
8
- const menus = window.__MICO_MENUS__;
9
- return Array.isArray(menus) ? menus : [];
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
+ base: page.base || '/',
26
+ icon: '',
27
+ loadType: (hasEntry ? 'microapp' : 'internal') as
28
+ | 'internal'
29
+ | 'microapp',
30
+ entry: page.htmlUrl || page.jsUrls?.[0] || undefined,
31
+ };
32
+ });
33
+ };
34
+
35
+ /**
36
+ * 获取动态路由(从 __MICO_PAGES__)
37
+ */
38
+ export const getDynamicRoutes = (): ParsedRoute[] => {
39
+ return extractRoutesFromPages(getPages());
40
+ };
41
+
42
+ /**
43
+ * 根据路径查找对应的页面配置
44
+ * 匹配逻辑与 findRouteByPath 一致(精确匹配 + 通配符匹配)
45
+ */
46
+ /**
47
+ * 判断指定路径的页面是否为免认证页面
48
+ * 当 accessControlEnabled === false 时,跳过 SSO 认证和权限校验
49
+ */
50
+ export const isPageAuthFree = (pathname: string): boolean => {
51
+ const page = findPageByPath(getPages(), pathname);
52
+ return page?.accessControlEnabled === false;
53
+ };
54
+
55
+ export const findPageByPath = (
56
+ pages: PublicPageItem[],
57
+ pathname: string,
58
+ ): PublicPageItem | undefined => {
59
+ let exact: PublicPageItem | undefined;
60
+ let bestWildcard: { page: PublicPageItem; basePath: string } | undefined;
61
+
62
+ for (const page of pages) {
63
+ if (!page.enabled) continue;
64
+
65
+ if (page.route === pathname) {
66
+ exact = page;
67
+ continue;
68
+ }
69
+
70
+ if (page.route.endsWith('/*')) {
71
+ const basePath = page.route.slice(0, -2);
72
+ if (pathname === basePath || pathname.startsWith(`${basePath}/`)) {
73
+ if (!bestWildcard || basePath.length > bestWildcard.basePath.length) {
74
+ bestWildcard = { page, basePath };
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ return exact || bestWildcard?.page;
10
81
  };
11
82
 
12
83
  export interface MenuFilterOptions {
@@ -16,7 +87,7 @@ export interface MenuFilterOptions {
16
87
  sideMenus?: string[];
17
88
  }
18
89
 
19
- const isSuperuserUser = (value?: boolean | number): boolean => {
90
+ export const isSuperuserUser = (value?: boolean | number): boolean => {
20
91
  return value === true || value === 1;
21
92
  };
22
93
 
@@ -24,21 +95,60 @@ export const buildMenuPath = (parentPath: string, name: string): string => {
24
95
  return parentPath ? `${parentPath}.${name}` : name;
25
96
  };
26
97
 
98
+ // ===== 多语言菜单标识符工具函数 =====
99
+
100
+ /**
101
+ * 判断当前是否为中文语言环境
102
+ * 基于 localStorage 的 umi_locale 配置
103
+ */
104
+ export const isChineseLocale = (): boolean => {
105
+ const locale = getCurrentLocale();
106
+ return locale === LOCALE.ZH_CN;
107
+ };
108
+
109
+ /**
110
+ * 获取菜单标识符(用于构建权限路径)
111
+ * - 中文格式:优先使用 name,兜底使用 nameKey
112
+ * - 英文格式:优先使用 nameEn,兜底使用 nameKey
113
+ * @param item 菜单项
114
+ * @param isChinese 是否中文格式
115
+ */
116
+ export const getMenuIdentifier = (
117
+ item: MenuItem,
118
+ isChinese: boolean,
119
+ ): string => {
120
+ if (isChinese) {
121
+ return item.name || item.nameKey || '';
122
+ }
123
+ return item.nameEn || item.nameKey || '';
124
+ };
125
+
27
126
  /**
28
127
  * 检查菜单路径是否允许访问(白名单逻辑)
128
+ * - 免权限校验路由,始终允许访问
29
129
  * - 超级用户可以访问所有菜单
30
- * - 非超级用户不能访问"权限管理"
130
+ * - 非超级用户不能访问 adminOnly 菜单
31
131
  * - 菜单路径在 sideMenus 中,或是 sideMenus 中某项的前缀(父级菜单)
32
132
  */
33
133
  const isMenuAllowed = (
34
134
  menuPath: string,
135
+ item: MenuItem,
35
136
  options: MenuFilterOptions,
36
137
  ): boolean => {
138
+ // 关闭权限控制时,所有菜单都允许访问
139
+ if (isAuthDisabled()) return true;
140
+
141
+ // 免权限校验路由,始终允许访问
142
+ const itemRoute = item.path ?? getMenuPage(item)?.route;
143
+ if (itemRoute && isNoPermissionRoute(itemRoute)) {
144
+ return true;
145
+ }
146
+
37
147
  // 超级用户可以访问所有菜单
38
148
  if (isSuperuserUser(options.isSuperuser)) return true;
39
149
 
40
- // 非超级用户不能访问"权限管理"
41
- if (menuPath === '权限管理') return false;
150
+ // 非超级用户不能访问 adminOnly 菜单
151
+ if (item.adminOnly) return false;
42
152
 
43
153
  const sideMenus = options.sideMenus || [];
44
154
 
@@ -55,13 +165,6 @@ const isMenuAllowed = (
55
165
  });
56
166
  };
57
167
 
58
- export const isRouteAllowed = (
59
- menuPath: string,
60
- options: MenuFilterOptions = {},
61
- ): boolean => {
62
- return isMenuAllowed(menuPath, options);
63
- };
64
-
65
168
  /**
66
169
  * 根据权限过滤菜单项(白名单逻辑)
67
170
  */
@@ -70,11 +173,16 @@ export const filterMenuItems = (
70
173
  options: MenuFilterOptions = {},
71
174
  parentPath = '',
72
175
  ): MenuItem[] => {
176
+ // 根据当前语言环境判断菜单标识符格式
177
+ const isChinese = isChineseLocale();
73
178
  return items
74
179
  .filter((item) => item.enabled)
75
180
  .map((item) => {
76
- const menuPath = buildMenuPath(parentPath, item.name);
77
- const isAllowed = isMenuAllowed(menuPath, options);
181
+ const menuPath = buildMenuPath(
182
+ parentPath,
183
+ getMenuIdentifier(item, isChinese),
184
+ );
185
+ const isAllowed = isMenuAllowed(menuPath, item, options);
78
186
 
79
187
  // 递归处理子菜单
80
188
  const nextChildren = item.children?.length
@@ -115,15 +223,13 @@ export const filterMenuItems = (
115
223
  * 判断页面的加载类型
116
224
  * 有 htmlUrl 或 jsUrls 时使用 qiankun 微应用加载
117
225
  */
118
- const getLoadType = (page: MenuItem['page']): 'internal' | 'microapp' => {
226
+ const getLoadType = (
227
+ page: PublicPageItem | null | undefined,
228
+ ): 'internal' | 'microapp' => {
119
229
  if (!page) return 'internal';
120
-
121
- // 有 htmlUrl 或 jsUrls 使用 qiankun 微应用加载
122
230
  if (page.htmlUrl || (page.jsUrls && page.jsUrls.length > 0)) {
123
231
  return 'microapp';
124
232
  }
125
-
126
- // 否则使用内部路由
127
233
  return 'internal';
128
234
  };
129
235
 
@@ -131,11 +237,21 @@ const getLoadType = (page: MenuItem['page']): 'internal' | 'microapp' => {
131
237
  * 获取微应用入口 URL
132
238
  * 优先使用 htmlUrl,其次使用 jsUrls[0]
133
239
  */
134
- const getEntry = (page: MenuItem['page']): string | undefined => {
240
+ const getEntry = (
241
+ page: PublicPageItem | null | undefined,
242
+ ): string | undefined => {
135
243
  if (!page) return undefined;
136
244
  return page.htmlUrl || page.jsUrls?.[0] || undefined;
137
245
  };
138
246
 
247
+ /**
248
+ * 获取菜单标识符(默认版本,用于不需要权限匹配的场景)
249
+ * 优先使用 nameKey,兜底使用 name
250
+ */
251
+ export const getMenuIdentifierDefault = (item: MenuItem): string => {
252
+ return item.nameKey || item.name;
253
+ };
254
+
139
255
  /**
140
256
  * 递归提取所有路由配置
141
257
  */
@@ -147,18 +263,22 @@ export const extractRoutes = (
147
263
  for (const item of items) {
148
264
  if (!item.enabled) continue;
149
265
 
150
- const menuPath = buildMenuPath(parentPath, item.name);
151
-
152
- if (item.type === 'page' && item.page && item.page.enabled) {
153
- const loadType = getLoadType(item.page);
154
- routes.push({
155
- path: item.page.route,
156
- name: item.name,
157
- icon: item.icon,
158
- loadType,
159
- entry: getEntry(item.page),
160
- pageConfig: item.page,
161
- });
266
+ const menuPath = buildMenuPath(parentPath, getMenuIdentifierDefault(item));
267
+
268
+ if (item.type === 'page' && item.pageId) {
269
+ const page = getMenuPage(item);
270
+ if (page && page.enabled) {
271
+ routes.push({
272
+ path: page.route,
273
+ base: page.base || '/',
274
+ name: item.name,
275
+ nameEn: item.nameEn,
276
+ icon: item.icon,
277
+ loadType: getLoadType(page),
278
+ entry: getEntry(page),
279
+ pageConfig: page,
280
+ });
281
+ }
162
282
  }
163
283
 
164
284
  if (item.children && item.children.length > 0) {
@@ -169,25 +289,50 @@ export const extractRoutes = (
169
289
  return routes;
170
290
  };
171
291
 
292
+ /**
293
+ * 获取菜单项的显示名称
294
+ * 优先级:name/nameEn > nameKey(走 intl 国际化) > 兜底 name
295
+ */
296
+ export const getMenuLabel = (
297
+ fields: { name?: string; nameEn?: string; nameKey?: string },
298
+ isChinese: boolean,
299
+ ): string => {
300
+ const directName = isChinese ? fields.name : fields.nameEn;
301
+ if (directName) return directName;
302
+
303
+ if (fields.nameKey) {
304
+ const intl = getIntl();
305
+ return intl.formatMessage({
306
+ id: fields.nameKey,
307
+ defaultMessage: fields.nameKey,
308
+ });
309
+ }
310
+
311
+ return fields.name || '';
312
+ };
313
+
172
314
  /**
173
315
  * 将菜单数据转换为菜单组件需要的格式
174
316
  */
175
317
  export const parseMenuItems = (items: MenuItem[]): ParsedMenuItem[] => {
318
+ const isChinese = isChineseLocale();
319
+
176
320
  return items
321
+
177
322
  .filter((item) => item.enabled)
178
323
  .sort((a, b) => a.sortOrder - b.sortOrder)
179
324
  .map((item) => {
180
325
  const parsed: ParsedMenuItem = {
181
326
  key: String(item.id),
182
- label: item.name,
327
+ label: getMenuLabel(item, isChinese),
183
328
  icon: item.icon,
184
329
  type: item.type,
185
330
  };
186
331
 
187
332
  if (item.type === 'link' && item.path) {
188
333
  parsed.path = item.path;
189
- } else if (item.type === 'page' && item.page) {
190
- parsed.path = item.page.route;
334
+ } else if (item.type === 'page') {
335
+ parsed.path = item.path ?? getMenuPage(item)?.route;
191
336
  }
192
337
 
193
338
  if (item.children && item.children.length > 0) {
@@ -198,6 +343,10 @@ export const parseMenuItems = (items: MenuItem[]): ParsedMenuItem[] => {
198
343
  });
199
344
  };
200
345
 
346
+ /** 去除尾部斜杠(根路径 "/" 保持不变) */
347
+ const stripTrailingSlash = (path: string): string =>
348
+ path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path;
349
+
201
350
  /**
202
351
  * 根据路径查找对应的路由配置
203
352
  */
@@ -205,18 +354,19 @@ export const findRouteByPath = (
205
354
  routes: ParsedRoute[],
206
355
  pathname: string,
207
356
  ): ParsedRoute | undefined => {
357
+ const normalizedPathname = stripTrailingSlash(pathname);
208
358
  let exact: ParsedRoute | undefined;
209
359
  let bestWildcard: { route: ParsedRoute; basePath: string } | undefined;
210
360
 
211
361
  for (const route of routes) {
212
- if (route.path === pathname) {
362
+ if (stripTrailingSlash(route.path) === normalizedPathname) {
213
363
  exact = route;
214
364
  continue;
215
365
  }
216
366
 
217
367
  if (route.path.endsWith('/*')) {
218
368
  const basePath = route.path.slice(0, -2);
219
- if (pathname === basePath || pathname.startsWith(`${basePath}/`)) {
369
+ if (normalizedPathname === basePath || normalizedPathname.startsWith(`${basePath}/`)) {
220
370
  if (!bestWildcard || basePath.length > bestWildcard.basePath.length) {
221
371
  bestWildcard = { route, basePath };
222
372
  }
@@ -10,8 +10,14 @@ export interface PageConfig {
10
10
  id: number;
11
11
  /** 页面名称 */
12
12
  name: string;
13
+ /** 英文名称(用于英文环境权限匹配) */
14
+ nameEn?: string;
15
+ /** 菜单唯一标识符(用于权限匹配的兜底) */
16
+ nameKey?: string;
13
17
  /** 路由路径 */
14
18
  route: string;
19
+ /** 路由前缀路径 */
20
+ base: string;
15
21
  /** 是否启用 */
16
22
  enabled: boolean;
17
23
  /** 微应用 HTML 入口 URL (用于 qiankun 加载) */
@@ -24,6 +30,12 @@ export interface PageConfig {
24
30
  prefixPath: string;
25
31
  /** 路由匹配模式 */
26
32
  routeMode: PageRouteMode;
33
+ /** 是否仅超级管理员可见 */
34
+ adminOnly?: boolean;
35
+ /** 是否开启权限控制 */
36
+ accessControlEnabled: boolean;
37
+ /** 路由权限标识(用于匹配 sideMenus) */
38
+ routeKey: string | null;
27
39
  /** 关联的主文档 ID */
28
40
  mainDocumentId: number;
29
41
  /** 所属工作空间子域名 */
@@ -40,6 +52,15 @@ export interface PageConfig {
40
52
  updatedAt: string;
41
53
  }
42
54
 
55
+ /**
56
+ * 公共页面配置 - 从 window.__MICO_PAGES__ 获取
57
+ * PageConfig 的子集,用于动态路由注册和页面级权限控制
58
+ */
59
+ export type PublicPageItem = Omit<
60
+ PageConfig,
61
+ 'workspaceSubdomain' | 'publishedBy' | 'versions' | 'createdAt' | 'updatedAt'
62
+ >;
63
+
43
64
  /**
44
65
  * 菜单项类型
45
66
  */
@@ -51,6 +72,8 @@ export type MenuItemType = 'group' | 'page' | 'link';
51
72
  export interface MenuItem {
52
73
  id: number;
53
74
  name: string;
75
+ nameEn?: string;
76
+ nameKey?: string;
54
77
  type: MenuItemType;
55
78
  /** 路由路径或外链地址 */
56
79
  path: string | null;
@@ -59,8 +82,8 @@ export interface MenuItem {
59
82
  enabled: boolean;
60
83
  sortOrder: number;
61
84
  pageId: number | null;
62
- /** 页面配置(type=page 时存在) */
63
- page: PageConfig | null;
85
+ /** 是否仅超级管理员可见 */
86
+ adminOnly?: boolean;
64
87
  children: MenuItem[];
65
88
  }
66
89
 
@@ -69,15 +92,21 @@ export interface MenuItem {
69
92
  */
70
93
  export interface ParsedRoute {
71
94
  path: string;
72
- /** 菜单显示名称 */
95
+ /** 微应用在主应用中的挂载前缀(来自页面配置 base,可与 path 不同) */
96
+ base: string;
97
+ /** 菜单显示名称(中文) */
73
98
  name: string;
99
+ /** 菜单显示名称(英文) */
100
+ nameEn?: string;
101
+ /** 国际化 key */
102
+ nameKey?: string;
74
103
  icon: string;
75
104
  /** 加载类型: internal(内部路由) | microapp(qiankun微应用) */
76
105
  loadType: 'internal' | 'microapp';
77
106
  /** 微应用入口 URL (htmlUrl 或 jsUrls[0]) */
78
107
  entry?: string;
79
108
  /** 原始页面配置 */
80
- pageConfig?: PageConfig;
109
+ pageConfig?: PublicPageItem;
81
110
  }
82
111
 
83
112
  /**
@@ -97,7 +126,7 @@ export interface ParsedMenuItem {
97
126
  */
98
127
  export interface MicroAppProps {
99
128
  /** 主应用标识 */
100
- mainApp: '<%= projectName %>';
129
+ mainApp: 'common-web';
101
130
  /** 共享的 request 实例,子应用可直接使用 */
102
131
  request: <T = any>(url: string, options?: any) => Promise<T>;
103
132
  }
@@ -118,6 +147,8 @@ export type WorkspaceStatus = 'active' | 'inactive' | 'pending';
118
147
  export interface WorkspaceConfig {
119
148
  /** 关联的应用 ID(可能为空) */
120
149
  appId: number | null;
150
+ /** CAS 服务端登录 URL,用于 SSO 外部登录跳转 */
151
+ casServerLoginUrl?: string;
121
152
  /** 创建时间 */
122
153
  createdAt: string;
123
154
  /** 创建者 ID */
@@ -151,10 +182,43 @@ export interface WorkspaceConfig {
151
182
  */
152
183
  declare global {
153
184
  interface Window {
185
+ __MICO_PAGES__?: PublicPageItem[];
154
186
  __MICO_MENUS__?: MenuItem[];
155
187
  __MICO_CONFIG__?: {
156
188
  appName?: string;
189
+ /** 站点/产品标题,用于登录页标题文案、logo alt 等 */
190
+ title?: string;
191
+ /** 登录页 logo 图片地址,不配置时使用应用内置 logo */
192
+ logo?: string;
193
+ appId?: string;
157
194
  apiBaseUrl?: string;
195
+ proxySuffix?: string;
196
+ loginEndpoint?: string;
197
+ refreshEndpoint?: string;
198
+ externalLoginPath?: string;
199
+ /** 请求客户端配置覆盖,与 config 中 defaultClientOptions 同名字段 */
200
+ defaultClientOptions?: Partial<
201
+ import('../request/types').RequestClientOptions
202
+ >;
203
+ /** 默认重定向路径,访问 "/" 时自动跳转到此路径 */
204
+ defaultPath?: string;
205
+ /**
206
+ * 免认证路由列表(跳过 SSO 登录)
207
+ * 支持精确匹配和前缀匹配(以 /* 结尾)
208
+ */
209
+ noAuthRouteList?: string[];
210
+ /**
211
+ * 免权限校验路由列表(跳过菜单权限检查)
212
+ * 支持精确匹配和前缀匹配(以 /* 结尾)
213
+ * 注意:如果同时需要跳过 SSO,需同时配置 noAuthRouteList
214
+ */
215
+ noPermissionRouteList?: string[];
216
+ /** 动态配置的不显示布局路由列表,支持精确匹配和前缀匹配(以 /* 结尾) */
217
+ noLayoutRouteList?: string[];
218
+ /** 关闭权限控制(菜单全部显示,路由不校验权限) */
219
+ disableAuth?: boolean;
220
+ /** 获取时区列表的 API 地址 */
221
+ timezoneListUrl?: string;
158
222
  [key: string]: unknown;
159
223
  };
160
224
  __MICO_WORKSPACE__?: WorkspaceConfig | null;
@@ -32,6 +32,40 @@ export type {
32
32
  MicroAppErrorSource,
33
33
  } from './types';
34
34
 
35
+
36
+ // ============================================
37
+ // 微应用名称生成
38
+ // ============================================
39
+
40
+ /**
41
+ * 从 entry URL 中提取微应用标识
42
+ * 同一个 entry 的所有路由使用相同的标识,避免频繁卸载/重载微应用
43
+ *
44
+ * 注意:使用完整的 origin + pathname 作为标识,避免同一 host 上多个子应用冲突
45
+ * 例如:http://localhost:8010/app1/ 和 http://localhost:8010/app2/ 会有不同的标识
46
+ *
47
+ * @param entry 微应用入口 URL
48
+ * @returns 微应用标识(字母数字和连字符组成)
49
+ */
50
+ export const getAppNameFromEntry = (entry: string): string => {
51
+ try {
52
+ const url = new URL(entry, window.location.href);
53
+ // 使用 origin + pathname 作为标识,确保不同路径的子应用有不同标识
54
+ // 如 "localhost-8010" 或 "localhost-8010-app1"
55
+ const identifier = url.host + url.pathname;
56
+ return identifier
57
+ .replace(/[^a-zA-Z0-9]/g, '-')
58
+ .replace(/-+/g, '-')
59
+ .replace(/^-|-$/g, '');
60
+ } catch {
61
+ // fallback:使用 entry 的 hash
62
+ return entry
63
+ .replace(/[^a-zA-Z0-9]/g, '-')
64
+ .replace(/-+/g, '-')
65
+ .replace(/^-|-$/g, '');
66
+ }
67
+ };
68
+
35
69
  // ============================================
36
70
  // 环境检测
37
71
  // ============================================
@@ -0,0 +1,109 @@
1
+ /**
2
+ * 微应用预加载管理
3
+ *
4
+ * 策略:当前子应用挂载成功后,再逐个预加载其他子应用
5
+ * 避免与当前加载的子应用竞争网络带宽
6
+ */
7
+
8
+ import { prefetchApps } from 'qiankun';
9
+ import { extractRoutes } from './menu';
10
+ import { getMenus } from './portal-data';
11
+ import { getAppNameFromEntry } from './micro';
12
+
13
+ /**
14
+ * 是否启用微应用预加载
15
+ * - 可通过 URL 参数 ?prefetch=false 禁用(方便调试加载时序问题)
16
+ * - 可通过 localStorage.setItem('DISABLE_MICRO_APP_PREFETCH', 'true') 禁用
17
+ * - 默认启用
18
+ */
19
+ export const isPrefetchEnabled = (): boolean => {
20
+ if (typeof window === 'undefined') return false;
21
+
22
+ // URL 参数优先级最高
23
+ const urlParams = new URLSearchParams(window.location.search);
24
+ const prefetchParam = urlParams.get('prefetch');
25
+ if (prefetchParam === 'false') {
26
+ console.log('[Prefetch] Disabled via URL parameter');
27
+ return false;
28
+ }
29
+
30
+ // localStorage 开关
31
+ if (localStorage.getItem('DISABLE_MICRO_APP_PREFETCH') === 'true') {
32
+ console.log('[Prefetch] Disabled via localStorage');
33
+ return false;
34
+ }
35
+
36
+ return true;
37
+ };
38
+
39
+ /** 已预加载的应用 entry 集合 */
40
+ const prefetchedApps = new Set<string>();
41
+
42
+ /**
43
+ * 获取所有需要预加载的微应用(排除已预加载的)
44
+ */
45
+ export const getMicroAppsForPrefetch = (
46
+ excludeEntry?: string,
47
+ ): Array<{ name: string; entry: string }> => {
48
+ try {
49
+ const menus = getMenus();
50
+ const routes = extractRoutes(menus);
51
+
52
+ return routes
53
+ .filter(
54
+ (route) =>
55
+ route.loadType === 'microapp' &&
56
+ route.entry &&
57
+ route.entry !== excludeEntry &&
58
+ !prefetchedApps.has(route.entry),
59
+ )
60
+ .map((route) => ({
61
+ name: getAppNameFromEntry(route.entry!),
62
+ entry: route.entry!,
63
+ }));
64
+ } catch (error) {
65
+ console.warn('[Prefetch] Failed to get micro apps:', error);
66
+ return [];
67
+ }
68
+ };
69
+
70
+ /**
71
+ * 预加载微应用(低优先级,在浏览器空闲时并发预加载)
72
+ * @param currentEntry 当前正在加载的应用 entry,会被排除
73
+ */
74
+ export const prefetchMicroAppsLowPriority = (currentEntry?: string): void => {
75
+ if (!isPrefetchEnabled()) {
76
+ return;
77
+ }
78
+
79
+ const apps = getMicroAppsForPrefetch(currentEntry);
80
+ if (apps.length === 0) {
81
+ return;
82
+ }
83
+
84
+ // 标记为已预加载,避免重复
85
+ apps.forEach((app) => prefetchedApps.add(app.entry));
86
+
87
+ console.log(
88
+ '[Prefetch] Will prefetch micro apps:',
89
+ apps.map((a) => a.name),
90
+ );
91
+
92
+ // 使用 requestIdleCallback 确保在浏览器空闲时执行,不阻塞当前渲染
93
+ const doPrefetch = () => {
94
+ prefetchApps(apps); // qiankun 会并发预加载所有应用
95
+ };
96
+
97
+ if ('requestIdleCallback' in window) {
98
+ window.requestIdleCallback(doPrefetch, { timeout: 5000 });
99
+ } else {
100
+ setTimeout(doPrefetch, 100);
101
+ }
102
+ };
103
+
104
+ /**
105
+ * 标记应用已加载(避免重复预加载)
106
+ */
107
+ export const markAppAsPrefetched = (entry: string): void => {
108
+ prefetchedApps.add(entry);
109
+ };
@@ -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
+ };