Source: utils/request.js

/**
 * @file utils/request.js
 * @description 配置 Axios HTTP 请求实例,包括基础 URL、超时设置、请求/响应拦截器。
 * 负责全局加载状态管理、认证 token 注入和统一错误处理。
 * @module Request
 */

import axios from 'axios'
import router from '@/router'
import { useAppStore } from '@/stores'
import { ElMessage } from 'element-plus'

/**
 * @constant {string} baseURL
 * @description API 请求的基础 URL,从环境变量中获取。
 */
const baseURL = import.meta.env.VITE_BASE_API

/**
 * @constant {AxiosInstance} request
 * @description 创建 Axios 请求实例。
 * 配置了基础 URL、超时时间,并默认携带 cookie。
 */
const request = axios.create({
  baseURL,
  timeout: 15000, // 请求超时时间
  withCredentials: true // 所有请求默认携带 cookie
})

/**
 * Axios 请求拦截器。
 * 在请求发送前执行,用于:
 * 1. 控制全局加载动画。
 * 2. 在请求头中注入认证 token。
 */
request.interceptors.request.use(
  /**
   * @function requestInterceptorSuccess
   * @description 请求成功拦截器。
   * @param {AxiosRequestConfig} config - 请求配置对象。
   * @param {boolean} [config.showGlobalLoading=false] - 自定义配置,控制是否显示全局加载动画。
   * @returns {AxiosRequestConfig} 修改后的请求配置对象。
   */
  (config) => {
    const appStore = useAppStore()
    // 根据配置决定是否显示全局加载动画
    if (config.showGlobalLoading) {
      appStore.startLoading()
    }

    const token = localStorage.getItem('token')
    // 如果存在 token,则在请求头中添加 Authorization 字段
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  /**
   * @function requestInterceptorError
   * @description 请求失败拦截器。
   * @param {any} err - 请求错误对象。
   * @returns {Promise<never>} 返回一个被拒绝的 Promise。
   */
  (err) => {
    const appStore = useAppStore()
    // 如果请求配置存在且开启了全局加载,则停止加载动画
    if (err.config && err.config.showGlobalLoading) {
      appStore.stopLoading()
    }
    return Promise.reject(err)
  }
)

/**
 * Axios 响应拦截器。
 * 在接收到响应后执行,用于:
 * 1. 停止全局加载动画。
 * 2. 处理成功的业务响应。
 * 3. 统一处理业务错误和 HTTP 错误(如 401)。
 */
request.interceptors.response.use(
  /**
   * @function responseInterceptorSuccess
   * @description 响应成功拦截器。
   * @param {AxiosResponse} response - 响应对象。
   * @param {boolean} [response.config.showGlobalLoading=false] - 请求配置,控制是否显示全局加载动画。
   * @returns {object|Promise<never>} 如果业务状态为 'success',返回响应数据;否则,显示错误信息并返回被拒绝的 Promise。
   */
  function (response) {
    const appStore = useAppStore()
    // 如果请求配置存在且开启了全局加载,则停止加载动画
    if (response.config.showGlobalLoading) {
      appStore.stopLoading()
    }

    // 检查后端返回的业务状态码
    if (response.data.status === 'success') {
      return response.data
    }
    // 业务失败,显示错误信息
    ElMessage.error(response?.data?.message || '服务异常')
    return Promise.reject(response.data) // 返回被拒绝的 Promise,传递后端返回的错误数据
  },
  /**
   * @function responseInterceptorError
   * @description 响应失败拦截器。
   * @param {AxiosError} error - 错误对象。
   * @param {boolean} [error.config.showGlobalLoading=false] - 请求配置,控制是否显示全局加载动画。
   * @returns {Promise<never>} 返回一个被拒绝的 Promise。
   */
  function (error) {
    const appStore = useAppStore()
    // 如果请求配置存在且开启了全局加载,则停止加载动画
    if (error.config && error.config.showGlobalLoading) {
      appStore.stopLoading()
    }

    // 处理 HTTP 状态码为 401 (未授权) 的情况
    if (error.response?.status === 401) {
      // 清除本地存储中的用户认证信息
      localStorage.removeItem('user')
      localStorage.removeItem('token')
      // 重定向到登录页面
      router.push('/auth')
    }
    // 关闭所有现有消息提示,避免重复弹窗
    ElMessage.closeAll()
    // 显示错误提示
    ElMessage({
      message: error.response?.data?.message || '错误异常',
      type: 'error',
      offset: 20 // 消息提示的偏移量
    })

    // 返回被拒绝的 Promise,传递响应中的错误数据
    return Promise.reject(error.response?.data)
  }
)

export default request