113 lines
3.5 KiB
TypeScript
113 lines
3.5 KiB
TypeScript
import { APP_CONFIG } from '@/config';
|
|
import { ensureLogin } from '@/utils/auth';
|
|
import { HttpMethod, httpRequest, toFormUrlEncoded } from '@/utils/http';
|
|
import { authStorage } from '@/utils/storage';
|
|
|
|
export interface RequestOptions {
|
|
method?: HttpMethod;
|
|
data?: unknown;
|
|
header?: Record<string, string>;
|
|
skipAuth?: boolean;
|
|
skipAutoLogin?: boolean;
|
|
}
|
|
|
|
export class RequestError extends Error {
|
|
constructor(
|
|
message: string,
|
|
public readonly statusCode?: number,
|
|
public readonly data?: unknown
|
|
) {
|
|
super(message);
|
|
this.name = 'RequestError';
|
|
}
|
|
}
|
|
|
|
const objectToQueryString = (data: object = {}) => {
|
|
return Object.entries(data)
|
|
.filter(([, value]) => value !== undefined && value !== null)
|
|
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
|
|
.join('&');
|
|
};
|
|
|
|
const buildUrl = (endpoint: string) => {
|
|
if (/^https?:\/\//.test(endpoint)) return endpoint;
|
|
return `${APP_CONFIG.API_BASE_URL}/${endpoint.replace(/^\//, '')}`;
|
|
};
|
|
|
|
const isAuthExpired = (statusCode: number, body: any) => {
|
|
const businessCode = Number(body?.code);
|
|
return statusCode === 401 || statusCode === 402 || businessCode === 401 || businessCode === 402;
|
|
};
|
|
|
|
const requestOnce = async <T>(url: string, options: RequestOptions, retried: boolean): Promise<T> => {
|
|
const headers: Record<string, string> = {
|
|
'content-type': 'application/json',
|
|
...options.header
|
|
};
|
|
|
|
if (!options.skipAuth) {
|
|
const token = authStorage.getToken();
|
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
|
|
const response = await httpRequest<T>({
|
|
url,
|
|
method: options.method || 'GET',
|
|
data: options.data,
|
|
header: headers
|
|
});
|
|
|
|
if (isAuthExpired(response.statusCode, response.data)) {
|
|
if (!options.skipAutoLogin && !options.skipAuth && !retried) {
|
|
authStorage.clearToken();
|
|
await ensureLogin();
|
|
return requestOnce<T>(url, options, true);
|
|
}
|
|
|
|
throw new RequestError('登录状态已失效', response.statusCode, response.data);
|
|
}
|
|
|
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
throw new RequestError(`HTTP 请求失败:${response.statusCode}`, response.statusCode, response.data);
|
|
}
|
|
|
|
if (Number((response.data as any)?.code) === 500) {
|
|
throw new RequestError((response.data as any)?.msg || '服务器处理失败', response.statusCode, response.data);
|
|
}
|
|
|
|
return response.data;
|
|
};
|
|
|
|
export const request = <T = any>(endpoint: string, options: RequestOptions = {}) => {
|
|
return requestOnce<T>(buildUrl(endpoint), options, false);
|
|
};
|
|
|
|
export const get = <T = any>(endpoint: string, params?: object, options: RequestOptions = {}) => {
|
|
const queryString = params ? `?${objectToQueryString(params)}` : '';
|
|
return request<T>(`${endpoint}${queryString}`, { ...options, method: 'GET' });
|
|
};
|
|
|
|
export const post = <T = any>(endpoint: string, data?: unknown, options: RequestOptions = {}) => {
|
|
return request<T>(endpoint, { ...options, method: 'POST', data });
|
|
};
|
|
|
|
export const postForm = <T = any>(endpoint: string, data?: Record<string, unknown>, options: RequestOptions = {}) => {
|
|
return request<T>(endpoint, {
|
|
...options,
|
|
method: 'POST',
|
|
data: toFormUrlEncoded(data),
|
|
header: {
|
|
...options.header,
|
|
'content-type': 'application/x-www-form-urlencoded'
|
|
}
|
|
});
|
|
};
|
|
|
|
export const put = <T = any>(endpoint: string, data?: unknown, options: RequestOptions = {}) => {
|
|
return request<T>(endpoint, { ...options, method: 'PUT', data });
|
|
};
|
|
|
|
export const del = <T = any>(endpoint: string, data?: unknown, options: RequestOptions = {}) => {
|
|
return request<T>(endpoint, { ...options, method: 'DELETE', data });
|
|
};
|