generator-mico-cli 0.2.28 → 0.2.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -20
- package/bin/mico.js +27 -62
- package/generators/micro-react/index.js +25 -1
- package/generators/micro-react/templates/.cursor/rules/always-read-docs.mdc +3 -0
- package/generators/micro-react/templates/.cursor/rules/project-overview.mdc +1 -0
- package/generators/micro-react/templates/CICD/start_dev.sh +11 -0
- package/generators/micro-react/templates/CICD/start_local.sh +9 -0
- package/generators/micro-react/templates/CICD/start_prod.sh +13 -0
- package/generators/micro-react/templates/CICD/start_test.sh +11 -0
- package/generators/micro-react/templates/CLAUDE.md +1 -0
- package/generators/micro-react/templates/README.md +1 -1
- package/generators/micro-react/templates/apps/layout/config/config.dev.ts +13 -5
- package/generators/micro-react/templates/apps/layout/config/config.prod.development.ts +12 -0
- package/generators/micro-react/templates/apps/layout/config/config.prod.testing.ts +12 -0
- package/generators/micro-react/templates/apps/layout/config/config.prod.ts +14 -0
- package/generators/micro-react/templates/apps/layout/docs/feature-PermissionFilter/346/214/211/351/222/256/346/235/203/351/231/220.md +116 -0
- package/generators/micro-react/templates/apps/layout/docs/feature-/345/233/275/351/231/205/345/214/226.md +121 -0
- package/generators/micro-react/templates/apps/layout/docs/feature-/345/276/256/345/211/215/347/253/257/346/250/241/345/274/217.md +8 -0
- package/generators/micro-react/templates/apps/layout/docs/feature-/350/217/234/345/215/225/346/235/203/351/231/220/346/216/247/345/210/266.md +83 -77
- package/generators/micro-react/templates/apps/layout/docs/feature-/350/267/257/347/224/261/344/270/216/350/217/234/345/215/225/350/247/243/350/200/246.md +50 -35
- package/generators/micro-react/templates/apps/layout/docs/feature-/350/267/257/347/224/261/346/235/203/351/231/220/346/227/245/345/277/227.md +162 -0
- package/generators/micro-react/templates/apps/layout/mock/api.mock.ts +23 -31
- package/generators/micro-react/templates/apps/layout/mock/menus.ts +14 -0
- package/generators/micro-react/templates/apps/layout/mock/pages.ts +27 -8
- package/generators/micro-react/templates/apps/layout/package.json +2 -0
- package/generators/micro-react/templates/apps/layout/src/app.tsx +85 -4
- package/generators/micro-react/templates/apps/layout/src/common/auth/index.ts +3 -0
- package/generators/micro-react/templates/apps/layout/src/common/auth/tenant.ts +25 -0
- package/generators/micro-react/templates/apps/layout/src/common/auth/type.ts +41 -27
- package/generators/micro-react/templates/apps/layout/src/common/intl/formatLayoutMessage.ts +30 -0
- package/generators/micro-react/templates/apps/layout/src/common/intl/index.ts +6 -0
- package/generators/micro-react/templates/apps/layout/src/common/intl/intlRuntime.ts +14 -0
- package/generators/micro-react/templates/apps/layout/src/common/intl/localeMapping.ts +30 -0
- package/generators/micro-react/templates/apps/layout/src/common/intl/types.ts +14 -0
- package/generators/micro-react/templates/apps/layout/src/common/intl/useLayoutIntl.ts +40 -0
- package/generators/micro-react/templates/apps/layout/src/common/logger.ts +3 -4
- package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +148 -85
- package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +29 -6
- package/generators/micro-react/templates/apps/layout/src/common/micro/types.ts +23 -0
- package/generators/micro-react/templates/apps/layout/src/common/portal-data.ts +46 -2
- package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +74 -15
- package/generators/micro-react/templates/apps/layout/src/common/request/token-refresh.ts +2 -0
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +32 -6
- package/generators/micro-react/templates/apps/layout/src/components/PermissionFilter/index.tsx +51 -0
- package/generators/micro-react/templates/apps/layout/src/components/RightContent/AvatarDropdown.tsx +10 -1
- package/generators/micro-react/templates/apps/layout/src/components/RightContent/TenantDropdown.tsx +76 -0
- package/generators/micro-react/templates/apps/layout/src/components/RightContent/index.ts +1 -0
- package/generators/micro-react/templates/apps/layout/src/components/RightContent/tenant-dropdown.less +48 -0
- package/generators/micro-react/templates/apps/layout/src/constants/index.ts +1 -0
- package/generators/micro-react/templates/apps/layout/src/hooks/index.ts +1 -0
- package/generators/micro-react/templates/apps/layout/src/hooks/useMenuState.ts +18 -0
- package/generators/micro-react/templates/apps/layout/src/hooks/useTenant.ts +41 -0
- package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.tsx +4 -1
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +21 -9
- package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +105 -60
- package/generators/micro-react/templates/apps/layout/src/locales/en-US.ts +28 -0
- package/generators/micro-react/templates/apps/layout/src/locales/zh-CN.ts +26 -0
- package/generators/micro-react/templates/apps/layout/src/pages/404/index.tsx +7 -3
- package/generators/micro-react/templates/apps/layout/src/pages/Home/index.less +32 -0
- package/generators/micro-react/templates/apps/layout/src/pages/Home/index.tsx +148 -4
- package/generators/micro-react/templates/apps/layout/src/requestErrorConfig.ts +2 -1
- package/generators/micro-react/templates/apps/layout/src/services/user.ts +79 -21
- package/generators/micro-react/templates/apps/layout/typings.d.ts +16 -0
- package/generators/micro-react/templates/docs/package-shared.md +189 -0
- package/generators/micro-react/templates/package.json +1 -1
- package/generators/micro-react/templates/packages/common-intl/README.md +78 -368
- package/generators/micro-react/templates/packages/common-intl/package.json +3 -13
- package/generators/micro-react/templates/packages/common-intl/src/index.ts +5 -6
- package/generators/micro-react/templates/packages/common-intl/src/intl.ts +115 -28
- package/generators/micro-react/templates/packages/common-intl/src/umiLocaleBridge.ts +101 -0
- package/generators/micro-react/templates/packages/common-intl/tsconfig.json +2 -4
- package/generators/micro-react/templates/packages/shared/README.md +120 -0
- package/generators/micro-react/templates/packages/shared/package.json +26 -0
- package/generators/micro-react/templates/packages/shared/services/common/index.ts +43 -0
- package/generators/micro-react/templates/packages/shared/services/index.ts +21 -0
- package/generators/micro-react/templates/packages/shared/services/request.ts +43 -0
- package/generators/micro-react/templates/packages/shared/timezone/index.ts +228 -0
- package/generators/micro-react/templates/packages/shared/tsconfig.json +20 -0
- package/generators/micro-react/templates/scripts/apply-sentry-plugin.ts +6 -1
- package/generators/micro-react/templates/turbo.json +9 -1
- package/generators/subapp-react/index.js +28 -22
- package/generators/subapp-react/templates/homepage/README.md +1 -0
- package/generators/subapp-react/templates/homepage/config/config.prod.development.ts +1 -0
- package/generators/subapp-react/templates/homepage/config/config.prod.testing.ts +1 -0
- package/generators/subapp-react/templates/homepage/config/config.prod.ts +1 -0
- package/generators/subapp-react/templates/homepage/config/config.ts +10 -0
- package/generators/subapp-react/templates/homepage/docs/feature-PermissionFilter/346/214/211/351/222/256/346/235/203/351/231/220.md +35 -0
- package/generators/subapp-react/templates/homepage/docs/feature-/345/233/275/351/231/205/345/214/226.md +124 -0
- package/generators/subapp-react/templates/homepage/package.json +3 -1
- package/generators/subapp-react/templates/homepage/src/app.tsx +104 -2
- package/generators/subapp-react/templates/homepage/src/common/intl/index.ts +15 -0
- package/generators/subapp-react/templates/homepage/src/common/intl/intlRuntime.ts +14 -0
- package/generators/subapp-react/templates/homepage/src/common/intl/localeMapping.ts +24 -0
- package/generators/subapp-react/templates/homepage/src/common/intl/subappIntlConfig.ts +28 -0
- package/generators/subapp-react/templates/homepage/src/common/intl/subappLocale.ts +18 -0
- package/generators/subapp-react/templates/homepage/src/common/intl/subappOwnIntl.ts +63 -0
- package/generators/subapp-react/templates/homepage/src/common/intl/types.ts +14 -0
- package/generators/subapp-react/templates/homepage/src/common/intl/useSubappIntl.ts +61 -0
- package/generators/subapp-react/templates/homepage/src/common/locale.ts +80 -0
- package/generators/subapp-react/templates/homepage/src/common/mainApp.ts +41 -2
- package/generators/subapp-react/templates/homepage/src/components/PermissionFilter/index.tsx +48 -0
- package/generators/subapp-react/templates/homepage/src/locales/en-US.ts +6 -0
- package/generators/subapp-react/templates/homepage/src/locales/zh-CN.ts +6 -0
- package/generators/subapp-react/templates/homepage/src/pages/index.less +10 -0
- package/generators/subapp-react/templates/homepage/src/pages/index.tsx +86 -1
- package/generators/subapp-react/templates/homepage/typings.d.ts +12 -0
- package/lib/utils.js +0 -1
- package/package.json +2 -2
- package/generators/micro-react/templates/apps/layout/docs/common-intl.md +0 -372
- package/generators/micro-react/templates/packages/common-intl/src/indexedDBUtils.ts +0 -51
- package/generators/micro-react/templates/packages/common-intl/src/utils.ts +0 -482
- package/generators/micro-react/templates/packages/common-intl/vite.config.ts +0 -25
|
@@ -10,13 +10,9 @@ export interface PageConfig {
|
|
|
10
10
|
id: number;
|
|
11
11
|
/** 页面名称 */
|
|
12
12
|
name: string;
|
|
13
|
-
/** 英文名称(用于英文环境权限匹配) */
|
|
14
|
-
nameEn?: string;
|
|
15
|
-
/** 菜单唯一标识符(用于权限匹配的兜底) */
|
|
16
|
-
nameKey?: string;
|
|
17
13
|
/** 路由路径 */
|
|
18
14
|
route: string;
|
|
19
|
-
/**
|
|
15
|
+
/** 路由前缀路径(微应用在主应用中的挂载前缀,可与 route 不同) */
|
|
20
16
|
base: string;
|
|
21
17
|
/** 是否启用 */
|
|
22
18
|
enabled: boolean;
|
|
@@ -34,7 +30,7 @@ export interface PageConfig {
|
|
|
34
30
|
adminOnly?: boolean;
|
|
35
31
|
/** 是否开启权限控制 */
|
|
36
32
|
accessControlEnabled: boolean;
|
|
37
|
-
/**
|
|
33
|
+
/** 路由权限标识(与用户信息 `menu_perms`、菜单项 code 一致) */
|
|
38
34
|
routeKey: string | null;
|
|
39
35
|
/** 关联的主文档 ID */
|
|
40
36
|
mainDocumentId: number;
|
|
@@ -219,6 +215,33 @@ declare global {
|
|
|
219
215
|
disableAuth?: boolean;
|
|
220
216
|
/** 获取时区列表的 API 地址 */
|
|
221
217
|
timezoneListUrl?: string;
|
|
218
|
+
/**
|
|
219
|
+
* 多语言中台 initIntl 参数(优先于下方 commonIntl;与 packages/common-intl/src/intl.ts 合并默认项)
|
|
220
|
+
*/
|
|
221
|
+
intl?: {
|
|
222
|
+
tag: string;
|
|
223
|
+
app_name: string;
|
|
224
|
+
indexedDBParams?: {
|
|
225
|
+
dbName?: string;
|
|
226
|
+
dbVersion?: number;
|
|
227
|
+
storeName?: string;
|
|
228
|
+
keyPathKey?: string;
|
|
229
|
+
};
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* 多语言中台:配置有效 `tag`、`app_name` 时主应用首屏拉取中台文案(见 packages/common-intl/README.md)
|
|
233
|
+
* @deprecated 优先使用 `intl`;仍保留以兼容旧 head 注入
|
|
234
|
+
*/
|
|
235
|
+
commonIntl?: {
|
|
236
|
+
tag: string;
|
|
237
|
+
app_name: string;
|
|
238
|
+
indexedDBParams?: {
|
|
239
|
+
dbName?: string;
|
|
240
|
+
dbVersion?: number;
|
|
241
|
+
storeName?: string;
|
|
242
|
+
keyPathKey?: string;
|
|
243
|
+
};
|
|
244
|
+
};
|
|
222
245
|
[key: string]: unknown;
|
|
223
246
|
};
|
|
224
247
|
__MICO_WORKSPACE__?: WorkspaceConfig | null;
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { ITenantItem } from '@/common/auth/type';
|
|
2
|
+
import type { SupportedLocale } from '@/common/locale';
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* 微前端 Props 类型定义
|
|
3
6
|
* 主应用通过 qiankun 传递给子应用的配置数据
|
|
@@ -8,6 +11,7 @@
|
|
|
8
11
|
* - 1: 鉴权失败(token 无效/过期),需要主应用重新获取 token
|
|
9
12
|
* - 2: 业务错误(参数错误、逻辑拒绝、网络超时、权限不足等)
|
|
10
13
|
*/
|
|
14
|
+
|
|
11
15
|
export enum EMicroAppErrorStatus {
|
|
12
16
|
/** 鉴权失败,需重获 token */
|
|
13
17
|
AUTH_FAILURE = 1,
|
|
@@ -163,6 +167,25 @@ export interface IMicroAppProps {
|
|
|
163
167
|
* 子应用可以直接使用这些服务,无需重复初始化
|
|
164
168
|
*/
|
|
165
169
|
services?: ISharedServices;
|
|
170
|
+
/** 主应用标识(固定为当前主应用名) */
|
|
171
|
+
mainApp?: string;
|
|
172
|
+
/**
|
|
173
|
+
* 主应用的 request 实例(umi-request/axios 封装)
|
|
174
|
+
* 子应用可直接使用,复用主应用的拦截器与鉴权信息
|
|
175
|
+
*/
|
|
176
|
+
request?: (...args: any[]) => Promise<any>;
|
|
177
|
+
/** 当前多语言类型(zh-CN / en-US) */
|
|
178
|
+
locale?: SupportedLocale;
|
|
179
|
+
/** 按钮级权限列表(与 currentUser.button_perms 一致,便于子应用直接消费) */
|
|
180
|
+
button_perms?: string[];
|
|
181
|
+
/** 菜单级权限列表 */
|
|
182
|
+
menu_perms?: string[];
|
|
183
|
+
/** 区域级权限列表 */
|
|
184
|
+
region_perms?: string[];
|
|
185
|
+
/** 当前租户 code(多租户模式下由主应用控制,单租户时为默认租户 code) */
|
|
186
|
+
current_tenant?: string;
|
|
187
|
+
/** 当前用户有权限的租户列表(已按 app_perms 过滤) */
|
|
188
|
+
allowed_tenants?: ITenantItem[];
|
|
166
189
|
/** 其他扩展字段 */
|
|
167
190
|
[key: string]: unknown;
|
|
168
191
|
}
|
|
@@ -7,6 +7,37 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { MenuItem, PublicPageItem } from './menu/types';
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* 根据路径查找对应的页面配置(与路由侧 `findRouteByPath` 规则一致:精确匹配 + 最长前缀通配 `/*`)
|
|
12
|
+
*/
|
|
13
|
+
export const findPageByPath = (
|
|
14
|
+
pages: PublicPageItem[],
|
|
15
|
+
pathname: string,
|
|
16
|
+
): PublicPageItem | undefined => {
|
|
17
|
+
let exact: PublicPageItem | undefined;
|
|
18
|
+
let bestWildcard: { page: PublicPageItem; basePath: string } | undefined;
|
|
19
|
+
|
|
20
|
+
for (const page of pages) {
|
|
21
|
+
if (!page.enabled) continue;
|
|
22
|
+
|
|
23
|
+
if (page.route === pathname) {
|
|
24
|
+
exact = page;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (page.route.endsWith('/*')) {
|
|
29
|
+
const basePath = page.route.slice(0, -2);
|
|
30
|
+
if (pathname === basePath || pathname.startsWith(`${basePath}/`)) {
|
|
31
|
+
if (!bestWildcard || basePath.length > bestWildcard.basePath.length) {
|
|
32
|
+
bestWildcard = { page, basePath };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return exact || bestWildcard?.page;
|
|
39
|
+
};
|
|
40
|
+
|
|
10
41
|
/** 获取页面列表 (window.__MICO_PAGES__) */
|
|
11
42
|
export const getPages = (): PublicPageItem[] => {
|
|
12
43
|
if (typeof window === 'undefined') return [];
|
|
@@ -39,7 +70,20 @@ export const getPageById = (pageId: number): PublicPageItem | undefined => {
|
|
|
39
70
|
return getPageIdIndex().get(pageId);
|
|
40
71
|
};
|
|
41
72
|
|
|
42
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* 获取菜单项关联的页面
|
|
75
|
+
* - 优先按 `pageId` 在页面列表中查找
|
|
76
|
+
* - 无匹配时再用菜单跳转路径 `path` 与页面 `route` 匹配(同 `findPageByPath`)
|
|
77
|
+
*/
|
|
43
78
|
export const getMenuPage = (item: MenuItem): PublicPageItem | undefined => {
|
|
44
|
-
|
|
79
|
+
if (item.type !== 'page') return undefined;
|
|
80
|
+
if (item.pageId) {
|
|
81
|
+
const byId = getPageById(item.pageId);
|
|
82
|
+
if (byId) return byId;
|
|
83
|
+
}
|
|
84
|
+
const path = item.path;
|
|
85
|
+
if (path) {
|
|
86
|
+
return findPageByPath(getPages(), path);
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
45
89
|
};
|
|
@@ -9,6 +9,9 @@ import {
|
|
|
9
9
|
} from '@/common/auth/auth-manager';
|
|
10
10
|
import { isPageAuthFree } from '@/common/menu';
|
|
11
11
|
import { isNoAuthRoute, ROUTES } from '@/constants';
|
|
12
|
+
import { captureError } from '@common-web/sentry';
|
|
13
|
+
import { formatLayoutMessage } from '@/common/intl';
|
|
14
|
+
import { Modal } from '@mico-platform/ui';
|
|
12
15
|
import {
|
|
13
16
|
getTicketParam,
|
|
14
17
|
resolveAuthToken,
|
|
@@ -26,6 +29,30 @@ import { removeParamFromUrl } from './url-resolver';
|
|
|
26
29
|
|
|
27
30
|
let ticketPromise: Promise<void> | null = null;
|
|
28
31
|
|
|
32
|
+
/** SSO 换取 token 的时间戳(毫秒),null 表示本次会话未经过 SSO 换取 */
|
|
33
|
+
let tokenAcquiredAt: number | null = null;
|
|
34
|
+
|
|
35
|
+
/** 登录失败弹窗是否已显示,防止多个请求同时失败时重复弹出 */
|
|
36
|
+
let redirectLimitModalShown = false;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 执行 SSO 重定向跳转,回跳地址中固定携带 redirect_count=1
|
|
40
|
+
*/
|
|
41
|
+
const performSsoRedirect = (): void => {
|
|
42
|
+
const externalLoginPath = resolveExternalLoginPath();
|
|
43
|
+
console.log('[SSO] 执行 SSO 重定向', { externalLoginPath });
|
|
44
|
+
|
|
45
|
+
const redirectUrl = new URL(window.location.href);
|
|
46
|
+
redirectUrl.searchParams.delete('redirect_count');
|
|
47
|
+
redirectUrl.searchParams.set('redirect_count', '1');
|
|
48
|
+
redirectUrl.searchParams.delete('ticket');
|
|
49
|
+
const serviceUrl = redirectUrl.toString();
|
|
50
|
+
|
|
51
|
+
window.location.href = `${
|
|
52
|
+
externalLoginPath ?? ROUTES.LOGIN
|
|
53
|
+
}?service=${encodeURIComponent(serviceUrl)}`;
|
|
54
|
+
};
|
|
55
|
+
|
|
29
56
|
/**
|
|
30
57
|
* 处理认证失败后的重定向
|
|
31
58
|
*/
|
|
@@ -50,23 +77,54 @@ export const handleAuthFailureRedirect = (): void => {
|
|
|
50
77
|
? parseInt(redirectCountParam, 10)
|
|
51
78
|
: 0;
|
|
52
79
|
|
|
53
|
-
|
|
80
|
+
console.log('[SSO] handleAuthFailureRedirect', { redirectCount });
|
|
81
|
+
|
|
82
|
+
// 如果 redirect 次数小于 1 次,则执行自动 redirect 跳转登录
|
|
54
83
|
if (redirectCount < 1) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
84
|
+
performSsoRedirect();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// redirect 次数已达上限,停止自动重定向,记录日志并弹窗询问用户是否手动重试
|
|
89
|
+
console.warn('[SSO] 认证失败,已达到最大重定向次数,停止重定向', {
|
|
90
|
+
redirectCount,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const tokenAge =
|
|
94
|
+
tokenAcquiredAt !== null
|
|
95
|
+
? Math.round((Date.now() - tokenAcquiredAt) / 1000)
|
|
96
|
+
: null;
|
|
97
|
+
|
|
98
|
+
captureError(new Error('认证失败:redirect_count 已达上限,停止重定向'), {
|
|
99
|
+
tags: { scene: 'auth_redirect_limit' },
|
|
100
|
+
extra: {
|
|
101
|
+
tokenAcquiredAt: tokenAcquiredAt
|
|
102
|
+
? new Date(tokenAcquiredAt).toISOString()
|
|
103
|
+
: null,
|
|
104
|
+
tokenAgeSeconds: tokenAge,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (redirectLimitModalShown) {
|
|
109
|
+
console.log('[SSO] 登录失败弹窗已显示,跳过重复弹出');
|
|
110
|
+
return;
|
|
69
111
|
}
|
|
112
|
+
|
|
113
|
+
redirectLimitModalShown = true;
|
|
114
|
+
Modal.confirm({
|
|
115
|
+
title: formatLayoutMessage({ id: 'sso_auth_failure_modal_title', defaultMessage: '登录提示' }),
|
|
116
|
+
content: formatLayoutMessage({ id: 'sso_auth_failure_modal_content', defaultMessage: '自动登录失败,是否重新尝试登录?' }),
|
|
117
|
+
okText: formatLayoutMessage({ id: 'sso_auth_failure_modal_ok', defaultMessage: '重新登录' }),
|
|
118
|
+
cancelText: formatLayoutMessage({ id: 'sso_auth_failure_modal_cancel', defaultMessage: '取消' }),
|
|
119
|
+
onOk: () => {
|
|
120
|
+
console.log('[SSO] 用户确认重新登录,执行手动重试重定向');
|
|
121
|
+
redirectLimitModalShown = false;
|
|
122
|
+
performSsoRedirect();
|
|
123
|
+
},
|
|
124
|
+
onCancel: () => {
|
|
125
|
+
redirectLimitModalShown = false;
|
|
126
|
+
},
|
|
127
|
+
});
|
|
70
128
|
};
|
|
71
129
|
|
|
72
130
|
/**
|
|
@@ -113,6 +171,7 @@ export const ensureSsoSession = async (): Promise<void> => {
|
|
|
113
171
|
setFetchingToken(false);
|
|
114
172
|
|
|
115
173
|
if (resolveAuthToken()) {
|
|
174
|
+
tokenAcquiredAt = Date.now();
|
|
116
175
|
processPendingRequests();
|
|
117
176
|
} else {
|
|
118
177
|
throw new Error('SSO 认证失败');
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
setStoredRefreshToken,
|
|
11
11
|
} from '@/common/auth/auth-manager';
|
|
12
12
|
import { resolveRefreshEndpoint } from './config';
|
|
13
|
+
import { buildDefaultHeaders } from './interceptors';
|
|
13
14
|
import type { PendingRequest, RequestContext } from './types';
|
|
14
15
|
|
|
15
16
|
// 当前是否在请求用ticket换取token
|
|
@@ -82,6 +83,7 @@ export const refreshAuthToken = async (): Promise<string | null> => {
|
|
|
82
83
|
}>(endpoint, {
|
|
83
84
|
method: 'POST',
|
|
84
85
|
data: { refresh: refreshToken },
|
|
86
|
+
headers: buildDefaultHeaders(),
|
|
85
87
|
});
|
|
86
88
|
|
|
87
89
|
maybePersistTokens(result);
|
package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IUserInfo } from '@/common/auth/type';
|
|
1
2
|
import { getAuthInfo } from '@/common/auth/auth-manager';
|
|
2
3
|
import { EEnv, getEnv } from '@/common/env';
|
|
3
4
|
import { getCurrentLocale } from '@/common/locale';
|
|
@@ -55,6 +56,7 @@ const MicroAppLoader: React.FC<MicroAppLoaderProps> = ({
|
|
|
55
56
|
});
|
|
56
57
|
|
|
57
58
|
const { initialState } = useModel('@@initialState');
|
|
59
|
+
const currentUser = initialState?.currentUser;
|
|
58
60
|
const location = useLocation();
|
|
59
61
|
const isAuthReady =
|
|
60
62
|
isAuthDisabled() ||
|
|
@@ -74,16 +76,24 @@ const MicroAppLoader: React.FC<MicroAppLoaderProps> = ({
|
|
|
74
76
|
: 'production';
|
|
75
77
|
}, []);
|
|
76
78
|
|
|
77
|
-
// 构建传递给子应用的 props
|
|
79
|
+
// 构建传递给子应用的 props(用户信息以 initialState.currentUser 为准,token 仍读存储)
|
|
78
80
|
const buildProps = useCallback(() => {
|
|
79
81
|
const authInfo = getAuthInfo();
|
|
80
|
-
|
|
82
|
+
const displayName = currentUser?.user_name || currentUser?.name || '';
|
|
83
|
+
// 用户有权限的租户列表:tenant ∩ app_perms(按 app_perm.key === tenant.code 匹配)
|
|
84
|
+
const allowedTenantCodes = new Set(
|
|
85
|
+
(currentUser?.app_perms ?? []).map((p) => p.key),
|
|
86
|
+
);
|
|
87
|
+
const allowedTenants = (currentUser?.tenant ?? []).filter((t) =>
|
|
88
|
+
allowedTenantCodes.has(t.code),
|
|
89
|
+
);
|
|
90
|
+
const props = {
|
|
81
91
|
mainApp: '<%= projectName %>',
|
|
82
92
|
env,
|
|
83
93
|
authToken: authInfo.token,
|
|
84
|
-
uid: authInfo.uid,
|
|
85
|
-
avatar: authInfo.avatar,
|
|
86
|
-
nickname: authInfo.nickname,
|
|
94
|
+
uid: currentUser ? String(currentUser.id) : authInfo.uid,
|
|
95
|
+
avatar: currentUser?.avatar ?? authInfo.avatar,
|
|
96
|
+
nickname: displayName || authInfo.nickname,
|
|
87
97
|
base: normalizeMicroAppBase(base || '/'),
|
|
88
98
|
// 传递当前路由路径,让子应用进行内部路由切换
|
|
89
99
|
routePath,
|
|
@@ -91,8 +101,24 @@ const MicroAppLoader: React.FC<MicroAppLoaderProps> = ({
|
|
|
91
101
|
request,
|
|
92
102
|
// 传递当前多语言类型给子应用(zh-CN, en-US)
|
|
93
103
|
locale: getCurrentLocale(),
|
|
104
|
+
// 与 fetchUserInfo 一致:子应用用于按钮级权限(如 PermissionFilter)
|
|
105
|
+
button_perms: currentUser?.button_perms ?? [],
|
|
106
|
+
menu_perms: currentUser?.menu_perms ?? [],
|
|
107
|
+
region_perms: currentUser?.region_perms ?? [],
|
|
108
|
+
is_superuser: currentUser?.is_superuser,
|
|
109
|
+
// 将当前用户信息传递给子应用,仅传使用到的字段
|
|
110
|
+
currentUser: {
|
|
111
|
+
tenant: currentUser?.tenant,
|
|
112
|
+
isMultiTenant: currentUser?.isMultiTenant,
|
|
113
|
+
email: currentUser?.email,
|
|
114
|
+
},
|
|
115
|
+
current_tenant: currentUser?.current_tenant,
|
|
116
|
+
allowed_tenants: allowedTenants,
|
|
94
117
|
};
|
|
95
|
-
|
|
118
|
+
const { request: _request, ...loggableProps } = props;
|
|
119
|
+
console.log('[MicroAppLoader] buildProps', { appName, ...loggableProps });
|
|
120
|
+
return props;
|
|
121
|
+
}, [base, env, routePath, currentUser, appName]);
|
|
96
122
|
|
|
97
123
|
// ref 持有最新的 buildProps,避免加载 effect 依赖它导致子应用被重载
|
|
98
124
|
const buildPropsRef = useRef(buildProps);
|
package/generators/micro-react/templates/apps/layout/src/components/PermissionFilter/index.tsx
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { isSuperuserUser } from '@/common/menu/parser';
|
|
2
|
+
import { useModel } from '@umijs/max';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
export interface IPermissionFilterProps {
|
|
6
|
+
/** 按钮/操作权限标识,需在用户 `button_perms` 中 */
|
|
7
|
+
permissionKey: string;
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
/** 无权限时渲染内容;不传则渲染 null */
|
|
10
|
+
fallback?: React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hasButtonPermission(
|
|
14
|
+
permissionKey: string,
|
|
15
|
+
buttonPerms: string[] | undefined,
|
|
16
|
+
isSuperuser?: boolean | number,
|
|
17
|
+
): boolean {
|
|
18
|
+
if (!permissionKey?.trim()) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (isSuperuserUser(isSuperuser)) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return Boolean(buttonPerms?.includes(permissionKey));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 按当前用户 `button_perms`(来自 fetchUserInfo)判断是否渲染子节点。
|
|
29
|
+
* 超级用户(is_superuser)视为拥有全部按钮权限。
|
|
30
|
+
*/
|
|
31
|
+
const PermissionFilter: React.FC<IPermissionFilterProps> = ({
|
|
32
|
+
permissionKey,
|
|
33
|
+
children,
|
|
34
|
+
fallback = null,
|
|
35
|
+
}) => {
|
|
36
|
+
const { initialState } = useModel('@@initialState');
|
|
37
|
+
const currentUser = initialState?.currentUser;
|
|
38
|
+
|
|
39
|
+
const allowed = hasButtonPermission(
|
|
40
|
+
permissionKey,
|
|
41
|
+
currentUser?.button_perms,
|
|
42
|
+
currentUser?.is_superuser,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
if (!allowed) {
|
|
46
|
+
return <>{fallback}</>;
|
|
47
|
+
}
|
|
48
|
+
return <>{children}</>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default PermissionFilter;
|
package/generators/micro-react/templates/apps/layout/src/components/RightContent/AvatarDropdown.tsx
CHANGED
|
@@ -42,8 +42,17 @@ interface IMenuItem {
|
|
|
42
42
|
|
|
43
43
|
// TODO: 临时假数据,后续接入真实登录态
|
|
44
44
|
const MOCK_USER: Partial<IUserInfo> = {
|
|
45
|
+
id: 0,
|
|
46
|
+
name: 'Test User',
|
|
45
47
|
user_name: 'Test User',
|
|
46
48
|
username: 'test@micous.com',
|
|
49
|
+
email: 'test@micous.com',
|
|
50
|
+
avatar: '',
|
|
51
|
+
app_perms: [],
|
|
52
|
+
region_perms: [],
|
|
53
|
+
menu_perms: [],
|
|
54
|
+
button_perms: [],
|
|
55
|
+
is_superuser: true,
|
|
47
56
|
};
|
|
48
57
|
|
|
49
58
|
export const AvatarName: React.FC<{ userName?: string }> = ({ userName }) => {
|
|
@@ -371,7 +380,7 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
|
|
|
371
380
|
</Avatar>
|
|
372
381
|
</div>
|
|
373
382
|
)}
|
|
374
|
-
<AvatarName userName={currentUser?.user_name} />
|
|
383
|
+
<AvatarName userName={currentUser?.user_name || currentUser?.email} />
|
|
375
384
|
<IconFont
|
|
376
385
|
type="webcs-outline_down1"
|
|
377
386
|
className="avatar-dropdown-icon"
|
package/generators/micro-react/templates/apps/layout/src/components/RightContent/TenantDropdown.tsx
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { setStoredTenantCode } from '@/common/auth/tenant';
|
|
2
|
+
import IconFont from '@/components/IconFont';
|
|
3
|
+
import useTenant from '@/hooks/useTenant';
|
|
4
|
+
import { Menu } from '@mico-platform/ui';
|
|
5
|
+
import { useModel } from '@umijs/max';
|
|
6
|
+
import React, { useMemo } from 'react';
|
|
7
|
+
import HeaderDropdown from '../HeaderDropdown';
|
|
8
|
+
import './tenant-dropdown.less';
|
|
9
|
+
|
|
10
|
+
const MenuItem = Menu.Item;
|
|
11
|
+
|
|
12
|
+
export const TenantDropdown: React.FC = () => {
|
|
13
|
+
const { isMultiTenant, tenants } = useTenant();
|
|
14
|
+
const { initialState } = useModel('@@initialState');
|
|
15
|
+
const currentUser = initialState?.currentUser;
|
|
16
|
+
const currentTenantCode = currentUser?.current_tenant;
|
|
17
|
+
|
|
18
|
+
// 仅保留 app_perms 中授权的租户(按 app_perm.key === tenant.code 匹配)
|
|
19
|
+
const allowedTenants = useMemo(() => {
|
|
20
|
+
const allowedCodes = new Set(
|
|
21
|
+
(currentUser?.app_perms ?? []).map((p) => p.key),
|
|
22
|
+
);
|
|
23
|
+
return tenants.filter((t) => allowedCodes.has(t.code));
|
|
24
|
+
}, [tenants, currentUser?.app_perms]);
|
|
25
|
+
|
|
26
|
+
const currentTenantName =
|
|
27
|
+
allowedTenants.find((t) => t.code === currentTenantCode)?.name || '';
|
|
28
|
+
|
|
29
|
+
if (!isMultiTenant || allowedTenants.length === 0) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const onClickTenant = (code: string) => {
|
|
34
|
+
if (!code || code === currentTenantCode) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
setStoredTenantCode(code);
|
|
38
|
+
// 参考语言/时区切换:整页 reload,确保菜单、路由、权限全部重建
|
|
39
|
+
window.location.reload();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<HeaderDropdown
|
|
44
|
+
position="br"
|
|
45
|
+
triggerProps={{ popupAlign: { bottom: [0, 20] } }}
|
|
46
|
+
droplist={
|
|
47
|
+
<Menu className="tenant-dropdown-menu">
|
|
48
|
+
{allowedTenants.map((tenant) => {
|
|
49
|
+
const selected = tenant.code === currentTenantCode;
|
|
50
|
+
return (
|
|
51
|
+
<MenuItem
|
|
52
|
+
key={tenant.code}
|
|
53
|
+
onClick={() => onClickTenant(tenant.code)}
|
|
54
|
+
>
|
|
55
|
+
<span className={selected ? 'selected-label' : ''}>
|
|
56
|
+
{tenant.name}
|
|
57
|
+
</span>
|
|
58
|
+
</MenuItem>
|
|
59
|
+
);
|
|
60
|
+
})}
|
|
61
|
+
</Menu>
|
|
62
|
+
}
|
|
63
|
+
>
|
|
64
|
+
<div className="flex items-center tenant-dropdown-trigger">
|
|
65
|
+
<span className="tenant-name">{currentTenantName}</span>
|
|
66
|
+
<IconFont
|
|
67
|
+
type="webcs-outline_down1"
|
|
68
|
+
className="tenant-dropdown-icon"
|
|
69
|
+
fontSize={12}
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
</HeaderDropdown>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export default TenantDropdown;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
@import '@mico-platform/theme/variables';
|
|
2
|
+
|
|
3
|
+
@tenant-dropdown-width: 140px;
|
|
4
|
+
|
|
5
|
+
.tenant-dropdown-trigger {
|
|
6
|
+
display: inline-flex;
|
|
7
|
+
justify-content: space-between;
|
|
8
|
+
width: @tenant-dropdown-width;
|
|
9
|
+
background-color: @color-fill-2;
|
|
10
|
+
border-radius: @border-radius-button;
|
|
11
|
+
height: 32px;
|
|
12
|
+
padding: 1px 10px;
|
|
13
|
+
color: @color-text-1;
|
|
14
|
+
cursor: pointer;
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.tenant-dropdown-trigger:hover {
|
|
19
|
+
background: @color-fill-3;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.tenant-dropdown-trigger .tenant-name {
|
|
23
|
+
flex: 1;
|
|
24
|
+
min-width: 0;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
text-overflow: ellipsis;
|
|
27
|
+
white-space: nowrap;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.tenant-dropdown-trigger.arco-dropdown-popup-visible .arco-icon-down {
|
|
31
|
+
transform: rotate(180deg);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.tenant-dropdown-menu.arco-dropdown-menu {
|
|
35
|
+
width: @tenant-dropdown-width;
|
|
36
|
+
max-height: 320px;
|
|
37
|
+
overflow-y: auto;
|
|
38
|
+
box-sizing: border-box;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.tenant-dropdown-icon {
|
|
42
|
+
margin-left: 4px;
|
|
43
|
+
transition: transform 0.2s;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.tenant-dropdown-menu .selected-label {
|
|
47
|
+
color: @Brand1-6;
|
|
48
|
+
}
|
|
@@ -22,6 +22,8 @@ interface UseMenuStateReturn {
|
|
|
22
22
|
isCollapsed: boolean,
|
|
23
23
|
type: 'clickTrigger' | 'responsive',
|
|
24
24
|
) => void;
|
|
25
|
+
handleMouseEnter: () => void;
|
|
26
|
+
handleMouseLeave: () => void;
|
|
25
27
|
setOpenKeys: (keys: string[]) => void;
|
|
26
28
|
}
|
|
27
29
|
|
|
@@ -98,6 +100,20 @@ export const useMenuState = ({
|
|
|
98
100
|
[triggerMode],
|
|
99
101
|
);
|
|
100
102
|
|
|
103
|
+
// hover 模式下鼠标进入自动展开
|
|
104
|
+
const handleMouseEnter = useCallback(() => {
|
|
105
|
+
if (triggerMode === ETriggerMode.HOVER_TRIGGER) {
|
|
106
|
+
setCollapsed(false);
|
|
107
|
+
}
|
|
108
|
+
}, [triggerMode]);
|
|
109
|
+
|
|
110
|
+
// hover 模式下鼠标离开自动收起
|
|
111
|
+
const handleMouseLeave = useCallback(() => {
|
|
112
|
+
if (triggerMode === ETriggerMode.HOVER_TRIGGER) {
|
|
113
|
+
setCollapsed(true);
|
|
114
|
+
}
|
|
115
|
+
}, [triggerMode]);
|
|
116
|
+
|
|
101
117
|
return {
|
|
102
118
|
selectedKeys,
|
|
103
119
|
openKeys,
|
|
@@ -105,6 +121,8 @@ export const useMenuState = ({
|
|
|
105
121
|
triggerMode,
|
|
106
122
|
handleClickMenuItem,
|
|
107
123
|
handleCollapsed,
|
|
124
|
+
handleMouseEnter,
|
|
125
|
+
handleMouseLeave,
|
|
108
126
|
setOpenKeys,
|
|
109
127
|
};
|
|
110
128
|
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ETenantModel, type ITenantItem } from '@/common/auth/type';
|
|
2
|
+
import { useModel } from '@umijs/max';
|
|
3
|
+
|
|
4
|
+
export interface IUseTenantResult {
|
|
5
|
+
/** 是否多租户模式 */
|
|
6
|
+
isMultiTenant: boolean;
|
|
7
|
+
/** 当前租户模式原始值(1 单租户,2 多租户) */
|
|
8
|
+
tenantModel: number;
|
|
9
|
+
/** 用户所属租户列表 */
|
|
10
|
+
tenants: ITenantItem[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 租户模式 Hook
|
|
15
|
+
*
|
|
16
|
+
* 从 initialState.currentUser 读取 tenant_model / tenant 字段,
|
|
17
|
+
* 对外暴露语义化的 isMultiTenant 标志和 tenants 列表,供业务组件直接使用。
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const { isMultiTenant, tenants } = useTenant();
|
|
21
|
+
* if (isMultiTenant) {
|
|
22
|
+
* // 渲染租户选择器
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
export function useTenant(): IUseTenantResult {
|
|
26
|
+
const { initialState } = useModel('@@initialState');
|
|
27
|
+
const currentUser = initialState?.currentUser;
|
|
28
|
+
|
|
29
|
+
const tenantModel = currentUser?.tenant_model ?? ETenantModel.Single;
|
|
30
|
+
const tenants = currentUser?.tenant ?? [];
|
|
31
|
+
const isMultiTenant =
|
|
32
|
+
currentUser?.isMultiTenant ?? tenantModel === ETenantModel.Multi;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
isMultiTenant,
|
|
36
|
+
tenantModel,
|
|
37
|
+
tenants,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default useTenant;
|
package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AvatarDropdown } from '@/components/RightContent';
|
|
1
|
+
import { AvatarDropdown, TenantDropdown } from '@/components/RightContent';
|
|
2
2
|
import { DEFAULT_NAME } from '@/constants';
|
|
3
3
|
import { Layout, Space } from '@mico-platform/ui';
|
|
4
4
|
import React from 'react';
|
|
@@ -28,6 +28,9 @@ const LayoutHeader: React.FC = () => {
|
|
|
28
28
|
{theme === 'dark' ? <IconSunFill /> : <IconMoonFill />}
|
|
29
29
|
</div> */}
|
|
30
30
|
|
|
31
|
+
{/* Tenant switcher (仅多租户模式可见) */}
|
|
32
|
+
<TenantDropdown />
|
|
33
|
+
|
|
31
34
|
<span className="split-line" />
|
|
32
35
|
|
|
33
36
|
{/* User info */}
|