Files
vue3-element-admin/src/utils/request.ts

141 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import axios, { type AxiosResponse, type InternalAxiosRequestConfig } from "axios";
import qs from "qs";
import { ApiCodeEnum } from "@/enums/api";
import { AuthStorage } from "@/utils/auth";
import { useUserStoreHook } from "@/stores/user";
import { usePermissionStoreHook } from "@/stores/permission";
import type { ApiResult } from "@/api/common";
// 防止同一请求在 token 刷新后重复进入重试,导致死循环
const retriedRequests = new WeakSet<InternalAxiosRequestConfig>();
const http = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 50000,
headers: { "Content-Type": "application/json;charset=utf-8" },
// 数组参数序列化为 ids=1&ids=2,而非 ids[]=1&ids[]=2
paramsSerializer: (params) => qs.stringify(params, { arrayFormat: "repeat" }),
});
// 大文件上传专用实例:直连后端地址(VITE_APP_API_URL),绕过 vite dev proxy,
// 避免 vite/http-proxy 转发大请求体时缓冲/超时导致连接中断(客户端 EOFException)。
const uploadHttp = axios.create({
baseURL: import.meta.env.VITE_APP_API_URL,
// 大文件上传放宽超时到 10 分钟(默认 50s 对 apks 等大文件不足)
timeout: 600000,
headers: { "Content-Type": "application/json;charset=utf-8" },
});
function setupInterceptors(instance: typeof http) {
instance.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = AuthStorage.getAccessToken();
// 约定:调用方设置 Authorization 为 "no-auth",或设置 anonymous 即跳过 token 注入
if (config.headers.Authorization === "no-auth" || config.anonymous) {
delete config.headers.Authorization;
} else if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error),
);
instance.interceptors.response.use(
(response: AxiosResponse<ApiResult>): AxiosResponse | any => {
const { responseType } = response.config;
// 二进制数据直接透传
if (responseType === "blob" || responseType === "arraybuffer") {
return response;
}
const { code, data, msg } = response.data;
if (code === ApiCodeEnum.SUCCESS) {
return data;
}
ElMessage.error(msg || "系统出错");
return Promise.reject(new Error(msg || "系统出错"));
},
async (error) => {
// 请求被主动取消(如切换路由取消未完成请求),不再提示错误
if (axios.isCancel(error)) {
return Promise.reject(error);
}
const { config, response } = error;
if (!response) {
ElMessage.error("网络连接失败");
return Promise.reject(error);
}
// 调用方声明静默的错误状态码(如轮询 401/403 不弹错误),直接透传
if (
response &&
(
error.config as { silentErrorStatuses?: number[] } | undefined
)?.silentErrorStatuses?.includes(response.status)
) {
return Promise.reject(error);
}
const { code, msg } = response.data as ApiResult;
// Token 过期
if (code === ApiCodeEnum.ACCESS_TOKEN_INVALID) {
if (!config || retriedRequests.has(config)) {
await useUserStoreHook().redirectToLogin();
return Promise.reject(new Error("Token Invalid"));
}
retriedRequests.add(config);
try {
const userStore = useUserStoreHook();
await userStore.refreshTokenOnce();
const token = AuthStorage.getAccessToken();
if (token) {
config.headers.set("Authorization", `Bearer ${token}`);
}
return instance(config);
} catch {
await useUserStoreHook().redirectToLogin();
return Promise.reject(new Error("Token Invalid"));
}
}
// Refresh token 失效
if (code === ApiCodeEnum.REFRESH_TOKEN_INVALID) {
await useUserStoreHook().redirectToLogin();
return Promise.reject(new Error("Token Invalid"));
}
// 权限不足
if (code === ApiCodeEnum.PERMISSION_DENIED) {
const permissionStore = usePermissionStoreHook();
await permissionStore.refreshPermissions();
ElMessage.error(msg || "权限不足");
return Promise.reject(new Error(msg || "权限不足"));
}
ElMessage.error(msg || "请求失败");
return Promise.reject(new Error(msg || "请求失败"));
},
);
}
setupInterceptors(http);
setupInterceptors(uploadHttp);
export default http;
export { uploadHttp };