generator-mico-cli 0.2.31 → 0.2.33

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 (179) hide show
  1. package/README.md +145 -18
  2. package/bin/mico.js +76 -0
  3. package/generators/h5-react/ignore-list.json +1 -0
  4. package/generators/h5-react/index.js +349 -0
  5. package/generators/h5-react/meta.json +11 -0
  6. package/generators/h5-react/templates/.commitlintrc.js +7 -0
  7. package/generators/h5-react/templates/.cursor/rules/cicd-deploy.mdc +104 -0
  8. package/generators/h5-react/templates/.cursor/rules/common-intl.mdc +42 -0
  9. package/generators/h5-react/templates/.cursor/rules/git-hooks.mdc +40 -0
  10. package/generators/h5-react/templates/.cursor/rules/internal-packages.mdc +46 -0
  11. package/generators/h5-react/templates/.cursor/rules/monorepo.mdc +64 -0
  12. package/generators/h5-react/templates/.cursor/rules/package-json.mdc +52 -0
  13. package/generators/h5-react/templates/.cursor/rules/tailwind-umi.mdc +60 -0
  14. package/generators/h5-react/templates/.cursor/rules/umi-app.mdc +74 -0
  15. package/generators/h5-react/templates/.cursor/rules/umi-config.mdc +86 -0
  16. package/generators/h5-react/templates/.cursor/rules/umi-mock.mdc +80 -0
  17. package/generators/h5-react/templates/.cursor/rules/workspace-request.mdc +52 -0
  18. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/SKILL.md +213 -0
  19. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/evals/evals.json +23 -0
  20. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/cursor-rule-template.md +60 -0
  21. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/phase-1-scanning.md +102 -0
  22. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/phase-2-context-analysis.md +102 -0
  23. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/phase-3-pattern-extraction.md +105 -0
  24. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/phase-4-module-mapping.md +65 -0
  25. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/phase-5-glossary.md +63 -0
  26. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/templates/DEV_PATTERNS.tpl.md +77 -0
  27. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/templates/GLOSSARY.tpl.md +17 -0
  28. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/templates/MODULE_MAP.tpl.md +45 -0
  29. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/templates/PROJECT_CONTEXT.tpl.md +155 -0
  30. package/generators/h5-react/templates/.cursor/skills/biz-app-analyzer/references/update-mode.md +116 -0
  31. package/generators/h5-react/templates/.env.development +5 -0
  32. package/generators/h5-react/templates/.env.production +5 -0
  33. package/generators/h5-react/templates/.env.testing +5 -0
  34. package/generators/h5-react/templates/.husky/commit-msg +2 -0
  35. package/generators/h5-react/templates/.husky/pre-commit +2 -0
  36. package/generators/h5-react/templates/.lintstagedrc.js +8 -0
  37. package/generators/h5-react/templates/.prettierrc.json +7 -0
  38. package/generators/h5-react/templates/CICD/before_build.sh +76 -0
  39. package/generators/h5-react/templates/CICD/start_dev.sh +54 -0
  40. package/generators/h5-react/templates/CICD/start_local.sh +30 -0
  41. package/generators/h5-react/templates/CICD/start_prod.sh +53 -0
  42. package/generators/h5-react/templates/CICD/start_test.sh +55 -0
  43. package/generators/h5-react/templates/CICD/wangsu_fresh_dev.sh +19 -0
  44. package/generators/h5-react/templates/CICD/wangsu_fresh_prod.sh +19 -0
  45. package/generators/h5-react/templates/CICD/wangsu_fresh_test.sh +19 -0
  46. package/generators/h5-react/templates/README.md +301 -0
  47. package/generators/h5-react/templates/_gitignore +30 -0
  48. package/generators/h5-react/templates/_npmrc +6 -0
  49. package/generators/h5-react/templates/apps/.gitkeep +0 -0
  50. package/generators/h5-react/templates/dev.preset.json +10 -0
  51. package/generators/h5-react/templates/package.json +56 -0
  52. package/generators/h5-react/templates/packages/common-intl/README.md +180 -0
  53. package/generators/h5-react/templates/packages/common-intl/eslint.config.ts +12 -0
  54. package/generators/h5-react/templates/packages/common-intl/package.json +31 -0
  55. package/generators/h5-react/templates/packages/common-intl/src/index.ts +3 -0
  56. package/generators/h5-react/templates/packages/common-intl/src/intl.ts +100 -0
  57. package/generators/h5-react/templates/packages/common-intl/tsconfig.json +3 -0
  58. package/generators/h5-react/templates/packages/components/eslint.config.ts +12 -0
  59. package/generators/h5-react/templates/packages/components/package.json +32 -0
  60. package/generators/h5-react/templates/packages/components/src/Layout/ImmersiveHeader.tsx +126 -0
  61. package/generators/h5-react/templates/packages/components/src/Layout/LayoutFooter.tsx +72 -0
  62. package/generators/h5-react/templates/packages/components/src/Layout/index.tsx +121 -0
  63. package/generators/h5-react/templates/packages/components/src/assets/image/back.png +0 -0
  64. package/generators/h5-react/templates/packages/components/src/index.ts +0 -0
  65. package/generators/h5-react/templates/packages/components/tsconfig.json +13 -0
  66. package/generators/h5-react/templates/packages/components/typings.d.ts +1 -0
  67. package/generators/h5-react/templates/packages/constant/eslint.config.ts +12 -0
  68. package/generators/h5-react/templates/packages/constant/package.json +19 -0
  69. package/generators/h5-react/templates/packages/constant/src/index.ts +0 -0
  70. package/generators/h5-react/templates/packages/constant/src/member.ts +8 -0
  71. package/generators/h5-react/templates/packages/constant/tsconfig.json +3 -0
  72. package/generators/h5-react/templates/packages/deeplink/eslint.config.ts +12 -0
  73. package/generators/h5-react/templates/packages/deeplink/package.json +18 -0
  74. package/generators/h5-react/templates/packages/deeplink/src/index.ts +7 -0
  75. package/generators/h5-react/templates/packages/deeplink/tsconfig.json +3 -0
  76. package/generators/h5-react/templates/packages/domain/eslint.config.ts +12 -0
  77. package/generators/h5-react/templates/packages/domain/package.json +18 -0
  78. package/generators/h5-react/templates/packages/domain/src/index.ts +29 -0
  79. package/generators/h5-react/templates/packages/domain/tsconfig.json +3 -0
  80. package/generators/h5-react/templates/packages/domain/types.d.ts +11 -0
  81. package/generators/h5-react/templates/packages/eslint/eslint.config.base.ts +36 -0
  82. package/generators/h5-react/templates/packages/eslint/eslint.config.react.ts +33 -0
  83. package/generators/h5-react/templates/packages/eslint/package.json +22 -0
  84. package/generators/h5-react/templates/packages/js-bridge/eslint.config.ts +17 -0
  85. package/generators/h5-react/templates/packages/js-bridge/package.json +23 -0
  86. package/generators/h5-react/templates/packages/js-bridge/src/call.ts +126 -0
  87. package/generators/h5-react/templates/packages/js-bridge/src/closeH5Page.ts +9 -0
  88. package/generators/h5-react/templates/packages/js-bridge/src/getUserInfo.ts +96 -0
  89. package/generators/h5-react/templates/packages/js-bridge/src/index.ts +15 -0
  90. package/generators/h5-react/templates/packages/js-bridge/tsconfig.json +3 -0
  91. package/generators/h5-react/templates/packages/js-bridge/type.d.ts +24 -0
  92. package/generators/h5-react/templates/packages/request/axios.d.ts +42 -0
  93. package/generators/h5-react/templates/packages/request/eslint.config.ts +17 -0
  94. package/generators/h5-react/templates/packages/request/package.json +22 -0
  95. package/generators/h5-react/templates/packages/request/src/index.ts +165 -0
  96. package/generators/h5-react/templates/packages/request/src/interceptors.ts +126 -0
  97. package/generators/h5-react/templates/packages/request/src/types.ts +101 -0
  98. package/generators/h5-react/templates/packages/request/src/url-resolver.ts +66 -0
  99. package/generators/h5-react/templates/packages/request/src/utils.ts +12 -0
  100. package/generators/h5-react/templates/packages/request/tsconfig.json +3 -0
  101. package/generators/h5-react/templates/packages/request/umi.d.ts +94 -0
  102. package/generators/h5-react/templates/packages/typescript/package.json +11 -0
  103. package/generators/h5-react/templates/packages/typescript/tsconfig.base.json +23 -0
  104. package/generators/h5-react/templates/packages/typescript/tsconfig.react.json +7 -0
  105. package/generators/h5-react/templates/packages/umi-config/eslint.config.ts +12 -0
  106. package/generators/h5-react/templates/packages/umi-config/package.json +31 -0
  107. package/generators/h5-react/templates/packages/umi-config/src/config.dev.ts +34 -0
  108. package/generators/h5-react/templates/packages/umi-config/src/config.prod.development.ts +17 -0
  109. package/generators/h5-react/templates/packages/umi-config/src/config.prod.production.ts +42 -0
  110. package/generators/h5-react/templates/packages/umi-config/src/config.prod.testing.ts +17 -0
  111. package/generators/h5-react/templates/packages/umi-config/src/config.prod.ts +56 -0
  112. package/generators/h5-react/templates/packages/umi-config/src/config.ts +86 -0
  113. package/generators/h5-react/templates/packages/umi-config/src/index.ts +25 -0
  114. package/generators/h5-react/templates/packages/umi-config/src/plugins/apply-sentry-plugin.ts +57 -0
  115. package/generators/h5-react/templates/packages/umi-config/src/type.d.ts +3 -0
  116. package/generators/h5-react/templates/packages/umi-config/tsconfig.json +3 -0
  117. package/generators/h5-react/templates/packages/utils/eslint.config.ts +12 -0
  118. package/generators/h5-react/templates/packages/utils/package.json +27 -0
  119. package/generators/h5-react/templates/packages/utils/src/date.ts +21 -0
  120. package/generators/h5-react/templates/packages/utils/src/env.ts +40 -0
  121. package/generators/h5-react/templates/packages/utils/src/index.ts +3 -0
  122. package/generators/h5-react/templates/packages/utils/src/md5.ts +17 -0
  123. package/generators/h5-react/templates/packages/utils/src/mock.ts +83 -0
  124. package/generators/h5-react/templates/packages/utils/src/number.ts +23 -0
  125. package/generators/h5-react/templates/packages/utils/src/tailwind.ts +12 -0
  126. package/generators/h5-react/templates/packages/utils/src/url.ts +19 -0
  127. package/generators/h5-react/templates/packages/utils/tsconfig.json +9 -0
  128. package/generators/h5-react/templates/page.config.ts +1 -0
  129. package/generators/h5-react/templates/pnpm-workspace.yaml +17 -0
  130. package/generators/h5-react/templates/scripts/collect-dist.js +78 -0
  131. package/generators/h5-react/templates/scripts/dev-preset.js +265 -0
  132. package/generators/h5-react/templates/scripts/dev-preset.schema.json +39 -0
  133. package/generators/h5-react/templates/scripts/dev.js +133 -0
  134. package/generators/h5-react/templates/scripts/gateway.ts +241 -0
  135. package/generators/h5-react/templates/turbo.json +86 -0
  136. package/generators/micro-react/templates/apps/layout/config/config.dev.ts +2 -0
  137. package/generators/micro-react/templates/apps/layout/docs/feature-/345/244/264/351/203/250/344/270/200/347/272/247/345/257/274/350/210/252/345/270/203/345/261/200.md +166 -0
  138. package/generators/micro-react/templates/apps/layout/src/app.tsx +20 -2
  139. package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +36 -0
  140. package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +6 -0
  141. package/generators/micro-react/templates/apps/layout/src/common/request/index.ts +8 -2
  142. package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +128 -11
  143. package/generators/micro-react/templates/apps/layout/src/constants/index.ts +22 -0
  144. package/generators/micro-react/templates/apps/layout/src/hooks/useFilteredMenuItems.ts +29 -0
  145. package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.less +73 -0
  146. package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.tsx +51 -4
  147. package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +25 -18
  148. package/generators/micro-react/templates/apps/layout/src/locales/en-US.ts +4 -0
  149. package/generators/micro-react/templates/apps/layout/src/locales/zh-CN.ts +4 -0
  150. package/generators/micro-react/templates/apps/layout/src/services/config/index.ts +1 -0
  151. package/generators/micro-react/templates/packages/common-intl/src/intl.ts +20 -0
  152. package/generators/subapp-h5/ignore-list.json +1 -0
  153. package/generators/subapp-h5/index.js +424 -0
  154. package/generators/subapp-h5/meta.json +10 -0
  155. package/generators/subapp-h5/templates/.env +1 -0
  156. package/generators/subapp-h5/templates/.stylelintrc.js +22 -0
  157. package/generators/subapp-h5/templates/config/config.dev.ts +7 -0
  158. package/generators/subapp-h5/templates/config/config.prod.development.ts +7 -0
  159. package/generators/subapp-h5/templates/config/config.prod.production.ts +10 -0
  160. package/generators/subapp-h5/templates/config/config.prod.testing.ts +7 -0
  161. package/generators/subapp-h5/templates/config/config.prod.ts +7 -0
  162. package/generators/subapp-h5/templates/config/config.ts +6 -0
  163. package/generators/subapp-h5/templates/config/routes.ts +13 -0
  164. package/generators/subapp-h5/templates/eslint.config.ts +12 -0
  165. package/generators/subapp-h5/templates/mock/user.ts +34 -0
  166. package/generators/subapp-h5/templates/package.json +42 -0
  167. package/generators/subapp-h5/templates/src/app.tsx +14 -0
  168. package/generators/subapp-h5/templates/src/assets/yay.jpg +0 -0
  169. package/generators/subapp-h5/templates/src/intl.ts +37 -0
  170. package/generators/subapp-h5/templates/src/layouts/index.tsx +10 -0
  171. package/generators/subapp-h5/templates/src/pages/index.tsx +22 -0
  172. package/generators/subapp-h5/templates/src/services/user.ts +38 -0
  173. package/generators/subapp-h5/templates/tailwind.config.js +16 -0
  174. package/generators/subapp-h5/templates/tailwind.css +7 -0
  175. package/generators/subapp-h5/templates/tsconfig.json +3 -0
  176. package/generators/subapp-h5/templates/typings.d.ts +1 -0
  177. package/lib/setup-multica-desktop.js +154 -0
  178. package/lib/utils.js +25 -0
  179. package/package.json +1 -1
@@ -13,6 +13,7 @@ import { isNoAuthRoute, ROUTES } from '@/constants';
13
13
  import { captureError } from '@common-web/sentry';
14
14
  import { formatLayoutMessage } from '@/common/intl';
15
15
  import { Modal } from '@mico-platform/ui';
16
+ import React from 'react';
16
17
  import {
17
18
  getTicketParam,
18
19
  resolveAuthToken,
@@ -36,6 +37,38 @@ let tokenAcquiredAt: number | null = null;
36
37
  /** 登录失败弹窗是否已显示,防止多个请求同时失败时重复弹出 */
37
38
  let redirectLimitModalShown = false;
38
39
 
40
+ /** 是否正在执行 SSO 硬跳转,置位后用于阻断渲染与拦截在途请求 */
41
+ let redirecting = false;
42
+
43
+ /** 是否正在执行 SSO 硬跳转 */
44
+ export const isRedirecting = (): boolean => redirecting;
45
+
46
+ /**
47
+ * ticket 换 token 阶段的网络层故障(Failed to fetch / NetworkError 等)。
48
+ * 由 ensureSsoSession() 在 catch 阶段抛出,供调用方识别是否为网络异常,
49
+ * 避免使用模块级 mutable 状态在函数间传递信号。
50
+ */
51
+ export class SsoNetworkError extends Error {
52
+ constructor(
53
+ message: string,
54
+ public readonly cause?: unknown,
55
+ ) {
56
+ super(message);
57
+ this.name = 'SsoNetworkError';
58
+ }
59
+ }
60
+
61
+ /** 判断错误是否为网络层故障(无法建立连接,而非 HTTP 错误响应) */
62
+ const isNetworkError = (err: Error): boolean => {
63
+ const msg = err.message.toLowerCase();
64
+ return (
65
+ msg.includes('failed to fetch') ||
66
+ msg.includes('networkerror') ||
67
+ msg.includes('network error') ||
68
+ msg.includes('load failed')
69
+ );
70
+ };
71
+
39
72
  /**
40
73
  * 执行 SSO 重定向跳转,回跳地址中固定携带 redirect_count=1
41
74
  */
@@ -49,6 +82,9 @@ const performSsoRedirect = (): void => {
49
82
  redirectUrl.searchParams.delete('ticket');
50
83
  const serviceUrl = redirectUrl.toString();
51
84
 
85
+ // 在赋值 location.href 之前置位,浏览器导航期间用于阻断渲染与拦截在途请求
86
+ redirecting = true;
87
+
52
88
  window.location.href = `${
53
89
  externalLoginPath ?? ROUTES.LOGIN
54
90
  }?service=${encodeURIComponent(serviceUrl)}`;
@@ -56,21 +92,28 @@ const performSsoRedirect = (): void => {
56
92
 
57
93
  /**
58
94
  * 处理认证失败后的重定向
95
+ *
96
+ * @param options.isNetworkError 是否由 ticket 换 token 阶段的网络异常触发,
97
+ * true 时弹窗展示 IOA/网络排障指引;其它认证失败入口(401 重试失败、登出等)
98
+ * 不传或传 false,使用默认「自动登录失败」文案。
59
99
  */
60
- export const handleAuthFailureRedirect = (): void => {
61
- if (typeof window === 'undefined') return;
100
+ export const handleAuthFailureRedirect = (
101
+ options: { isNetworkError?: boolean } = {},
102
+ ): boolean => {
103
+ const { isNetworkError: isNetworkErr = false } = options;
104
+ if (typeof window === 'undefined') return false;
62
105
 
63
106
  const authCheckPath = resolveAuthCheckPath(window.location.pathname);
64
107
 
65
108
  // 与 getInitialState 一致:用 authCheckPath 判断,避免「/ + defaultPath」首屏仍按 / 误判为免认证
66
109
  if (isNoAuthRoute(authCheckPath)) {
67
110
  console.log('[SSO] 当前路由在免认证列表中,跳过 SSO 重定向', { authCheckPath });
68
- return;
111
+ return false;
69
112
  }
70
113
 
71
114
  if (isPageAuthFree(authCheckPath)) {
72
115
  console.log('[SSO] 页面 accessControlEnabled=false,跳过 SSO 重定向', { authCheckPath });
73
- return;
116
+ return false;
74
117
  }
75
118
 
76
119
  // 从 URL 中获取当前 redirect 登录的次数
@@ -85,7 +128,7 @@ export const handleAuthFailureRedirect = (): void => {
85
128
  // 如果 redirect 次数小于 1 次,则执行自动 redirect 跳转登录
86
129
  if (redirectCount < 1) {
87
130
  performSsoRedirect();
88
- return;
131
+ return true;
89
132
  }
90
133
 
91
134
  // redirect 次数已达上限,停止自动重定向,记录日志并弹窗询问用户是否手动重试
@@ -105,18 +148,69 @@ export const handleAuthFailureRedirect = (): void => {
105
148
  ? new Date(tokenAcquiredAt).toISOString()
106
149
  : null,
107
150
  tokenAgeSeconds: tokenAge,
151
+ isNetworkError: isNetworkErr,
108
152
  },
109
153
  });
110
154
 
111
155
  if (redirectLimitModalShown) {
112
156
  console.log('[SSO] 登录失败弹窗已显示,跳过重复弹出');
113
- return;
157
+ return false;
114
158
  }
115
159
 
116
160
  redirectLimitModalShown = true;
161
+ const secondaryStyle: React.CSSProperties = {
162
+ marginTop: 8,
163
+ color: 'var(--color-text-3, #86909c)',
164
+ fontSize: 13,
165
+ };
166
+ const modalContent = isNetworkErr
167
+ ? React.createElement(
168
+ 'div',
169
+ null,
170
+ React.createElement(
171
+ 'div',
172
+ null,
173
+ formatLayoutMessage({
174
+ id: 'sso_auth_failure_network_primary',
175
+ defaultMessage: '网络异常,无法完成自动登录',
176
+ }),
177
+ ),
178
+ React.createElement(
179
+ 'div',
180
+ { style: secondaryStyle },
181
+ React.createElement(
182
+ 'div',
183
+ null,
184
+ formatLayoutMessage({
185
+ id: 'sso_auth_failure_network_hint_title',
186
+ defaultMessage: '请检查以下情况:',
187
+ }),
188
+ ),
189
+ React.createElement(
190
+ 'div',
191
+ null,
192
+ formatLayoutMessage({
193
+ id: 'sso_auth_failure_network_hint_ioa',
194
+ defaultMessage: '1. IOA 是否已连接',
195
+ }),
196
+ ),
197
+ React.createElement(
198
+ 'div',
199
+ null,
200
+ formatLayoutMessage({
201
+ id: 'sso_auth_failure_network_hint_network',
202
+ defaultMessage: '2. 当前网络是否可正常访问',
203
+ }),
204
+ ),
205
+ ),
206
+ )
207
+ : formatLayoutMessage({
208
+ id: 'sso_auth_failure_modal_content',
209
+ defaultMessage: '自动登录失败,是否重新尝试登录?',
210
+ });
117
211
  Modal.confirm({
118
212
  title: formatLayoutMessage({ id: 'sso_auth_failure_modal_title', defaultMessage: '登录提示' }),
119
- content: formatLayoutMessage({ id: 'sso_auth_failure_modal_content', defaultMessage: '自动登录失败,是否重新尝试登录?' }),
213
+ content: modalContent,
120
214
  okText: formatLayoutMessage({ id: 'sso_auth_failure_modal_ok', defaultMessage: '重新登录' }),
121
215
  cancelText: formatLayoutMessage({ id: 'sso_auth_failure_modal_cancel', defaultMessage: '取消' }),
122
216
  onOk: () => {
@@ -128,6 +222,9 @@ export const handleAuthFailureRedirect = (): void => {
128
222
  redirectLimitModalShown = false;
129
223
  },
130
224
  });
225
+
226
+ // 仅弹出 Modal 询问,未执行硬跳转
227
+ return false;
131
228
  };
132
229
 
133
230
  /**
@@ -180,12 +277,32 @@ export const ensureSsoSession = async (): Promise<void> => {
180
277
  throw new Error('SSO 认证失败');
181
278
  }
182
279
  } catch (error) {
183
- console.error('SSO ticket exchange failed', error);
280
+ const err = error instanceof Error ? error : new Error(String(error));
281
+ const networkError = isNetworkError(err);
282
+ console.error('[SSO] ticket 交换失败', err.message, err.stack);
283
+ captureError(err, {
284
+ tags: {
285
+ scene: 'sso_ticket_exchange_failed',
286
+ is_network_error: String(networkError),
287
+ },
288
+ extra: {
289
+ ticketParam,
290
+ message: err.message,
291
+ },
292
+ });
184
293
  setFetchingToken(false);
185
294
 
186
- rejectPendingRequests(
187
- error instanceof Error ? error : new Error('SSO 认证失败'),
188
- );
295
+ const propagatedErr = networkError
296
+ ? new SsoNetworkError(err.message, err)
297
+ : err;
298
+ rejectPendingRequests(propagatedErr);
299
+
300
+ // 仅在网络异常时向外抛出,让调用方据此切换 UI 文案;
301
+ // 其它失败(如 SSO 业务态失败)保持原有「静默」语义,由调用方
302
+ // 通过 token 是否就绪判断后续走向。
303
+ if (networkError) {
304
+ throw propagatedErr;
305
+ }
189
306
  } finally {
190
307
  removeParamFromUrl(ticketParam);
191
308
  ticketPromise = null;
@@ -132,6 +132,28 @@ export const isAuthDisabled = (): boolean => {
132
132
  return window.__MICO_CONFIG__?.disableAuth === true;
133
133
  };
134
134
 
135
+ /**
136
+ * 布局模式
137
+ * - 'side':经典左侧完整目录树
138
+ * - 'mix':一级目录显示在头部 Tab,对应子级目录显示在侧边栏
139
+ */
140
+ export type TLayoutMode = 'side' | 'mix';
141
+
142
+ /**
143
+ * 获取布局模式
144
+ * 读取 window.__MICO_CONFIG__.layoutMode,未配置时默认 'side'(兼容旧版)
145
+ */
146
+ export const getLayoutMode = (): TLayoutMode => {
147
+ return window.__MICO_CONFIG__?.layoutMode === 'mix' ? 'mix' : 'side';
148
+ };
149
+
150
+ /**
151
+ * 判断是否为「头部一级 + 侧边二级」混合布局
152
+ */
153
+ export const isMixLayout = (): boolean => {
154
+ return getLayoutMode() === 'mix';
155
+ };
156
+
135
157
  /**
136
158
  * 主题相关常量
137
159
  */
@@ -0,0 +1,29 @@
1
+ import type { ParsedMenuItem } from '@/common/menu';
2
+ import { filterMenuItems, parseMenuItems } from '@/common/menu';
3
+ import { getMenus } from '@/common/portal-data';
4
+ import { isAuthDisabled } from '@/constants';
5
+ import { useModel } from '@umijs/max';
6
+ import { useMemo } from 'react';
7
+
8
+ /**
9
+ * 权限过滤 + 解析后的菜单树
10
+ * 供侧边栏与头部一级导航共用,避免重复解析
11
+ */
12
+ export const useFilteredMenuItems = (): ParsedMenuItem[] => {
13
+ const { initialState } = useModel('@@initialState');
14
+ const currentUser = initialState?.currentUser;
15
+
16
+ return useMemo(() => {
17
+ const menus = getMenus();
18
+ if (isAuthDisabled()) {
19
+ return parseMenuItems(menus);
20
+ }
21
+ const filteredMenus = filterMenuItems(menus, {
22
+ isSuperuser: currentUser?.is_superuser,
23
+ menuPerms: currentUser?.menu_perms || [],
24
+ });
25
+ return parseMenuItems(filteredMenus);
26
+ }, [currentUser?.is_superuser, currentUser?.menu_perms]);
27
+ };
28
+
29
+ export default useFilteredMenuItems;
@@ -40,6 +40,7 @@
40
40
  .layout-header-logo {
41
41
  display: flex;
42
42
  align-items: center;
43
+ // side 布局保持原行为:Logo 居左,右侧内容靠右
43
44
  margin-right: auto;
44
45
 
45
46
  .logo-text {
@@ -47,12 +48,84 @@
47
48
  font-weight: @font-weight-bold;
48
49
  color: @Brand1-6;
49
50
  letter-spacing: 1px;
51
+ overflow: hidden;
52
+ white-space: nowrap;
53
+ text-overflow: ellipsis;
54
+ }
55
+ }
56
+
57
+ // mix 布局:Logo 列对齐侧边栏宽度,使一级导航起始 x 与内容区左边界对齐
58
+ // 左侧 padding(@spacing-lg) + 此宽度 = 侧边栏右边界(@sidebar-width)
59
+ .layout-header--mix .layout-header-logo {
60
+ width: calc(@sidebar-width - @spacing-lg);
61
+ margin-right: 0;
62
+ flex-shrink: 0;
63
+ }
64
+
65
+ // 头部一级目录导航(mix 布局)
66
+ // 样式对齐 Figma 中台组件「水平 Tab」:14/22 PingFang SC,
67
+ // 激活态品牌紫 + Medium + 底部下划线,未激活 text/2 + Regular
68
+ .layout-header-nav {
69
+ flex: 1;
70
+ display: flex;
71
+ align-items: center;
72
+ overflow: hidden;
73
+ margin-left: @spacing-xl;
74
+
75
+ .layout-header-tabs.arco-tabs {
76
+ width: 100%;
77
+
78
+ // 隐藏整条底部分隔线,仅保留激活态下划线,避免与头部阴影叠加
79
+ .arco-tabs-header-nav::before {
80
+ display: none;
81
+ }
82
+
83
+ .arco-tabs-header-title {
84
+ // 字号 14 / 行高 22,标题为「胶囊」形态,hover 显示浅灰圆角底
85
+ margin: 0 8px;
86
+ padding: 5px 12px;
87
+ font-size: 14px;
88
+ line-height: 22px;
89
+ font-weight: 400;
90
+ color: @color-text-2;
91
+ border-radius: @border-radius-md;
92
+ transition: background-color @animation-duration-fast,
93
+ color @animation-duration-fast;
94
+
95
+ &:first-child {
96
+ margin-left: 0;
97
+ }
98
+
99
+ // 未选中 hover:仅加浅灰底(fill/2),文字保持灰色不变紫
100
+ &:hover {
101
+ color: @color-text-2;
102
+ background-color: @color-fill-2;
103
+ }
104
+
105
+ // 选中态:品牌紫 + Medium
106
+ &-active {
107
+ color: @Brand1-6;
108
+ font-weight: 500;
109
+ }
110
+
111
+ // 选中 + hover:品牌紫文字 + 浅灰圆角底
112
+ &-active:hover {
113
+ color: @Brand1-6;
114
+ background-color: @color-fill-2;
115
+ }
116
+ }
117
+
118
+ // 激活下划线(ink bar):品牌紫
119
+ .arco-tabs-header-ink {
120
+ background-color: @Brand1-6;
121
+ }
50
122
  }
51
123
  }
52
124
 
53
125
  .layout-header-right {
54
126
  display: flex;
55
127
  align-items: center;
128
+ margin-left: auto;
56
129
  }
57
130
 
58
131
  .layout-header-action {
@@ -1,14 +1,45 @@
1
+ import { findFirstLeaf, getActiveTopKey } from '@/common/menu';
1
2
  import { AvatarDropdown, TenantDropdown } from '@/components/RightContent';
2
- import { DEFAULT_NAME } from '@/constants';
3
- import { Layout, Space } from '@mico-platform/ui';
4
- import React from 'react';
3
+ import { DEFAULT_NAME, isMixLayout } from '@/constants';
4
+ import { useFilteredMenuItems } from '@/hooks/useFilteredMenuItems';
5
+ import { Layout, Space, Tabs } from '@mico-platform/ui';
6
+ import { history, useLocation } from '@umijs/max';
7
+ import React, { useMemo } from 'react';
5
8
  import './index.less';
6
9
 
7
10
  const Header = Layout.Header;
11
+ const TabPane = Tabs.TabPane;
8
12
 
9
13
  const LayoutHeader: React.FC = () => {
14
+ const location = useLocation();
15
+ const menuItems = useFilteredMenuItems();
16
+ const mix = isMixLayout();
17
+
18
+ // 头部一级导航数据(仅 mix 布局使用顶层菜单项)
19
+ const topMenuItems = mix ? menuItems : [];
20
+
21
+ const activeTopKey = useMemo(() => {
22
+ if (!mix) return undefined;
23
+ return getActiveTopKey(topMenuItems, location.pathname);
24
+ }, [mix, topMenuItems, location.pathname]);
25
+
26
+ // 点击一级 Tab:跳转到该一级目录下第一个可跳转叶子
27
+ // - 外链(link):新窗口打开,避免外部 URL 被推进路由
28
+ // - 内部页面(page):路由跳转
29
+ const handleTopTabChange = (key: string) => {
30
+ const target = topMenuItems.find((item) => item.key === key);
31
+ if (!target) return;
32
+ const leaf = findFirstLeaf(target);
33
+ if (!leaf?.path) return;
34
+ if (leaf.type === 'link') {
35
+ window.open(leaf.path, '_blank');
36
+ return;
37
+ }
38
+ history.push(leaf.path);
39
+ };
40
+
10
41
  return (
11
- <Header className="layout-header">
42
+ <Header className={`layout-header${mix ? ' layout-header--mix' : ''}`}>
12
43
  {/* Logo */}
13
44
  <div className="layout-header-logo">
14
45
  <span className="logo-text">
@@ -16,6 +47,22 @@ const LayoutHeader: React.FC = () => {
16
47
  </span>
17
48
  </div>
18
49
 
50
+ {/* 一级目录导航(mix 布局) */}
51
+ {mix && topMenuItems.length > 0 && (
52
+ <div className="layout-header-nav">
53
+ <Tabs
54
+ className="layout-header-tabs"
55
+ type="line"
56
+ activeTab={activeTopKey}
57
+ onChange={handleTopTabChange}
58
+ >
59
+ {topMenuItems.map((item) => (
60
+ <TabPane key={item.key} title={item.label} />
61
+ ))}
62
+ </Tabs>
63
+ </div>
64
+ )}
65
+
19
66
  {/* Right section */}
20
67
  <div className="layout-header-right">
21
68
  <Space size="medium">
@@ -1,13 +1,13 @@
1
1
  import type { ParsedMenuItem } from '@/common/menu';
2
- import { filterMenuItems, parseMenuItems } from '@/common/menu';
3
- import { getMenus } from '@/common/portal-data';
2
+ import { getActiveTopKey } from '@/common/menu';
4
3
  import IconFont from '@/components/IconFont';
5
- import { isAuthDisabled } from '@/constants';
4
+ import { isMixLayout } from '@/constants';
5
+ import { useFilteredMenuItems } from '@/hooks/useFilteredMenuItems';
6
6
  import { useMenuState } from '@/hooks/useMenuState';
7
7
  import { useTheme } from '@/hooks/useTheme';
8
8
  import { Layout, Menu } from '@mico-platform/ui';
9
9
  import * as Icons from '@mico-platform/ui/icon';
10
- import { useModel } from '@umijs/max';
10
+ import { useLocation } from '@umijs/max';
11
11
  import React, { useEffect, useMemo, useRef } from 'react';
12
12
  import './index.less';
13
13
 
@@ -115,23 +115,25 @@ interface LayoutMenuProps {
115
115
  const LayoutMenu: React.FC<LayoutMenuProps> = () => {
116
116
  const siderRef = useRef<HTMLDivElement>(null);
117
117
  const { isDark } = useTheme();
118
- const { initialState } = useModel('@@initialState');
119
- const currentUser = initialState?.currentUser;
118
+ const location = useLocation();
120
119
 
121
- // Parse menu data
120
+ // 权限过滤 + 解析后的完整菜单树(与头部一级导航共用)
122
121
  // getMenuItemFilterOutcome 内部已对每个菜单项单独检查 isNoPermissionRoute,
123
122
  // 无需在此按当前页面路径全局跳过过滤,避免菜单可见性随页面变化
123
+ const allMenuItems = useFilteredMenuItems();
124
+
125
+ // mix 布局:侧边栏只渲染「当前激活一级目录」下的子树;side 布局:渲染完整树
124
126
  const menuItems = useMemo(() => {
125
- const menus = getMenus();
126
- if (isAuthDisabled()) {
127
- return parseMenuItems(menus);
127
+ if (!isMixLayout()) {
128
+ return allMenuItems;
128
129
  }
129
- const filteredMenus = filterMenuItems(menus, {
130
- isSuperuser: currentUser?.is_superuser,
131
- menuPerms: currentUser?.menu_perms || [],
132
- });
133
- return parseMenuItems(filteredMenus);
134
- }, [currentUser?.is_superuser, currentUser?.menu_perms]);
130
+ const activeTopKey = getActiveTopKey(allMenuItems, location.pathname);
131
+ const activeTop = allMenuItems.find((item) => item.key === activeTopKey);
132
+ return activeTop?.children ?? [];
133
+ }, [allMenuItems, location.pathname]);
134
+
135
+ // mix 布局下,当前一级目录没有子目录时隐藏侧边栏
136
+ const hideSider = isMixLayout() && menuItems.length === 0;
135
137
 
136
138
  // 使用菜单状态 Hook
137
139
  const {
@@ -150,10 +152,10 @@ const LayoutMenu: React.FC<LayoutMenuProps> = () => {
150
152
  if (layoutContent) {
151
153
  (layoutContent as HTMLElement).style.setProperty(
152
154
  '--sider-width',
153
- collapsed ? '48px' : '200px',
155
+ hideSider ? '0px' : collapsed ? '48px' : '200px',
154
156
  );
155
157
  }
156
- }, [collapsed]);
158
+ }, [collapsed, hideSider]);
157
159
 
158
160
  useEffect(() => {
159
161
  const sider = siderRef.current;
@@ -166,6 +168,11 @@ const LayoutMenu: React.FC<LayoutMenuProps> = () => {
166
168
  };
167
169
  }, [handleMouseEnter, handleMouseLeave]);
168
170
 
171
+ // 一级目录无子目录:不渲染侧边栏(内容区占满,--sider-width 已置 0)
172
+ if (hideSider) {
173
+ return null;
174
+ }
175
+
169
176
  // 点击触发按钮图标
170
177
  const clickTriggerBtnIcon = collapsed
171
178
  ? 'webcs-outline_fold'
@@ -48,6 +48,10 @@ export default {
48
48
  sso_auth_failure_modal_content: 'Auto login failed. Would you like to try logging in again?',
49
49
  sso_auth_failure_modal_ok: 'Re-login',
50
50
  sso_auth_failure_modal_cancel: 'Cancel',
51
+ sso_auth_failure_network_primary: 'Network error. Unable to complete auto login.',
52
+ sso_auth_failure_network_hint_title: 'Please check:',
53
+ sso_auth_failure_network_hint_ioa: '1. Whether IOA is connected',
54
+ sso_auth_failure_network_hint_network: '2. Whether your network can access the service',
51
55
 
52
56
  // AvatarDropdown
53
57
  'avatar.language': 'Language',
@@ -45,6 +45,10 @@ export default {
45
45
  sso_auth_failure_modal_content: '自动登录失败,是否重新尝试登录?',
46
46
  sso_auth_failure_modal_ok: '重新登录',
47
47
  sso_auth_failure_modal_cancel: '取消',
48
+ sso_auth_failure_network_primary: '网络异常,无法完成自动登录',
49
+ sso_auth_failure_network_hint_title: '请检查以下情况:',
50
+ sso_auth_failure_network_hint_ioa: '1. IOA 是否已连接',
51
+ sso_auth_failure_network_hint_network: '2. 当前网络是否可正常访问',
48
52
 
49
53
  // AvatarDropdown
50
54
  'avatar.language': '语言',
@@ -16,6 +16,7 @@ const STATIC_TIMEZONE_LIST: ITimezone[] = [
16
16
  { region: 'DE', utc_offset: 1 },
17
17
  { region: 'FJ', utc_offset: 15 },
18
18
  { region: 'FR', utc_offset: 1 },
19
+ { region: 'GB', utc_offset: 0 },
19
20
  { region: 'Global', utc_offset: 8 },
20
21
  { region: 'ID', utc_offset: 7 },
21
22
  { region: 'IN', utc_offset: 5 },
@@ -132,6 +132,26 @@ const intl = addIntl({
132
132
  key: 'sso_auth_failure_modal_cancel',
133
133
  defaultMessage: '取消',
134
134
  }),
135
+ sso_auth_failure_network_primary: () =>
136
+ i18n({
137
+ key: 'sso_auth_failure_network_primary',
138
+ defaultMessage: '网络异常,无法完成自动登录',
139
+ }),
140
+ sso_auth_failure_network_hint_title: () =>
141
+ i18n({
142
+ key: 'sso_auth_failure_network_hint_title',
143
+ defaultMessage: '请检查以下情况:',
144
+ }),
145
+ sso_auth_failure_network_hint_ioa: () =>
146
+ i18n({
147
+ key: 'sso_auth_failure_network_hint_ioa',
148
+ defaultMessage: '1. IOA 是否已连接',
149
+ }),
150
+ sso_auth_failure_network_hint_network: () =>
151
+ i18n({
152
+ key: 'sso_auth_failure_network_hint_network',
153
+ defaultMessage: '2. 当前网络是否可正常访问',
154
+ }),
135
155
  }, i18n);
136
156
 
137
157
  export default intl;
@@ -0,0 +1 @@
1
+ ["node_modules", "dist", ".umi", ".umi-production", ".turbo"]