generator-mico-cli 0.1.28 → 0.2.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.
- package/generators/micro-react/templates/.cursor/rules/layout-app.mdc +1 -1
- package/generators/micro-react/templates/.cursor/rules/request-auth.mdc +2 -2
- package/generators/micro-react/templates/.cursor/rules/theme-system.mdc +2 -2
- package/generators/micro-react/templates/CLAUDE.md +27 -11
- package/generators/micro-react/templates/README.md +2 -0
- package/generators/micro-react/templates/apps/layout/config/config.dev.ts +11 -4
- package/generators/micro-react/templates/apps/layout/config/config.prod.development.ts +1 -11
- package/generators/micro-react/templates/apps/layout/config/config.prod.testing.ts +0 -7
- package/generators/micro-react/templates/apps/layout/config/routes.ts +10 -0
- 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 +105 -0
- package/generators/micro-react/templates/apps/layout/docs/arch-/350/257/267/346/261/202/346/250/241/345/235/227.md +17 -15
- 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 +234 -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 +432 -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 +175 -0
- package/generators/micro-react/templates/apps/layout/mock/api.mock.ts +15 -15
- package/generators/micro-react/templates/apps/layout/src/app.tsx +43 -28
- package/generators/micro-react/templates/apps/layout/src/common/auth/{cs-auth-manager.ts → auth-manager.ts} +6 -63
- package/generators/micro-react/templates/apps/layout/src/common/auth/index.ts +1 -1
- package/generators/micro-react/templates/apps/layout/src/common/auth/tool.ts +2 -3
- package/generators/micro-react/templates/apps/layout/src/common/auth/type.ts +38 -0
- package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +109 -2
- package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +74 -1
- package/generators/micro-react/templates/apps/layout/src/common/micro/index.ts +0 -8
- package/generators/micro-react/templates/apps/layout/src/common/micro/types.ts +0 -2
- package/generators/micro-react/templates/apps/layout/src/common/request/config.ts +1 -1
- package/generators/micro-react/templates/apps/layout/src/common/request/index.ts +3 -11
- package/generators/micro-react/templates/apps/layout/src/common/request/interceptors.ts +2 -2
- package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +3 -2
- package/generators/micro-react/templates/apps/layout/src/common/request/token-refresh.ts +2 -2
- package/generators/micro-react/templates/apps/layout/src/common/request/url-resolver.ts +32 -48
- package/generators/micro-react/templates/apps/layout/src/components/AppTabs/index.less +1 -1
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +62 -14
- package/generators/micro-react/templates/apps/layout/src/global.less +5 -1
- package/generators/micro-react/templates/apps/layout/src/hooks/useAuth.ts +2 -3
- package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.less +1 -1
- package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.tsx +17 -13
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.less +1 -1
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +11 -3
- package/generators/micro-react/templates/apps/layout/src/layouts/index.less +1 -1
- package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +52 -8
- package/generators/micro-react/templates/apps/layout/src/pages/403/index.tsx +28 -0
- package/generators/micro-react/templates/apps/layout/src/pages/User/Login/index.less +275 -0
- package/generators/micro-react/templates/apps/layout/src/pages/User/Login/index.tsx +142 -0
- package/generators/micro-react/templates/apps/layout/src/services/auth.ts +1 -0
- package/generators/micro-react/templates/apps/layout/src/services/user.ts +25 -0
- package/generators/micro-react/templates/packages/shared-styles/README.md +16 -14
- package/generators/micro-react/templates/packages/shared-styles/arco-design-mobile-override.less +91 -0
- package/generators/micro-react/templates/packages/shared-styles/arco-override.less +41 -0
- package/generators/micro-react/templates/packages/shared-styles/index.d.ts +44 -0
- package/generators/micro-react/templates/packages/shared-styles/index.less +0 -1
- package/generators/micro-react/templates/packages/shared-styles/package.json +6 -3
- package/generators/micro-react/templates/packages/shared-styles/themes/dark/custom-var.less +118 -74
- package/generators/micro-react/templates/packages/shared-styles/themes/normal/custom-var.less +175 -101
- package/generators/micro-react/templates/packages/shared-styles/variables-only.less +357 -225
- package/generators/micro-react/templates/packages/shared-styles/variables.less +290 -201
- package/generators/subapp-react/templates/homepage/config/config.prod.ts +2 -2
- package/generators/subapp-react/templates/homepage/config/config.ts +6 -0
- package/generators/subapp-react/templates/homepage/mock/api.mock.ts +43 -43
- package/generators/subapp-react/templates/homepage/typings.d.ts +76 -0
- package/package.json +1 -1
- package/generators/micro-react/templates/apps/layout/src/styles/arco-override.less +0 -78
- package/generators/micro-react/templates/apps/layout/src/styles/themes/dark/custom-var.less +0 -244
- package/generators/micro-react/templates/apps/layout/src/styles/themes/normal/custom-var.less +0 -195
- package/generators/micro-react/templates/apps/layout/src/styles/variables.less +0 -5
|
@@ -3,14 +3,14 @@ import { prefetchApps } from 'qiankun';
|
|
|
3
3
|
import { errorConfig } from './requestErrorConfig';
|
|
4
4
|
// 解决「React19 中无法使用 Message/Notification」的问题。 @see https://github.com/arco-design/arco-design/issues/2900#issuecomment-2796571653
|
|
5
5
|
import * as arco from '@arco-design/web-react';
|
|
6
|
-
import '@arco-design/web-react/dist/css/arco.css';
|
|
7
|
-
import '@arco-design/web-react/es/_util/react-19-adapter';
|
|
8
6
|
import React from 'react';
|
|
9
7
|
import ReactDOM from 'react-dom/client';
|
|
10
8
|
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
9
|
+
import { getStoredAuthToken } from './common/auth/auth-manager';
|
|
10
|
+
import type { IUserInfo } from './common/auth/type';
|
|
11
|
+
import { fetchUserInfo } from './services/user';
|
|
13
12
|
import { extractRoutes, getWindowMenus } from './common/menu';
|
|
13
|
+
import { ensureSsoSession } from './common/request/sso';
|
|
14
14
|
import {
|
|
15
15
|
clearMicroAppProps,
|
|
16
16
|
type IMicroAppProps,
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from './common/micro';
|
|
19
19
|
import { initTheme } from './common/theme';
|
|
20
20
|
import MicroAppLoader from './components/MicroAppLoader';
|
|
21
|
+
import { NO_AUTH_ROUTE_LIST } from './constants';
|
|
21
22
|
import './global.less';
|
|
22
23
|
|
|
23
24
|
// ==================== 微应用预加载 ====================
|
|
@@ -104,38 +105,48 @@ initTheme();
|
|
|
104
105
|
* @see https://umijs.org/docs/api/runtime-config#getinitialstate
|
|
105
106
|
*/
|
|
106
107
|
export async function getInitialState(): Promise<{
|
|
107
|
-
// FIXME: 待修改类型
|
|
108
108
|
settings?: any;
|
|
109
|
-
currentUser?:
|
|
109
|
+
currentUser?: IUserInfo;
|
|
110
110
|
loading?: boolean;
|
|
111
|
-
fetchUserInfo?: () => Promise<
|
|
111
|
+
fetchUserInfo?: () => Promise<IUserInfo | null>;
|
|
112
112
|
}> {
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
/**
|
|
114
|
+
* 获取用户信息
|
|
115
|
+
*/
|
|
116
|
+
const fetchUserInfoFn = async (): Promise<IUserInfo | null> => {
|
|
117
|
+
try {
|
|
118
|
+
return await fetchUserInfo();
|
|
119
|
+
} catch (e) {
|
|
120
|
+
console.warn('[App] 获取用户信息失败', e);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
115
125
|
const { location } = history;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const authInfo = getAuthInfo();
|
|
122
|
-
const currentUser = {
|
|
123
|
-
name: authInfo.nickname || '未知用户',
|
|
124
|
-
avatar:
|
|
125
|
-
authInfo.avatar ||
|
|
126
|
-
'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
|
|
127
|
-
userid: authInfo.uid,
|
|
128
|
-
};
|
|
129
|
-
return {
|
|
130
|
-
fetchUserInfo,
|
|
131
|
-
currentUser,
|
|
132
|
-
};
|
|
126
|
+
const isNoAuthRoute = NO_AUTH_ROUTE_LIST.includes(location.pathname);
|
|
127
|
+
|
|
128
|
+
// 非免认证路由:走 SSO 流程
|
|
129
|
+
if (!isNoAuthRoute) {
|
|
130
|
+
await ensureSsoSession();
|
|
133
131
|
}
|
|
132
|
+
|
|
133
|
+
// 有 token 就获取用户信息(无论在哪个页面,支持登录后 refresh 场景)
|
|
134
|
+
if (getStoredAuthToken()) {
|
|
135
|
+
const userInfo = await fetchUserInfoFn();
|
|
136
|
+
if (userInfo) {
|
|
137
|
+
return {
|
|
138
|
+
fetchUserInfo: fetchUserInfoFn,
|
|
139
|
+
currentUser: userInfo,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
134
144
|
return {
|
|
135
|
-
fetchUserInfo,
|
|
145
|
+
fetchUserInfo: fetchUserInfoFn,
|
|
136
146
|
};
|
|
137
147
|
}
|
|
138
148
|
|
|
149
|
+
|
|
139
150
|
/**
|
|
140
151
|
* @name request 配置,可以配置错误处理
|
|
141
152
|
* 它基于 axios 和 ahooks 的 useRequest 提供了一套统一的网络请求和错误处理方案。
|
|
@@ -168,7 +179,11 @@ export function patchClientRoutes({ routes }: { routes: UmiRoute[] }) {
|
|
|
168
179
|
const menus = getWindowMenus();
|
|
169
180
|
const dynamicRoutes = extractRoutes(menus);
|
|
170
181
|
|
|
171
|
-
console.log('[app.tsx] patchClientRoutes:', {
|
|
182
|
+
console.log('[app.tsx] patchClientRoutes:', {
|
|
183
|
+
menus,
|
|
184
|
+
dynamicRoutes,
|
|
185
|
+
routes,
|
|
186
|
+
});
|
|
172
187
|
|
|
173
188
|
// 找到根路由(全局布局)
|
|
174
189
|
const rootRoute = routes.find((r) => r.path === '/');
|
|
@@ -16,20 +16,16 @@ export interface IAuthInfo {
|
|
|
16
16
|
nickname: string;
|
|
17
17
|
uid: string;
|
|
18
18
|
token: string;
|
|
19
|
-
wsToken: string;
|
|
20
19
|
refreshToken: string;
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
/**
|
|
24
|
-
*
|
|
23
|
+
* 用户信息类型(yufu_login 接口返回)
|
|
25
24
|
*/
|
|
26
|
-
interface
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
avatar?: string;
|
|
31
|
-
nickname?: string;
|
|
32
|
-
};
|
|
25
|
+
interface YufuUserInfo {
|
|
26
|
+
user_name?: string;
|
|
27
|
+
email?: string;
|
|
28
|
+
avatar?: string;
|
|
33
29
|
}
|
|
34
30
|
|
|
35
31
|
/**
|
|
@@ -38,14 +34,13 @@ interface MibotTokenInfo {
|
|
|
38
34
|
interface AuthResponseData {
|
|
39
35
|
access?: string;
|
|
40
36
|
refresh?: string;
|
|
41
|
-
|
|
37
|
+
user_info?: YufuUserInfo;
|
|
42
38
|
}
|
|
43
39
|
|
|
44
40
|
interface AuthResponse {
|
|
45
41
|
code?: number;
|
|
46
42
|
data?: AuthResponseData;
|
|
47
43
|
refresh?: string;
|
|
48
|
-
mibot_token_info?: MibotTokenInfo;
|
|
49
44
|
}
|
|
50
45
|
|
|
51
46
|
/**
|
|
@@ -64,14 +59,6 @@ export const getStoredRefreshToken = (): string | undefined => {
|
|
|
64
59
|
return getFromStorage(REFRESH_TOKEN_KEY) ?? undefined;
|
|
65
60
|
};
|
|
66
61
|
|
|
67
|
-
/**
|
|
68
|
-
* 获取存储的 WebSocket token
|
|
69
|
-
* @returns WebSocket token,如果不存在则返回 undefined
|
|
70
|
-
*/
|
|
71
|
-
export const getStoredWsToken = (): string | undefined => {
|
|
72
|
-
return getFromStorage(WS_TOKEN_KEY) ?? undefined;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
62
|
/**
|
|
76
63
|
* 设置存储的认证 token
|
|
77
64
|
* @param token - 认证 token,如果为 null 或 undefined 则删除
|
|
@@ -96,18 +83,6 @@ export const setStoredRefreshToken = (token?: string | null): void => {
|
|
|
96
83
|
setStorage(REFRESH_TOKEN_KEY, token);
|
|
97
84
|
};
|
|
98
85
|
|
|
99
|
-
/**
|
|
100
|
-
* 设置存储的 WebSocket token
|
|
101
|
-
* @param token - WebSocket token,如果为 null 或 undefined 则删除
|
|
102
|
-
*/
|
|
103
|
-
export const setStoredWsToken = (token?: string | null): void => {
|
|
104
|
-
if (!token) {
|
|
105
|
-
removeStorage(WS_TOKEN_KEY);
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
setStorage(WS_TOKEN_KEY, token);
|
|
109
|
-
};
|
|
110
|
-
|
|
111
86
|
/**
|
|
112
87
|
* 清除所有存储的认证相关 token 和信息
|
|
113
88
|
*/
|
|
@@ -130,7 +105,6 @@ export const getAuthInfo = (): IAuthInfo => {
|
|
|
130
105
|
nickname: getFromStorage(NICKNAME) || '',
|
|
131
106
|
uid: getFromStorage(UID) || '',
|
|
132
107
|
token: getFromStorage(AUTH_TOKEN_KEY) || '',
|
|
133
|
-
wsToken: getFromStorage(WS_TOKEN_KEY) || '',
|
|
134
108
|
refreshToken: getFromStorage(REFRESH_TOKEN_KEY) || '',
|
|
135
109
|
};
|
|
136
110
|
};
|
|
@@ -152,32 +126,11 @@ export const setAuthInfo = (info: IAuthInfo): void => {
|
|
|
152
126
|
if (info.token) {
|
|
153
127
|
setStorage(AUTH_TOKEN_KEY, info.token);
|
|
154
128
|
}
|
|
155
|
-
if (info.wsToken) {
|
|
156
|
-
setStorage(WS_TOKEN_KEY, info.wsToken);
|
|
157
|
-
}
|
|
158
129
|
if (info.refreshToken) {
|
|
159
130
|
setStorage(REFRESH_TOKEN_KEY, info.refreshToken);
|
|
160
131
|
}
|
|
161
132
|
};
|
|
162
133
|
|
|
163
|
-
/**
|
|
164
|
-
* 持久化 Mibot Token 信息(提取的公共函数)
|
|
165
|
-
*/
|
|
166
|
-
const persistMibotTokenInfo = (mibotTokenInfo: MibotTokenInfo): void => {
|
|
167
|
-
if (mibotTokenInfo.token) {
|
|
168
|
-
setStoredWsToken(mibotTokenInfo.token);
|
|
169
|
-
}
|
|
170
|
-
if (mibotTokenInfo.staff_uid) {
|
|
171
|
-
setStorage(UID, mibotTokenInfo.staff_uid);
|
|
172
|
-
}
|
|
173
|
-
if (mibotTokenInfo.staff_prop?.avatar) {
|
|
174
|
-
setStorage(AVATAR, mibotTokenInfo.staff_prop.avatar);
|
|
175
|
-
}
|
|
176
|
-
if (mibotTokenInfo.staff_prop?.nickname) {
|
|
177
|
-
setStorage(NICKNAME, mibotTokenInfo.staff_prop.nickname);
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
|
|
181
134
|
/**
|
|
182
135
|
* 从响应数据中持久化 token 信息
|
|
183
136
|
* 支持多种响应格式:
|
|
@@ -194,13 +147,6 @@ export const maybePersistTokens = (payload: AuthResponse | null): void => {
|
|
|
194
147
|
if (refresh) {
|
|
195
148
|
setStoredRefreshToken(refresh);
|
|
196
149
|
}
|
|
197
|
-
|
|
198
|
-
const mibotTokenInfo =
|
|
199
|
-
payload.mibot_token_info ?? payload.data?.mibot_token_info;
|
|
200
|
-
if (mibotTokenInfo) {
|
|
201
|
-
console.info('设置用户TOKEN信息', mibotTokenInfo);
|
|
202
|
-
persistMibotTokenInfo(mibotTokenInfo);
|
|
203
|
-
}
|
|
204
150
|
}
|
|
205
151
|
|
|
206
152
|
// 处理 code === 200 格式的响应(登录接口)
|
|
@@ -213,8 +159,5 @@ export const maybePersistTokens = (payload: AuthResponse | null): void => {
|
|
|
213
159
|
if (data.refresh) {
|
|
214
160
|
setStoredRefreshToken(data.refresh);
|
|
215
161
|
}
|
|
216
|
-
if (data.mibot_token_info) {
|
|
217
|
-
persistMibotTokenInfo(data.mibot_token_info);
|
|
218
|
-
}
|
|
219
162
|
}
|
|
220
163
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { authLogger } from '@/common/logger';
|
|
2
2
|
import { checkStaffAuth } from '@/services/auth';
|
|
3
3
|
import { handleAuthFailureRedirect } from '../request';
|
|
4
|
-
import { clearStoredTokens, getAuthInfo } from './
|
|
4
|
+
import { clearStoredTokens, getAuthInfo } from './auth-manager';
|
|
5
5
|
|
|
6
6
|
export function logout(): void {
|
|
7
7
|
clearStoredTokens();
|
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export { getAuthInfo, setAuthInfo, type IAuthInfo } from './cs-auth-manager';
|
|
1
|
+
export { getAuthInfo, setAuthInfo } from './auth-manager';
|
|
2
|
+
export type { IAuthInfo } from './auth-manager';
|
|
@@ -7,3 +7,41 @@ export const UID = 'uid';
|
|
|
7
7
|
|
|
8
8
|
/** 微应用模式下主应用传递的环境标识 */
|
|
9
9
|
export const MICRO_ENV_KEY = 'micro_env';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 权限树节点
|
|
13
|
+
*/
|
|
14
|
+
export interface IPermissionNode {
|
|
15
|
+
id: number;
|
|
16
|
+
name: string;
|
|
17
|
+
sort: number;
|
|
18
|
+
level: number;
|
|
19
|
+
parent: number | null;
|
|
20
|
+
codename: string;
|
|
21
|
+
sub_menu?: IPermissionNode[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 完整用户信息(/api/user/info 接口返回的 data 字段)
|
|
26
|
+
*/
|
|
27
|
+
export interface IUserInfo {
|
|
28
|
+
id: number;
|
|
29
|
+
username: string;
|
|
30
|
+
email: string;
|
|
31
|
+
user_name: string;
|
|
32
|
+
avatar: string;
|
|
33
|
+
phone: string;
|
|
34
|
+
first_name: string;
|
|
35
|
+
last_name: string;
|
|
36
|
+
is_superuser: boolean;
|
|
37
|
+
is_staff: boolean;
|
|
38
|
+
is_active: boolean;
|
|
39
|
+
type: number;
|
|
40
|
+
agency_id: number;
|
|
41
|
+
last_login: string;
|
|
42
|
+
permission_tree: IPermissionNode[];
|
|
43
|
+
miss_permissions: string[];
|
|
44
|
+
side_menus: string[];
|
|
45
|
+
app_permissions: string[];
|
|
46
|
+
groups: string[];
|
|
47
|
+
}
|
|
@@ -4,7 +4,111 @@ import type { MenuItem, ParsedMenuItem, ParsedRoute } from './types';
|
|
|
4
4
|
* 获取 window 上挂载的菜单数据
|
|
5
5
|
*/
|
|
6
6
|
export const getWindowMenus = (): MenuItem[] => {
|
|
7
|
-
|
|
7
|
+
if (typeof window === 'undefined') return [];
|
|
8
|
+
const menus = window.__MICO_MENUS__;
|
|
9
|
+
return Array.isArray(menus) ? menus : [];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export interface MenuFilterOptions {
|
|
13
|
+
/** 是否是超级用户 */
|
|
14
|
+
isSuperuser?: boolean | number;
|
|
15
|
+
/** 允许访问的菜单路径列表(白名单) */
|
|
16
|
+
sideMenus?: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const isSuperuserUser = (value?: boolean | number): boolean => {
|
|
20
|
+
return value === true || value === 1;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const buildMenuPath = (parentPath: string, name: string): string => {
|
|
24
|
+
return parentPath ? `${parentPath}.${name}` : name;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 检查菜单路径是否允许访问(白名单逻辑)
|
|
29
|
+
* - 超级用户可以访问所有菜单
|
|
30
|
+
* - 非超级用户不能访问"权限管理"
|
|
31
|
+
* - 菜单路径在 sideMenus 中,或是 sideMenus 中某项的前缀(父级菜单)
|
|
32
|
+
*/
|
|
33
|
+
const isMenuAllowed = (
|
|
34
|
+
menuPath: string,
|
|
35
|
+
options: MenuFilterOptions,
|
|
36
|
+
): boolean => {
|
|
37
|
+
// 超级用户可以访问所有菜单
|
|
38
|
+
if (isSuperuserUser(options.isSuperuser)) return true;
|
|
39
|
+
|
|
40
|
+
// 非超级用户不能访问"权限管理"
|
|
41
|
+
if (menuPath === '权限管理') return false;
|
|
42
|
+
|
|
43
|
+
const sideMenus = options.sideMenus || [];
|
|
44
|
+
|
|
45
|
+
// 如果没有配置 sideMenus,非超级用户没有任何菜单权限
|
|
46
|
+
if (sideMenus.length === 0) return false;
|
|
47
|
+
|
|
48
|
+
// 检查是否在白名单中(精确匹配或前缀匹配)
|
|
49
|
+
return sideMenus.some((allowedPath) => {
|
|
50
|
+
// 精确匹配:菜单路径完全等于白名单中的路径
|
|
51
|
+
if (menuPath === allowedPath) return true;
|
|
52
|
+
// 前缀匹配:白名单路径以菜单路径开头(说明菜单是父级)
|
|
53
|
+
if (allowedPath.startsWith(menuPath + '.')) return true;
|
|
54
|
+
return false;
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const isRouteAllowed = (
|
|
59
|
+
menuPath: string,
|
|
60
|
+
options: MenuFilterOptions = {},
|
|
61
|
+
): boolean => {
|
|
62
|
+
return isMenuAllowed(menuPath, options);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 根据权限过滤菜单项(白名单逻辑)
|
|
67
|
+
*/
|
|
68
|
+
export const filterMenuItems = (
|
|
69
|
+
items: MenuItem[],
|
|
70
|
+
options: MenuFilterOptions = {},
|
|
71
|
+
parentPath = '',
|
|
72
|
+
): MenuItem[] => {
|
|
73
|
+
return items
|
|
74
|
+
.filter((item) => item.enabled)
|
|
75
|
+
.map((item) => {
|
|
76
|
+
const menuPath = buildMenuPath(parentPath, item.name);
|
|
77
|
+
const isAllowed = isMenuAllowed(menuPath, options);
|
|
78
|
+
|
|
79
|
+
// 递归处理子菜单
|
|
80
|
+
const nextChildren = item.children?.length
|
|
81
|
+
? filterMenuItems(item.children, options, menuPath)
|
|
82
|
+
: [];
|
|
83
|
+
|
|
84
|
+
// 分组类型:如果没有子菜单,不显示
|
|
85
|
+
if (item.type === 'group' && nextChildren.length === 0) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 当前菜单不允许访问
|
|
90
|
+
if (!isAllowed) {
|
|
91
|
+
// 但如果有允许的子菜单,仍需显示当前菜单作为容器
|
|
92
|
+
if (nextChildren.length > 0) {
|
|
93
|
+
return { ...item, children: nextChildren };
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 当前菜单允许访问
|
|
99
|
+
const hasChildren = (item.children?.length || 0) > 0;
|
|
100
|
+
if (!hasChildren) {
|
|
101
|
+
return item;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 有子菜单但过滤后为空,不显示
|
|
105
|
+
if (nextChildren.length === 0) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { ...item, children: nextChildren };
|
|
110
|
+
})
|
|
111
|
+
.filter((item): item is MenuItem => Boolean(item));
|
|
8
112
|
};
|
|
9
113
|
|
|
10
114
|
/**
|
|
@@ -38,10 +142,13 @@ const getEntry = (page: MenuItem['page']): string | undefined => {
|
|
|
38
142
|
export const extractRoutes = (
|
|
39
143
|
items: MenuItem[],
|
|
40
144
|
routes: ParsedRoute[] = [],
|
|
145
|
+
parentPath = '',
|
|
41
146
|
): ParsedRoute[] => {
|
|
42
147
|
for (const item of items) {
|
|
43
148
|
if (!item.enabled) continue;
|
|
44
149
|
|
|
150
|
+
const menuPath = buildMenuPath(parentPath, item.name);
|
|
151
|
+
|
|
45
152
|
if (item.type === 'page' && item.page && item.page.enabled) {
|
|
46
153
|
const loadType = getLoadType(item.page);
|
|
47
154
|
routes.push({
|
|
@@ -55,7 +162,7 @@ export const extractRoutes = (
|
|
|
55
162
|
}
|
|
56
163
|
|
|
57
164
|
if (item.children && item.children.length > 0) {
|
|
58
|
-
extractRoutes(item.children, routes);
|
|
165
|
+
extractRoutes(item.children, routes, menuPath);
|
|
59
166
|
}
|
|
60
167
|
}
|
|
61
168
|
|
|
@@ -1,18 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 页面路由匹配模式
|
|
3
|
+
*/
|
|
4
|
+
export type PageRouteMode = 'prefix' | 'exact';
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* 页面配置 - 从统一中台获取
|
|
3
8
|
*/
|
|
4
9
|
export interface PageConfig {
|
|
5
10
|
id: number;
|
|
11
|
+
/** 页面名称 */
|
|
6
12
|
name: string;
|
|
7
13
|
/** 路由路径 */
|
|
8
14
|
route: string;
|
|
15
|
+
/** 是否启用 */
|
|
9
16
|
enabled: boolean;
|
|
10
17
|
/** 微应用 HTML 入口 URL (用于 qiankun 加载) */
|
|
11
|
-
htmlUrl: string
|
|
18
|
+
htmlUrl: string;
|
|
12
19
|
/** 微应用 JS 入口 URL 列表 */
|
|
13
20
|
jsUrls: string[];
|
|
14
21
|
/** 微应用 CSS URL 列表 */
|
|
15
22
|
cssUrls: string[];
|
|
23
|
+
/** 路由前缀路径 */
|
|
24
|
+
prefixPath: string;
|
|
25
|
+
/** 路由匹配模式 */
|
|
26
|
+
routeMode: PageRouteMode;
|
|
27
|
+
/** 关联的主文档 ID */
|
|
28
|
+
mainDocumentId: number;
|
|
29
|
+
/** 所属工作空间子域名 */
|
|
30
|
+
workspaceSubdomain: string;
|
|
31
|
+
/** 当前版本号 */
|
|
32
|
+
version: string;
|
|
33
|
+
/** 历史版本列表 */
|
|
34
|
+
versions: string[];
|
|
35
|
+
/** 发布者邮箱 */
|
|
36
|
+
publishedBy: string;
|
|
37
|
+
/** 创建时间 */
|
|
38
|
+
createdAt: string;
|
|
39
|
+
/** 更新时间 */
|
|
40
|
+
updatedAt: string;
|
|
16
41
|
}
|
|
17
42
|
|
|
18
43
|
/**
|
|
@@ -77,6 +102,50 @@ export interface MicroAppProps {
|
|
|
77
102
|
request: <T = any>(url: string, options?: any) => Promise<T>;
|
|
78
103
|
}
|
|
79
104
|
|
|
105
|
+
/**
|
|
106
|
+
* 工作空间渲染模式
|
|
107
|
+
*/
|
|
108
|
+
export type WorkspaceRenderMode = 'micro-frontend' | 'default';
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 工作空间状态
|
|
112
|
+
*/
|
|
113
|
+
export type WorkspaceStatus = 'active' | 'inactive' | 'pending';
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 工作空间配置 - 从统一中台获取
|
|
117
|
+
*/
|
|
118
|
+
export interface WorkspaceConfig {
|
|
119
|
+
/** 关联的应用 ID(可能为空) */
|
|
120
|
+
appId: number | null;
|
|
121
|
+
/** 创建时间 */
|
|
122
|
+
createdAt: string;
|
|
123
|
+
/** 创建者 ID */
|
|
124
|
+
createdBy: number;
|
|
125
|
+
/** 描述信息 */
|
|
126
|
+
description: string;
|
|
127
|
+
/** 首页路径 */
|
|
128
|
+
homePath: string;
|
|
129
|
+
/** 最后更新时间 */
|
|
130
|
+
lastUpdate: string;
|
|
131
|
+
/** 工作空间名称 */
|
|
132
|
+
name: string;
|
|
133
|
+
/** 生产环境域名 */
|
|
134
|
+
productionDomain: string;
|
|
135
|
+
/** 渲染模式 */
|
|
136
|
+
renderMode: WorkspaceRenderMode;
|
|
137
|
+
/** 仓库地址 */
|
|
138
|
+
repository: string;
|
|
139
|
+
/** 是否启用 SSO */
|
|
140
|
+
ssoEnabled: boolean;
|
|
141
|
+
/** 状态 */
|
|
142
|
+
status: WorkspaceStatus;
|
|
143
|
+
/** 子域名标识 */
|
|
144
|
+
subdomain: string;
|
|
145
|
+
/** 测试环境域名 */
|
|
146
|
+
testDomain: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
80
149
|
/**
|
|
81
150
|
* Window 上挂载的菜单数据
|
|
82
151
|
*/
|
|
@@ -88,5 +157,9 @@ declare global {
|
|
|
88
157
|
apiBaseUrl?: string;
|
|
89
158
|
[key: string]: unknown;
|
|
90
159
|
};
|
|
160
|
+
__MICO_WORKSPACE__?: WorkspaceConfig | null;
|
|
161
|
+
__MICO_PAGE__?: PageConfig | null;
|
|
162
|
+
/** qiankun 注入的全局变量,表示当前运行在 qiankun 子应用环境中 */
|
|
163
|
+
__POWERED_BY_QIANKUN__?: boolean;
|
|
91
164
|
}
|
|
92
165
|
}
|
|
@@ -32,13 +32,6 @@ export type {
|
|
|
32
32
|
MicroAppErrorSource,
|
|
33
33
|
} from './types';
|
|
34
34
|
|
|
35
|
-
declare global {
|
|
36
|
-
interface Window {
|
|
37
|
-
/** qiankun 注入的全局变量,表示当前运行在 qiankun 子应用环境中 */
|
|
38
|
-
__POWERED_BY_QIANKUN__?: boolean;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
35
|
// ============================================
|
|
43
36
|
// 环境检测
|
|
44
37
|
// ============================================
|
|
@@ -140,7 +133,6 @@ export const setMicroAppProps = (props: IMicroAppProps): void => {
|
|
|
140
133
|
// 1. 存储认证信息到 localStorage(复用现有的 setAuthInfo)
|
|
141
134
|
setAuthInfo({
|
|
142
135
|
token: props.authToken || '',
|
|
143
|
-
wsToken: props.wsToken || '',
|
|
144
136
|
uid: props.uid || '',
|
|
145
137
|
avatar: props.avatar || '',
|
|
146
138
|
nickname: props.nickname || '',
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 请求客户端配置管理
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { getStoredAuthToken } from '../auth/
|
|
5
|
+
import { getStoredAuthToken } from '../auth/auth-manager';
|
|
6
6
|
import type { RequestClientOptions, TokenResolver } from './types';
|
|
7
7
|
|
|
8
8
|
const defaultClientOptions: RequestClientOptions = {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { request as rawRequest } from '@umijs/max';
|
|
18
|
-
import { setStoredAuthToken } from '../auth/
|
|
18
|
+
import { setStoredAuthToken } from '../auth/auth-manager';
|
|
19
19
|
import { ROUTES } from '../constants';
|
|
20
20
|
|
|
21
21
|
// 配置相关
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
} from './interceptors';
|
|
37
37
|
|
|
38
38
|
// SSO 相关
|
|
39
|
-
import {
|
|
39
|
+
import { handleAuthFailureRedirect } from './sso';
|
|
40
40
|
|
|
41
41
|
// Token 刷新相关
|
|
42
42
|
import {
|
|
@@ -57,9 +57,6 @@ import type {
|
|
|
57
57
|
// URL 解析
|
|
58
58
|
import { resolveRequestUrl } from './url-resolver';
|
|
59
59
|
|
|
60
|
-
// 始终使用远程地址的路径
|
|
61
|
-
const ALWAYS_REMOTE_PATHS: string[] = [];
|
|
62
|
-
|
|
63
60
|
// 初始化默认拦截器
|
|
64
61
|
initDefaultInterceptors(isFetchingToken, addToPendingQueue);
|
|
65
62
|
|
|
@@ -77,12 +74,7 @@ export const request = async <T = unknown>(
|
|
|
77
74
|
url: string,
|
|
78
75
|
options?: UnifiedRequestOptions,
|
|
79
76
|
): Promise<T> => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const isAlwaysRemote = ALWAYS_REMOTE_PATHS.some((path) =>
|
|
83
|
-
url.startsWith(path),
|
|
84
|
-
);
|
|
85
|
-
const resolvedUrl = resolveRequestUrl(url, options, isAlwaysRemote);
|
|
77
|
+
const resolvedUrl = resolveRequestUrl(url, options);
|
|
86
78
|
|
|
87
79
|
const ctx: RequestContext = {
|
|
88
80
|
url: resolvedUrl,
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { getFromStorage, safeParseJSON } from '@/common/helpers';
|
|
6
6
|
import { Message } from '@arco-design/web-react';
|
|
7
|
-
import { maybePersistTokens } from '
|
|
8
|
-
import { UID } from '
|
|
7
|
+
import { maybePersistTokens } from '@/common/auth/auth-manager';
|
|
8
|
+
import { UID } from '@/common/auth/type';
|
|
9
9
|
import { getClientOptions, resolveAuthToken } from './config';
|
|
10
10
|
import type {
|
|
11
11
|
RequestContext,
|
|
@@ -6,7 +6,8 @@ import { request as rawRequest } from '@umijs/max';
|
|
|
6
6
|
import {
|
|
7
7
|
maybePersistTokens,
|
|
8
8
|
setStoredAuthToken,
|
|
9
|
-
} from '
|
|
9
|
+
} from '@/common/auth/auth-manager';
|
|
10
|
+
import { ROUTES } from '@/common/constants';
|
|
10
11
|
import {
|
|
11
12
|
getTicketParam,
|
|
12
13
|
resolveAuthToken,
|
|
@@ -49,7 +50,7 @@ export const handleAuthFailureRedirect = (): void => {
|
|
|
49
50
|
const serviceUrl = redirectUrl.toString();
|
|
50
51
|
|
|
51
52
|
window.location.href = `${
|
|
52
|
-
externalLoginPath ??
|
|
53
|
+
externalLoginPath ?? ROUTES.LOGIN
|
|
53
54
|
}?service=${encodeURIComponent(serviceUrl)}`;
|
|
54
55
|
} else {
|
|
55
56
|
console.warn('认证失败,但已达到最大重定向次数,停止重定向');
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
maybePersistTokens,
|
|
9
9
|
setStoredAuthToken,
|
|
10
10
|
setStoredRefreshToken,
|
|
11
|
-
} from '
|
|
11
|
+
} from '@/common/auth/auth-manager';
|
|
12
12
|
import { resolveRefreshEndpoint } from './config';
|
|
13
13
|
import type { PendingRequest, RequestContext } from './types';
|
|
14
14
|
|
|
@@ -130,7 +130,7 @@ export const shouldAttemptRefresh = (error: unknown): boolean => {
|
|
|
130
130
|
if (error && typeof error === 'object' && 'response' in error) {
|
|
131
131
|
const typedError = error as { response?: { status?: number } };
|
|
132
132
|
const status = typedError.response?.status;
|
|
133
|
-
return status === 401
|
|
133
|
+
return status === 401;
|
|
134
134
|
}
|
|
135
135
|
return false;
|
|
136
136
|
};
|