179 lines
5.0 KiB
TypeScript
179 lines
5.0 KiB
TypeScript
import { auth } from '@/hooks/';
|
|
import { message } from 'ant-design-vue';
|
|
|
|
const baseUrl = import.meta.env?.VITE_API_BASE_URL || '';
|
|
const DEFAULT_TIMEOUT = 10000;
|
|
|
|
const activeControllers = new Set<AbortController>();
|
|
|
|
const getResponseErrorMsg = (data: any, fallback = '请求失败') =>
|
|
data?.data?.msg || data?.msg || data?.message || fallback;
|
|
|
|
/** 统一处理 401 未授权逻辑 */
|
|
const handleUnauthorized = (msg?: string) => {
|
|
auth.logout();
|
|
message.error(msg || '登录状态已过期,请重新登录');
|
|
window.location.href = '/login';
|
|
};
|
|
|
|
const request = (
|
|
url: string,
|
|
options: RequestInit & {
|
|
query?: Record<string, any>;
|
|
responseType?: 'json' | 'blob';
|
|
} = {},
|
|
) => {
|
|
let finalUrl = baseUrl + url;
|
|
|
|
if (options.query) {
|
|
finalUrl += '?' + new URLSearchParams(options.query).toString();
|
|
}
|
|
|
|
const token = auth.token.value;
|
|
|
|
const headers: Record<string, string> = {};
|
|
if (!(options.body instanceof FormData)) {
|
|
headers['Content-Type'] = 'application/json';
|
|
}
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
|
|
// 优先使用外部传入的 signal(来自 useRequest 取消机制);
|
|
// 无外部 signal 时自建 controller 用于超时取消
|
|
const externalSignal = (options as RequestInit & { signal?: AbortSignal }).signal;
|
|
const controller = new AbortController();
|
|
|
|
activeControllers.add(controller);
|
|
|
|
// 合并外部 signal 与内部 controller:任一 abort 都会取消请求
|
|
const onExternalAbort = () => controller.abort();
|
|
if (externalSignal) {
|
|
if (externalSignal.aborted) {
|
|
controller.abort();
|
|
} else {
|
|
externalSignal.addEventListener('abort', onExternalAbort, { once: true });
|
|
}
|
|
}
|
|
|
|
const init: RequestInit = {
|
|
...options,
|
|
headers: { ...headers, ...(options.headers as Record<string, string>) },
|
|
signal: controller.signal,
|
|
};
|
|
|
|
const timeoutId = setTimeout(() => {
|
|
controller.abort(); // 超时触发 abort
|
|
}, DEFAULT_TIMEOUT);
|
|
|
|
const cleanupSignal = () => {
|
|
clearTimeout(timeoutId);
|
|
if (externalSignal) externalSignal.removeEventListener('abort', onExternalAbort);
|
|
};
|
|
|
|
return fetch(finalUrl, init)
|
|
.then(async (res) => {
|
|
cleanupSignal();
|
|
activeControllers.delete(controller);
|
|
|
|
if (res.status === 401) {
|
|
const errorData = await res.json().catch(() => ({}));
|
|
const errorMsg = getResponseErrorMsg(errorData, '登录状态已过期,请重新登录');
|
|
handleUnauthorized(errorMsg);
|
|
return Promise.reject(new Error(errorMsg));
|
|
}
|
|
|
|
if (res.status === 500) {
|
|
const errorData = await res.json().catch(() => ({}));
|
|
const errorMsg = getResponseErrorMsg(errorData, '服务器开小差了,请稍后再试');
|
|
message.error(errorMsg);
|
|
return Promise.reject(new Error(errorMsg));
|
|
}
|
|
|
|
if (!res.ok) {
|
|
const errorData = await res.json().catch(() => ({}));
|
|
const errorMsg = getResponseErrorMsg(errorData);
|
|
message.error(errorMsg);
|
|
return Promise.reject(new Error(errorMsg));
|
|
}
|
|
|
|
if (options.responseType === 'blob') {
|
|
return res.blob();
|
|
}
|
|
|
|
const data = await res.json();
|
|
|
|
if (data.code == 401) {
|
|
const errorMsg = getResponseErrorMsg(data, '登录状态已过期,请重新登录');
|
|
handleUnauthorized(errorMsg);
|
|
return Promise.reject(new Error(errorMsg));
|
|
}
|
|
|
|
if (data.code && data.code != '200') {
|
|
const errorMsg = getResponseErrorMsg(data);
|
|
message.error(errorMsg);
|
|
return Promise.reject(new Error(errorMsg));
|
|
}
|
|
|
|
return data;
|
|
})
|
|
.catch((err) => {
|
|
cleanupSignal();
|
|
|
|
activeControllers.delete(controller);
|
|
|
|
if (err.name === 'AbortError' || err.message === 'The user aborted a request.') {
|
|
// 使用特殊标记对象区分“请求被取消”和“请求成功但返回 null”
|
|
return Promise.resolve({ __aborted: true });
|
|
}
|
|
|
|
if (err.message === 'Failed to fetch') {
|
|
message.error('网络连接失败,请检查网络');
|
|
}
|
|
|
|
return Promise.reject(err);
|
|
});
|
|
};
|
|
|
|
export const cancelAllRequests = () => {
|
|
activeControllers.forEach((controller) => {
|
|
controller.abort();
|
|
});
|
|
activeControllers.clear();
|
|
};
|
|
|
|
export { request };
|
|
|
|
export const get = (url: string, query?: Record<string, any>, options?: { signal?: AbortSignal }) =>
|
|
request(url, { method: 'GET', query, signal: options?.signal });
|
|
|
|
export const post = (
|
|
url: string,
|
|
body?: any,
|
|
query?: Record<string, any>,
|
|
options?: { signal?: AbortSignal },
|
|
) =>
|
|
request(url, {
|
|
method: 'POST',
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
query,
|
|
signal: options?.signal,
|
|
});
|
|
|
|
export const put = (url: string, body?: any, query?: Record<string, any>) =>
|
|
request(url, {
|
|
method: 'PUT',
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
query,
|
|
});
|
|
|
|
export const del = (url: string, query?: Record<string, any>) =>
|
|
request(url, { method: 'DELETE', query });
|
|
|
|
export default {
|
|
get,
|
|
post,
|
|
put,
|
|
delete: del,
|
|
};
|