request.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import axios, {
  2. type InternalAxiosRequestConfig,
  3. type AxiosResponse,
  4. type AxiosInstance,
  5. type AxiosError,
  6. } from "axios";
  7. import qs from "qs";
  8. import { ResultEnum } from "@/enums/api/result.enum";
  9. import { Auth } from "@/utils/auth";
  10. import { redirectToLogin } from "@/utils/authRedirect";
  11. declare module "axios" {
  12. export interface AxiosRequestConfig {
  13. /** 静默请求:失败不弹 toast(后台自动刷新用)。401 / TOKEN_EXPIRED 的跳登录行为不变 */
  14. silent?: boolean;
  15. }
  16. }
  17. /**
  18. * 创建 HTTP 请求实例
  19. */
  20. const httpRequest: AxiosInstance = axios.create({
  21. baseURL: import.meta.env.VITE_APP_BASE_API,
  22. timeout: import.meta.env.VITE_TIMEOUT,
  23. headers: { "Content-Type": "application/json;charset=utf-8" },
  24. paramsSerializer: (params) => qs.stringify(params, { indices: false }),
  25. });
  26. /**
  27. * 请求拦截器 - 添加 Authorization 头
  28. */
  29. httpRequest.interceptors.request.use(
  30. (config: InternalAxiosRequestConfig) => {
  31. const accessToken = Auth.getAccessToken();
  32. const auth = config.headers.Authorization;
  33. // 显式跳过鉴权(与单接口 headers 约定一致)
  34. if (auth === "no-auth") {
  35. delete config.headers.Authorization;
  36. return config;
  37. }
  38. // 未手动设置 Authorization 时自动附加 Bearer;已设置则保留(避免误删调用方传入的令牌)
  39. if (!auth && accessToken) {
  40. config.headers.Authorization = `Bearer ${accessToken}`;
  41. }
  42. return config;
  43. },
  44. (error) => {
  45. const msg = error instanceof Error ? error.message : String(error);
  46. ElMessage.error(msg);
  47. return Promise.reject(error);
  48. }
  49. );
  50. /**
  51. * 响应拦截器 - 统一处理响应和错误
  52. */
  53. httpRequest.interceptors.response.use(
  54. (response: AxiosResponse<ApiResponse>) => {
  55. // 如果响应是二进制流,则直接返回(用于文件下载、Excel 导出等)
  56. if (response.config.responseType === "blob") {
  57. return response;
  58. }
  59. const data = response.data;
  60. // 检查请求是否失败
  61. if (data.code !== ResultEnum.SUCCESS) {
  62. if (!response.config.silent) {
  63. ElMessage.error(data.msg);
  64. }
  65. return Promise.reject(response);
  66. }
  67. // 如果请求不是 GET 请求,且不是登录或退出登录接口,请求成功时显示成功提示
  68. if (
  69. !response.config.silent &&
  70. response.config.method?.toUpperCase() !== "GET" &&
  71. !response.config.url?.includes("login") &&
  72. !response.config.url?.includes("logout")
  73. ) {
  74. ElMessage.success(data.msg);
  75. }
  76. return response;
  77. },
  78. async (error: AxiosError<ApiResponse>) => {
  79. const silent = error.config?.silent === true;
  80. // 处理网络错误(连接拒绝、超时等)
  81. if (!error.response) {
  82. let errorMessage = "网络连接异常";
  83. // 根据错误类型提供更友好的提示
  84. if (error.message?.includes("ECONNREFUSED")) {
  85. errorMessage = "服务器连接失败,请检查后端服务是否正常运行";
  86. } else if (error.message?.includes("timeout")) {
  87. errorMessage = "请求超时,请稍后重试";
  88. } else if (error.message?.includes("Network Error")) {
  89. errorMessage = "网络连接错误,请检查您的网络设置";
  90. }
  91. console.error("网络请求失败:", error);
  92. if (!silent) {
  93. ElMessage.error(errorMessage);
  94. }
  95. return Promise.reject(new Error(errorMessage));
  96. }
  97. const data = error.response?.data;
  98. // 处理blob类型的错误响应
  99. if (error.response?.config.responseType === "blob" && error.response.data instanceof Blob) {
  100. try {
  101. // 将blob转换为JSON
  102. const text = await new Response(error.response.data).text();
  103. const jsonData: ApiResponse = JSON.parse(text);
  104. if (jsonData.code === ResultEnum.ERROR) {
  105. ElMessage.error(jsonData.msg || "请求错误");
  106. return Promise.reject(new Error(jsonData.msg || "请求错误"));
  107. } else if (jsonData.code === ResultEnum.EXCEPTION) {
  108. ElMessage.error(jsonData.msg || "服务异常");
  109. return Promise.reject(new Error(jsonData.msg || "服务异常"));
  110. }
  111. } catch (e) {
  112. console.error("请求异常:", e);
  113. // 如果无法解析为JSON,则使用默认错误处理
  114. ElMessage.error("数据解析失败");
  115. return Promise.reject(new Error("数据解析失败"));
  116. }
  117. }
  118. const status = error.response.status;
  119. /** 是否为后端约定的 JSON 业务码结构 */
  120. const hasApiCode =
  121. data !== undefined &&
  122. data !== null &&
  123. typeof data === "object" &&
  124. "code" in data &&
  125. typeof (data as ApiResponse).code === "number";
  126. // HTTP 401 且无约定 body(如网关仅返回状态码、HTML、空 body):按登录失效处理
  127. if (status === 401 && !hasApiCode) {
  128. await redirectToLogin("登录已失效,请重新登录");
  129. return Promise.reject(new Error("Unauthorized"));
  130. }
  131. if (data?.code === ResultEnum.TOKEN_EXPIRED) {
  132. await redirectToLogin("登录已过期,请重新登录");
  133. return Promise.reject(new Error(data.msg));
  134. } else if (data?.code === ResultEnum.ERROR) {
  135. if (!silent) {
  136. ElMessage.error(data.msg || "请求错误");
  137. }
  138. return Promise.reject(new Error(data.msg || "请求错误"));
  139. } else if (data?.code === ResultEnum.UNAUTHORIZED) {
  140. if (!silent) {
  141. ElMessage.error(data.msg || "暂无权限");
  142. }
  143. return Promise.reject(new Error(data.msg || "请求错误"));
  144. } else if (data?.code === ResultEnum.EXCEPTION) {
  145. if (!silent) {
  146. ElMessage.error(data.msg || "服务异常");
  147. }
  148. return Promise.reject(new Error(data.msg || "服务异常"));
  149. } else {
  150. if (!silent) {
  151. ElMessage.error("请求处理失败,请稍后重试");
  152. }
  153. return Promise.reject(new Error("请求处理失败"));
  154. }
  155. }
  156. );
  157. export default httpRequest;