generator-mico-cli 0.1.18
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 +84 -0
- package/bin/mico.js +316 -0
- package/generators/micro-react/ignore-list.json +8 -0
- package/generators/micro-react/index.js +158 -0
- package/generators/micro-react/templates/.commitlintrc.js +6 -0
- package/generators/micro-react/templates/.cursor/rules/always-read-docs.mdc +129 -0
- package/generators/micro-react/templates/.cursor/rules/cicd-deploy.mdc +143 -0
- package/generators/micro-react/templates/.cursor/rules/coding-conventions.mdc +206 -0
- package/generators/micro-react/templates/.cursor/rules/commit-conventions.mdc +111 -0
- package/generators/micro-react/templates/.cursor/rules/development-guide.mdc +295 -0
- package/generators/micro-react/templates/.cursor/rules/layout-app.mdc +275 -0
- package/generators/micro-react/templates/.cursor/rules/micro-frontend.mdc +196 -0
- package/generators/micro-react/templates/.cursor/rules/project-overview.mdc +128 -0
- package/generators/micro-react/templates/.cursor/rules/request-auth.mdc +220 -0
- package/generators/micro-react/templates/.cursor/rules/theme-system.mdc +206 -0
- package/generators/micro-react/templates/.editorconfig +16 -0
- package/generators/micro-react/templates/.env +3 -0
- package/generators/micro-react/templates/.eslintrc.js +30 -0
- package/generators/micro-react/templates/.husky/commit-msg +2 -0
- package/generators/micro-react/templates/.husky/pre-commit +2 -0
- package/generators/micro-react/templates/.lintstagedrc +5 -0
- package/generators/micro-react/templates/.stylelintrc.js +25 -0
- package/generators/micro-react/templates/AGENTS.md +39 -0
- package/generators/micro-react/templates/CICD/start_dev.sh +30 -0
- package/generators/micro-react/templates/CICD/start_local.sh +30 -0
- package/generators/micro-react/templates/CICD/start_prod.sh +30 -0
- package/generators/micro-react/templates/CICD/start_test.sh +30 -0
- package/generators/micro-react/templates/CICD/wangsu_fresh_dev.sh +19 -0
- package/generators/micro-react/templates/CICD/wangsu_fresh_prod.sh +19 -0
- package/generators/micro-react/templates/CICD/wangsu_fresh_test.sh +19 -0
- package/generators/micro-react/templates/CLAUDE.md +106 -0
- package/generators/micro-react/templates/README.md +84 -0
- package/generators/micro-react/templates/_gitignore +57 -0
- package/generators/micro-react/templates/_npmrc +2 -0
- package/generators/micro-react/templates/apps/layout/.env +4 -0
- package/generators/micro-react/templates/apps/layout/.eslintrc.js +10 -0
- package/generators/micro-react/templates/apps/layout/.lintstagedrc +17 -0
- package/generators/micro-react/templates/apps/layout/.prettierignore +3 -0
- package/generators/micro-react/templates/apps/layout/.prettierrc +8 -0
- package/generators/micro-react/templates/apps/layout/.stylelintrc.js +20 -0
- package/generators/micro-react/templates/apps/layout/README.md +37 -0
- package/generators/micro-react/templates/apps/layout/config/config.dev.ts +54 -0
- package/generators/micro-react/templates/apps/layout/config/config.prod.ts +37 -0
- package/generators/micro-react/templates/apps/layout/config/config.testing.ts +27 -0
- package/generators/micro-react/templates/apps/layout/config/config.ts +132 -0
- package/generators/micro-react/templates/apps/layout/config/routes.ts +13 -0
- package/generators/micro-react/templates/apps/layout/mock/api.mock.ts +78 -0
- package/generators/micro-react/templates/apps/layout/mock/menus.json +100 -0
- package/generators/micro-react/templates/apps/layout/mock/menus.ts +11 -0
- package/generators/micro-react/templates/apps/layout/mock/user.mock.ts +20 -0
- package/generators/micro-react/templates/apps/layout/package.json +45 -0
- package/generators/micro-react/templates/apps/layout/public/font/ar-SA.js +54 -0
- package/generators/micro-react/templates/apps/layout/public/font/default.js +54 -0
- package/generators/micro-react/templates/apps/layout/src/app.tsx +123 -0
- package/generators/micro-react/templates/apps/layout/src/assets/.gitkeep +0 -0
- package/generators/micro-react/templates/apps/layout/src/common/auth/cs-auth-manager.ts +220 -0
- package/generators/micro-react/templates/apps/layout/src/common/auth/index.ts +41 -0
- package/generators/micro-react/templates/apps/layout/src/common/auth/tool.ts +3 -0
- package/generators/micro-react/templates/apps/layout/src/common/auth/type.ts +6 -0
- package/generators/micro-react/templates/apps/layout/src/common/constants.ts +38 -0
- package/generators/micro-react/templates/apps/layout/src/common/env.ts +73 -0
- package/generators/micro-react/templates/apps/layout/src/common/helpers.ts +69 -0
- package/generators/micro-react/templates/apps/layout/src/common/locale.ts +123 -0
- package/generators/micro-react/templates/apps/layout/src/common/logger.ts +45 -0
- package/generators/micro-react/templates/apps/layout/src/common/menu/index.ts +2 -0
- package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +143 -0
- package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +92 -0
- package/generators/micro-react/templates/apps/layout/src/common/request/config.ts +73 -0
- package/generators/micro-react/templates/apps/layout/src/common/request/index.ts +188 -0
- package/generators/micro-react/templates/apps/layout/src/common/request/interceptors.ts +186 -0
- package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +132 -0
- package/generators/micro-react/templates/apps/layout/src/common/request/token-refresh.ts +136 -0
- package/generators/micro-react/templates/apps/layout/src/common/request/types.ts +44 -0
- package/generators/micro-react/templates/apps/layout/src/common/request/url-resolver.ts +75 -0
- package/generators/micro-react/templates/apps/layout/src/common/theme.ts +107 -0
- package/generators/micro-react/templates/apps/layout/src/common/types.ts +7 -0
- package/generators/micro-react/templates/apps/layout/src/common/upload/index.ts +2 -0
- package/generators/micro-react/templates/apps/layout/src/common/upload/oss.ts +401 -0
- package/generators/micro-react/templates/apps/layout/src/common/upload/types.ts +47 -0
- package/generators/micro-react/templates/apps/layout/src/common/uploadFiles.ts +35 -0
- package/generators/micro-react/templates/apps/layout/src/components/IconFont/index.tsx +25 -0
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.less +44 -0
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +121 -0
- package/generators/micro-react/templates/apps/layout/src/constants/index.ts +15 -0
- package/generators/micro-react/templates/apps/layout/src/global.less +13 -0
- package/generators/micro-react/templates/apps/layout/src/hooks/index.ts +3 -0
- package/generators/micro-react/templates/apps/layout/src/hooks/useAuth.ts +75 -0
- package/generators/micro-react/templates/apps/layout/src/hooks/useMenu.ts +35 -0
- package/generators/micro-react/templates/apps/layout/src/hooks/useMenuState.ts +112 -0
- package/generators/micro-react/templates/apps/layout/src/hooks/useTheme.ts +124 -0
- package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.less +109 -0
- package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.tsx +97 -0
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.less +164 -0
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +165 -0
- package/generators/micro-react/templates/apps/layout/src/layouts/index.less +71 -0
- package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +91 -0
- package/generators/micro-react/templates/apps/layout/src/locales/en-US.ts +20 -0
- package/generators/micro-react/templates/apps/layout/src/locales/zh-CN.ts +19 -0
- package/generators/micro-react/templates/apps/layout/src/models/global.ts +13 -0
- package/generators/micro-react/templates/apps/layout/src/pages/Home/index.less +3 -0
- package/generators/micro-react/templates/apps/layout/src/pages/Home/index.tsx +7 -0
- package/generators/micro-react/templates/apps/layout/src/requestErrorConfig.ts +171 -0
- package/generators/micro-react/templates/apps/layout/src/services/auth.ts +37 -0
- package/generators/micro-react/templates/apps/layout/src/services/oss.ts +40 -0
- package/generators/micro-react/templates/apps/layout/src/styles/arco-override.less +78 -0
- package/generators/micro-react/templates/apps/layout/src/styles/themes/dark/custom-var.less +244 -0
- package/generators/micro-react/templates/apps/layout/src/styles/themes/normal/custom-var.less +195 -0
- package/generators/micro-react/templates/apps/layout/src/styles/variables.less +5 -0
- package/generators/micro-react/templates/apps/layout/src/utils/format.ts +4 -0
- package/generators/micro-react/templates/apps/layout/tailwind.config.js +7 -0
- package/generators/micro-react/templates/apps/layout/tailwind.css +3 -0
- package/generators/micro-react/templates/apps/layout/tsconfig.json +3 -0
- package/generators/micro-react/templates/apps/layout/typings.d.ts +1 -0
- package/generators/micro-react/templates/deployDesc.md +60 -0
- package/generators/micro-react/templates/docs/commit-message.md +98 -0
- package/generators/micro-react/templates/package.json +35 -0
- package/generators/micro-react/templates/packages/shared-styles/README.md +125 -0
- package/generators/micro-react/templates/packages/shared-styles/arco-override.less +78 -0
- package/generators/micro-react/templates/packages/shared-styles/index.less +14 -0
- package/generators/micro-react/templates/packages/shared-styles/package.json +27 -0
- package/generators/micro-react/templates/packages/shared-styles/theme-inject.less +10 -0
- package/generators/micro-react/templates/packages/shared-styles/themes/dark/custom-var.less +246 -0
- package/generators/micro-react/templates/packages/shared-styles/themes/normal/custom-var.less +195 -0
- package/generators/micro-react/templates/packages/shared-styles/variables-only.less +301 -0
- package/generators/micro-react/templates/packages/shared-styles/variables.less +363 -0
- package/generators/micro-react/templates/pnpm-workspace.yaml +9 -0
- package/generators/micro-react/templates/scripts/collect-dist.js +68 -0
- package/generators/micro-react/templates/scripts/create-umi-app.sh +61 -0
- package/generators/micro-react/templates/scripts/dev.js +133 -0
- package/generators/micro-react/templates/turbo.json +68 -0
- package/generators/subapp-react/ignore-list.json +7 -0
- package/generators/subapp-react/index.js +189 -0
- package/generators/subapp-react/templates/homepage/.env +4 -0
- package/generators/subapp-react/templates/homepage/README.md +116 -0
- package/generators/subapp-react/templates/homepage/_gitignore +9 -0
- package/generators/subapp-react/templates/homepage/config/config.dev.ts +59 -0
- package/generators/subapp-react/templates/homepage/config/config.prod.ts +41 -0
- package/generators/subapp-react/templates/homepage/config/config.testing.ts +40 -0
- package/generators/subapp-react/templates/homepage/config/config.ts +102 -0
- package/generators/subapp-react/templates/homepage/config/routes.ts +7 -0
- package/generators/subapp-react/templates/homepage/mock/api.mock.ts +59 -0
- package/generators/subapp-react/templates/homepage/package.json +30 -0
- package/generators/subapp-react/templates/homepage/src/app.tsx +80 -0
- package/generators/subapp-react/templates/homepage/src/assets/yay.jpg +0 -0
- package/generators/subapp-react/templates/homepage/src/common/logger.ts +42 -0
- package/generators/subapp-react/templates/homepage/src/common/mainApp.ts +53 -0
- package/generators/subapp-react/templates/homepage/src/common/request.ts +49 -0
- package/generators/subapp-react/templates/homepage/src/global.less +26 -0
- package/generators/subapp-react/templates/homepage/src/pages/index.less +139 -0
- package/generators/subapp-react/templates/homepage/src/pages/index.tsx +342 -0
- package/generators/subapp-react/templates/homepage/src/styles/theme.less +6 -0
- package/generators/subapp-react/templates/homepage/tsconfig.json +3 -0
- package/generators/subapp-react/templates/homepage/typings.d.ts +17 -0
- package/lib/utils.js +165 -0
- package/package.json +31 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 请求/响应拦截器管理
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getFromStorage, safeParseJSON } from '@/common/helpers';
|
|
6
|
+
import { Message } from '@arco-design/web-react';
|
|
7
|
+
import { maybePersistTokens } from '../auth/cs-auth-manager';
|
|
8
|
+
import { UID } from '../auth/type';
|
|
9
|
+
import { getClientOptions, resolveAuthToken } from './config';
|
|
10
|
+
import type {
|
|
11
|
+
RequestContext,
|
|
12
|
+
RequestInterceptor,
|
|
13
|
+
ResponseInterceptor,
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
const APP_INFO_KEY = 'appInfo';
|
|
17
|
+
const IS_SUPERUSER_KEY = 'is_superuser';
|
|
18
|
+
const GROUPS_KEY = 'groups';
|
|
19
|
+
|
|
20
|
+
const requestInterceptors: Array<{ id: number; handler: RequestInterceptor }> =
|
|
21
|
+
[];
|
|
22
|
+
const responseInterceptors: Array<{
|
|
23
|
+
id: number;
|
|
24
|
+
handler: ResponseInterceptor;
|
|
25
|
+
}> = [];
|
|
26
|
+
|
|
27
|
+
let interceptorSeed = 0;
|
|
28
|
+
|
|
29
|
+
export const addRequestInterceptor = (
|
|
30
|
+
handler: RequestInterceptor,
|
|
31
|
+
): (() => void) => {
|
|
32
|
+
const id = ++interceptorSeed;
|
|
33
|
+
requestInterceptors.push({ id, handler });
|
|
34
|
+
return () => {
|
|
35
|
+
const index = requestInterceptors.findIndex((item) => item.id === id);
|
|
36
|
+
if (index >= 0) {
|
|
37
|
+
requestInterceptors.splice(index, 1);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const addResponseInterceptor = (
|
|
43
|
+
handler: ResponseInterceptor,
|
|
44
|
+
): (() => void) => {
|
|
45
|
+
const id = ++interceptorSeed;
|
|
46
|
+
responseInterceptors.push({ id, handler });
|
|
47
|
+
return () => {
|
|
48
|
+
const index = responseInterceptors.findIndex((item) => item.id === id);
|
|
49
|
+
if (index >= 0) {
|
|
50
|
+
responseInterceptors.splice(index, 1);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const runRequestInterceptors = async (
|
|
56
|
+
ctx: RequestContext,
|
|
57
|
+
): Promise<RequestContext> => {
|
|
58
|
+
let current = ctx;
|
|
59
|
+
for (const { handler } of requestInterceptors) {
|
|
60
|
+
current = await handler(current);
|
|
61
|
+
}
|
|
62
|
+
return current;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const runResponseInterceptors = async <T>(
|
|
66
|
+
response: T,
|
|
67
|
+
ctx: RequestContext,
|
|
68
|
+
): Promise<T> => {
|
|
69
|
+
let current = response;
|
|
70
|
+
for (const { handler } of responseInterceptors) {
|
|
71
|
+
current = await handler(current, ctx);
|
|
72
|
+
}
|
|
73
|
+
return current;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 构建默认请求头
|
|
78
|
+
*/
|
|
79
|
+
export const buildDefaultHeaders = (): Record<string, string> => {
|
|
80
|
+
const headers: Record<string, string> = {};
|
|
81
|
+
const clientOptions = getClientOptions();
|
|
82
|
+
const appId = clientOptions.appId;
|
|
83
|
+
|
|
84
|
+
if (appId) {
|
|
85
|
+
headers['custom-header-appid'] = appId;
|
|
86
|
+
headers['X-Biz-Code'] = appId;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const appInfo = safeParseJSON<{ name?: string }>(
|
|
90
|
+
getFromStorage(APP_INFO_KEY),
|
|
91
|
+
);
|
|
92
|
+
if (appInfo?.name) {
|
|
93
|
+
headers.app = appInfo.name;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const isSuperuser = getFromStorage(IS_SUPERUSER_KEY);
|
|
97
|
+
if (isSuperuser) {
|
|
98
|
+
headers['is-superuser'] = isSuperuser;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const groups = getFromStorage(GROUPS_KEY);
|
|
102
|
+
if (groups) {
|
|
103
|
+
headers.groups = encodeURIComponent(groups);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const authToken = resolveAuthToken();
|
|
107
|
+
if (authToken) {
|
|
108
|
+
headers.authtoken = authToken;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const uid = getFromStorage(UID);
|
|
112
|
+
if (uid) {
|
|
113
|
+
headers['X-Uid'] = uid;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return headers;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 初始化默认拦截器
|
|
121
|
+
*/
|
|
122
|
+
export const initDefaultInterceptors = (
|
|
123
|
+
isFetchingToken: () => boolean,
|
|
124
|
+
addToPendingQueue: (
|
|
125
|
+
ctx: RequestContext,
|
|
126
|
+
resolve: (ctx: RequestContext) => void,
|
|
127
|
+
reject: (error: Error) => void,
|
|
128
|
+
) => void,
|
|
129
|
+
): void => {
|
|
130
|
+
// 在 fetchingToken 为 true 时将请求放入等待队列
|
|
131
|
+
addRequestInterceptor(async (ctx) => {
|
|
132
|
+
if (isFetchingToken()) {
|
|
133
|
+
return new Promise<RequestContext>((resolve, reject) => {
|
|
134
|
+
addToPendingQueue(ctx, resolve, reject);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return ctx;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// 添加默认请求头
|
|
141
|
+
addRequestInterceptor(async (ctx) => {
|
|
142
|
+
const defaultHeaders = buildDefaultHeaders();
|
|
143
|
+
return {
|
|
144
|
+
url: ctx.url,
|
|
145
|
+
options: {
|
|
146
|
+
...ctx.options,
|
|
147
|
+
headers: {
|
|
148
|
+
...defaultHeaders,
|
|
149
|
+
...(ctx.options.headers || {}),
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// 处理响应中的错误提示
|
|
156
|
+
addResponseInterceptor(async (data: unknown) => {
|
|
157
|
+
if (data && typeof data === 'object' && 'success' in data) {
|
|
158
|
+
const typedData = data as { success: boolean; errorMessage?: string };
|
|
159
|
+
if (typedData.success === false) {
|
|
160
|
+
Message.error(typedData.errorMessage ?? '请求失败!');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
maybePersistTokens(data as Parameters<typeof maybePersistTokens>[0]);
|
|
164
|
+
return data;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// 处理业务错误码
|
|
168
|
+
addResponseInterceptor(async (data: unknown) => {
|
|
169
|
+
if (data && typeof data === 'object' && 'result' in data) {
|
|
170
|
+
const typedData = data as { result?: { code?: number; desc?: string } };
|
|
171
|
+
if (typedData.result) {
|
|
172
|
+
const { code, desc } = typedData.result;
|
|
173
|
+
if (typeof code === 'number' && code !== 0) {
|
|
174
|
+
const error = new Error(desc || `请求失败 (${code})`) as Error & {
|
|
175
|
+
code: number;
|
|
176
|
+
response: unknown;
|
|
177
|
+
};
|
|
178
|
+
error.code = code;
|
|
179
|
+
error.response = data;
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return data;
|
|
185
|
+
});
|
|
186
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSO 单点登录逻辑
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { request as rawRequest } from '@umijs/max';
|
|
6
|
+
import {
|
|
7
|
+
maybePersistTokens,
|
|
8
|
+
setStoredAuthToken,
|
|
9
|
+
} from '../auth/cs-auth-manager';
|
|
10
|
+
import {
|
|
11
|
+
getTicketParam,
|
|
12
|
+
resolveAuthToken,
|
|
13
|
+
resolveExternalLoginPath,
|
|
14
|
+
resolveLoginEndpoint,
|
|
15
|
+
} from './config';
|
|
16
|
+
import { buildDefaultHeaders } from './interceptors';
|
|
17
|
+
import {
|
|
18
|
+
isFetchingToken,
|
|
19
|
+
processPendingRequests,
|
|
20
|
+
rejectPendingRequests,
|
|
21
|
+
setFetchingToken,
|
|
22
|
+
} from './token-refresh';
|
|
23
|
+
import { removeParamFromUrl } from './url-resolver';
|
|
24
|
+
|
|
25
|
+
let ticketPromise: Promise<void> | null = null;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 处理认证失败后的重定向
|
|
29
|
+
*/
|
|
30
|
+
export const handleAuthFailureRedirect = (): void => {
|
|
31
|
+
if (typeof window === 'undefined') return;
|
|
32
|
+
|
|
33
|
+
// 从 URL 中获取当前 redirect 登录的次数
|
|
34
|
+
const currentUrl = new URL(window.location.href);
|
|
35
|
+
const redirectCountParam = currentUrl.searchParams.get('redirect_count');
|
|
36
|
+
const redirectCount = redirectCountParam
|
|
37
|
+
? parseInt(redirectCountParam, 10)
|
|
38
|
+
: 0;
|
|
39
|
+
|
|
40
|
+
// 如果 redirect 次数小于 1 次,则执行 redirect 跳转登录
|
|
41
|
+
if (redirectCount < 1) {
|
|
42
|
+
const externalLoginPath = resolveExternalLoginPath();
|
|
43
|
+
console.log('handleAuthFailureRedirect', externalLoginPath);
|
|
44
|
+
|
|
45
|
+
// 构建回跳地址
|
|
46
|
+
const redirectUrl = new URL(window.location.href);
|
|
47
|
+
redirectUrl.searchParams.delete('redirect_count');
|
|
48
|
+
redirectUrl.searchParams.set('redirect_count', '1');
|
|
49
|
+
const serviceUrl = redirectUrl.toString();
|
|
50
|
+
|
|
51
|
+
window.location.href = `${
|
|
52
|
+
externalLoginPath ?? '/login'
|
|
53
|
+
}?service=${encodeURIComponent(serviceUrl)}`;
|
|
54
|
+
} else {
|
|
55
|
+
console.warn('认证失败,但已达到最大重定向次数,停止重定向');
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 确保 SSO 会话有效
|
|
61
|
+
*/
|
|
62
|
+
export const ensureSsoSession = async (): Promise<void> => {
|
|
63
|
+
if (typeof window === 'undefined') return;
|
|
64
|
+
|
|
65
|
+
if (isFetchingToken()) return;
|
|
66
|
+
|
|
67
|
+
const ticketParam = getTicketParam();
|
|
68
|
+
const ticket = new URLSearchParams(window.location.search).get(ticketParam);
|
|
69
|
+
if (!ticket) return;
|
|
70
|
+
|
|
71
|
+
if (!ticketPromise) {
|
|
72
|
+
ticketPromise = (async () => {
|
|
73
|
+
const endpoint = resolveLoginEndpoint();
|
|
74
|
+
const hasQuery = endpoint.includes('?');
|
|
75
|
+
const joiner = hasQuery ? '&' : '?';
|
|
76
|
+
const loginUrl = `${endpoint}${joiner}${ticketParam}=${encodeURIComponent(
|
|
77
|
+
ticket,
|
|
78
|
+
)}`;
|
|
79
|
+
|
|
80
|
+
setFetchingToken(true);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const result = await rawRequest<unknown>(loginUrl, {
|
|
84
|
+
method: 'GET',
|
|
85
|
+
headers: buildDefaultHeaders(),
|
|
86
|
+
responseInterceptors: [
|
|
87
|
+
(response) => {
|
|
88
|
+
const typedResponse = response as {
|
|
89
|
+
headers?: { authtoken?: string };
|
|
90
|
+
};
|
|
91
|
+
if (typedResponse.headers?.authtoken) {
|
|
92
|
+
setStoredAuthToken(typedResponse.headers.authtoken);
|
|
93
|
+
}
|
|
94
|
+
return response;
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
maybePersistTokens(result as Parameters<typeof maybePersistTokens>[0]);
|
|
100
|
+
setFetchingToken(false);
|
|
101
|
+
|
|
102
|
+
if (resolveAuthToken()) {
|
|
103
|
+
// SSO 认证成功,删除 URL 中的 redirect_count 参数
|
|
104
|
+
if (typeof window !== 'undefined') {
|
|
105
|
+
const current = new URL(window.location.href);
|
|
106
|
+
current.searchParams.delete('redirect_count');
|
|
107
|
+
const nextUrl = `${current.pathname}${
|
|
108
|
+
current.search ? `?${current.searchParams}` : ''
|
|
109
|
+
}${current.hash}`;
|
|
110
|
+
window.history.replaceState(null, '', nextUrl);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
processPendingRequests();
|
|
114
|
+
} else {
|
|
115
|
+
throw new Error('SSO 认证失败');
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error('SSO ticket exchange failed', error);
|
|
119
|
+
setFetchingToken(false);
|
|
120
|
+
|
|
121
|
+
rejectPendingRequests(
|
|
122
|
+
error instanceof Error ? error : new Error('SSO 认证失败'),
|
|
123
|
+
);
|
|
124
|
+
} finally {
|
|
125
|
+
removeParamFromUrl(ticketParam);
|
|
126
|
+
ticketPromise = null;
|
|
127
|
+
}
|
|
128
|
+
})();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await ticketPromise;
|
|
132
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token 刷新逻辑
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { request as rawRequest } from '@umijs/max';
|
|
6
|
+
import {
|
|
7
|
+
getStoredRefreshToken,
|
|
8
|
+
maybePersistTokens,
|
|
9
|
+
setStoredAuthToken,
|
|
10
|
+
setStoredRefreshToken,
|
|
11
|
+
} from '../auth/cs-auth-manager';
|
|
12
|
+
import { resolveRefreshEndpoint } from './config';
|
|
13
|
+
import type { PendingRequest, RequestContext } from './types';
|
|
14
|
+
|
|
15
|
+
// 当前是否在请求用ticket换取token
|
|
16
|
+
let fetchingToken = false;
|
|
17
|
+
|
|
18
|
+
// 等待队列:存储正在获取认证令牌时被拦截的请求
|
|
19
|
+
const pendingRequestsQueue: PendingRequest[] = [];
|
|
20
|
+
|
|
21
|
+
// Token 刷新 Promise 缓存
|
|
22
|
+
const AUTH_REFRESH_PROMISE: { current?: Promise<string | null> } = {};
|
|
23
|
+
|
|
24
|
+
export const isFetchingToken = (): boolean => fetchingToken;
|
|
25
|
+
|
|
26
|
+
export const setFetchingToken = (value: boolean): void => {
|
|
27
|
+
fetchingToken = value;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const addToPendingQueue = (
|
|
31
|
+
ctx: RequestContext,
|
|
32
|
+
resolve: (ctx: RequestContext) => void,
|
|
33
|
+
reject: (error: Error) => void,
|
|
34
|
+
): void => {
|
|
35
|
+
pendingRequestsQueue.push({ ctx, resolve, reject });
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 执行等待队列中的请求
|
|
40
|
+
*/
|
|
41
|
+
export const processPendingRequests = (): void => {
|
|
42
|
+
const queue = [...pendingRequestsQueue];
|
|
43
|
+
pendingRequestsQueue.length = 0;
|
|
44
|
+
|
|
45
|
+
queue.forEach(({ ctx, resolve }) => {
|
|
46
|
+
resolve(ctx);
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 拒绝等待队列中的请求
|
|
52
|
+
*/
|
|
53
|
+
export const rejectPendingRequests = (error: Error): void => {
|
|
54
|
+
const queue = [...pendingRequestsQueue];
|
|
55
|
+
pendingRequestsQueue.length = 0;
|
|
56
|
+
|
|
57
|
+
queue.forEach(({ reject }) => {
|
|
58
|
+
reject(error);
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 刷新认证 Token
|
|
64
|
+
*/
|
|
65
|
+
export const refreshAuthToken = async (): Promise<string | null> => {
|
|
66
|
+
if (fetchingToken) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const refreshToken = getStoredRefreshToken();
|
|
71
|
+
if (!refreshToken) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const endpoint = resolveRefreshEndpoint();
|
|
76
|
+
fetchingToken = true;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const result = await rawRequest<{
|
|
80
|
+
code?: number;
|
|
81
|
+
data?: { access?: string };
|
|
82
|
+
}>(endpoint, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
data: { refresh: refreshToken },
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
maybePersistTokens(result);
|
|
88
|
+
const success = result?.code === 200;
|
|
89
|
+
const token = success ? result?.data?.access ?? null : null;
|
|
90
|
+
fetchingToken = false;
|
|
91
|
+
|
|
92
|
+
if (token) {
|
|
93
|
+
processPendingRequests();
|
|
94
|
+
} else {
|
|
95
|
+
rejectPendingRequests(new Error('获取认证令牌失败'));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return typeof token === 'string' ? token : null;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.error('refresh auth token failed', err);
|
|
101
|
+
fetchingToken = false;
|
|
102
|
+
setStoredAuthToken(null);
|
|
103
|
+
setStoredRefreshToken(null);
|
|
104
|
+
|
|
105
|
+
rejectPendingRequests(
|
|
106
|
+
err instanceof Error ? err : new Error('获取认证令牌失败'),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 获取刷新 Token 的 Promise(防止重复请求)
|
|
115
|
+
*/
|
|
116
|
+
export const getRefreshPromise = (): Promise<string | null> => {
|
|
117
|
+
if (!AUTH_REFRESH_PROMISE.current) {
|
|
118
|
+
console.info('try to refresh auth token');
|
|
119
|
+
AUTH_REFRESH_PROMISE.current = refreshAuthToken().finally(() => {
|
|
120
|
+
AUTH_REFRESH_PROMISE.current = undefined;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return AUTH_REFRESH_PROMISE.current;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 判断是否应该尝试刷新 Token
|
|
128
|
+
*/
|
|
129
|
+
export const shouldAttemptRefresh = (error: unknown): boolean => {
|
|
130
|
+
if (error && typeof error === 'object' && 'response' in error) {
|
|
131
|
+
const typedError = error as { response?: { status?: number } };
|
|
132
|
+
const status = typedError.response?.status;
|
|
133
|
+
return status === 401 || status === 403;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 请求模块类型定义
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type UnifiedRequestOptions = {
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
proxySuffix?: string;
|
|
8
|
+
useCache?: boolean;
|
|
9
|
+
validateCache?: (url: string, options: UnifiedRequestOptions) => boolean;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type RequestContext = {
|
|
14
|
+
url: string;
|
|
15
|
+
options: UnifiedRequestOptions;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type RequestInterceptor = (
|
|
19
|
+
ctx: RequestContext,
|
|
20
|
+
) => Promise<RequestContext> | RequestContext;
|
|
21
|
+
|
|
22
|
+
export type ResponseInterceptor<T = unknown> = (
|
|
23
|
+
response: T,
|
|
24
|
+
ctx: RequestContext,
|
|
25
|
+
) => Promise<T> | T;
|
|
26
|
+
|
|
27
|
+
export type TokenResolver = () => string | undefined;
|
|
28
|
+
|
|
29
|
+
export interface RequestClientOptions {
|
|
30
|
+
appId: string;
|
|
31
|
+
ticketParam: string;
|
|
32
|
+
loginEndpoint: string;
|
|
33
|
+
refreshEndpoint: string;
|
|
34
|
+
externalLoginPath: string;
|
|
35
|
+
logoutPath: string;
|
|
36
|
+
apiBaseUrl: string;
|
|
37
|
+
proxySuffix: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PendingRequest {
|
|
41
|
+
ctx: RequestContext;
|
|
42
|
+
resolve: (ctx: RequestContext) => void;
|
|
43
|
+
reject: (error: Error) => void;
|
|
44
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL 解析与拼接工具
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
resolveProxySuffix as getProxySuffix,
|
|
7
|
+
resolveApiBaseUrl,
|
|
8
|
+
} from './config';
|
|
9
|
+
import type { UnifiedRequestOptions } from './types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 拼接 baseUrl 和路径,自动处理多余的斜杠
|
|
13
|
+
*/
|
|
14
|
+
export const joinBaseUrl = (baseUrl: string, url: string): string => {
|
|
15
|
+
if (
|
|
16
|
+
url.startsWith('http://') ||
|
|
17
|
+
url.startsWith('https://') ||
|
|
18
|
+
url.startsWith('//')
|
|
19
|
+
) {
|
|
20
|
+
return url;
|
|
21
|
+
}
|
|
22
|
+
const normalizedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
23
|
+
const normalizedPath = url.startsWith('/') ? url : `/${url}`;
|
|
24
|
+
return `${normalizedBase}${normalizedPath}`;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 拼接代理路径和 API 路径,避免双斜杠
|
|
29
|
+
*/
|
|
30
|
+
export const joinProxyAndPath = (proxySuffix: string, path: string): string => {
|
|
31
|
+
if (
|
|
32
|
+
path.startsWith('http://') ||
|
|
33
|
+
path.startsWith('https://') ||
|
|
34
|
+
path.startsWith('//')
|
|
35
|
+
) {
|
|
36
|
+
// 绝对 URL 直接透传,不拼接代理前缀
|
|
37
|
+
return path;
|
|
38
|
+
}
|
|
39
|
+
const normalizedProxy = proxySuffix.endsWith('/')
|
|
40
|
+
? proxySuffix.slice(0, -1)
|
|
41
|
+
: proxySuffix;
|
|
42
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
43
|
+
return `${normalizedProxy}${normalizedPath}`;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 解析最终请求 URL
|
|
48
|
+
*/
|
|
49
|
+
export const resolveRequestUrl = (
|
|
50
|
+
url: string,
|
|
51
|
+
options?: UnifiedRequestOptions,
|
|
52
|
+
isAlwaysRemote = false,
|
|
53
|
+
): string => {
|
|
54
|
+
const useRelative = process.env.NODE_ENV !== 'production' && !isAlwaysRemote;
|
|
55
|
+
const proxySuffix = getProxySuffix(options?.proxySuffix);
|
|
56
|
+
|
|
57
|
+
if (useRelative) {
|
|
58
|
+
return url;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return joinBaseUrl(resolveApiBaseUrl(), joinProxyAndPath(proxySuffix, url));
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 从 URL 中移除指定参数
|
|
66
|
+
*/
|
|
67
|
+
export const removeParamFromUrl = (param: string): void => {
|
|
68
|
+
if (typeof window === 'undefined') return;
|
|
69
|
+
const current = new URL(window.location.href);
|
|
70
|
+
current.searchParams.delete(param);
|
|
71
|
+
const nextUrl = `${current.pathname}${
|
|
72
|
+
current.search ? `?${current.searchParams}` : ''
|
|
73
|
+
}${current.hash}`;
|
|
74
|
+
window.history.replaceState(null, '', nextUrl);
|
|
75
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 主题管理工具
|
|
3
|
+
* 支持在 localStorage 中存储主题状态,并在页面加载时动态加载对应的主题
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { THEME } from './constants';
|
|
7
|
+
|
|
8
|
+
export type ThemeMode = 'light' | 'dark';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 获取当前主题
|
|
12
|
+
*/
|
|
13
|
+
export function getTheme(): ThemeMode {
|
|
14
|
+
if (typeof window === 'undefined') {
|
|
15
|
+
return THEME.DEFAULT;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const theme = localStorage.getItem(THEME.STORAGE_KEY) as ThemeMode | null;
|
|
19
|
+
if (theme === 'light' || theme === 'dark') {
|
|
20
|
+
return theme;
|
|
21
|
+
}
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('获取主题失败:', error);
|
|
24
|
+
}
|
|
25
|
+
return THEME.DEFAULT;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 设置主题
|
|
30
|
+
*/
|
|
31
|
+
export function setTheme(theme: ThemeMode): void {
|
|
32
|
+
if (typeof window === 'undefined') {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
localStorage.setItem(THEME.STORAGE_KEY, theme);
|
|
37
|
+
applyTheme(theme);
|
|
38
|
+
// 触发自定义事件,通知其他组件主题已变化
|
|
39
|
+
window.dispatchEvent(new CustomEvent('themechange', { detail: theme }));
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error('设置主题失败:', error);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 应用主题到 DOM
|
|
47
|
+
* 通过切换 body 的 data-theme 和 arco-theme 属性来应用不同的主题
|
|
48
|
+
*/
|
|
49
|
+
export function applyTheme(theme: ThemeMode): void {
|
|
50
|
+
if (typeof document === 'undefined') return;
|
|
51
|
+
|
|
52
|
+
if (theme === 'dark') {
|
|
53
|
+
document.body.setAttribute('arco-theme', 'dark');
|
|
54
|
+
document.body.setAttribute('data-theme', 'dark');
|
|
55
|
+
} else {
|
|
56
|
+
document.body.removeAttribute('arco-theme');
|
|
57
|
+
document.body.setAttribute('data-theme', 'normal');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 获取系统主题偏好
|
|
63
|
+
*/
|
|
64
|
+
export function getSystemTheme(): ThemeMode {
|
|
65
|
+
if (typeof window === 'undefined') return THEME.DEFAULT;
|
|
66
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
67
|
+
? 'dark'
|
|
68
|
+
: 'light';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 获取存储的主题或系统主题
|
|
73
|
+
*/
|
|
74
|
+
export function getStoredTheme(): ThemeMode {
|
|
75
|
+
if (typeof window === 'undefined') return THEME.DEFAULT;
|
|
76
|
+
const stored = localStorage.getItem(THEME.STORAGE_KEY);
|
|
77
|
+
if (stored === 'light' || stored === 'dark') {
|
|
78
|
+
return stored;
|
|
79
|
+
}
|
|
80
|
+
return getSystemTheme();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 初始化主题
|
|
85
|
+
* 在页面加载时调用,根据 localStorage 中的主题状态加载对应的主题
|
|
86
|
+
* 注意:这个函数需要在页面最早期调用,在 React 渲染之前
|
|
87
|
+
*/
|
|
88
|
+
export function initTheme(): void {
|
|
89
|
+
if (typeof window === 'undefined') {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const theme = getStoredTheme();
|
|
94
|
+
|
|
95
|
+
// 立即设置 data-theme 属性,避免闪烁
|
|
96
|
+
if (document.body) {
|
|
97
|
+
applyTheme(theme);
|
|
98
|
+
} else {
|
|
99
|
+
// 如果 body 还没有加载,等待 DOMContentLoaded
|
|
100
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
101
|
+
applyTheme(getStoredTheme());
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 导出常量供外部使用
|
|
107
|
+
export { THEME };
|