All files / src/services http.js

0% Statements 0/52
0% Branches 0/40
0% Functions 0/8
0% Lines 0/46

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111                                                                                                                                                                                                                             
import axios from 'axios'
import { useAuthStore } from '@/stores/auth'
import { Message } from '@/components/ui/message/message-manager'
 
const AUTH_ERROR_CODE = 401
const SESSION_REPLACED_CODE = 40101
 
const http = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
  timeout: 15000,
  headers: {
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
})
 
let redirectingToLogin = false
 
function redirectToLogin(reason) {
  const currentPath = window.location.pathname.replace(/\/+$/, '') || '/'
  if (currentPath === '/login' || redirectingToLogin) return
 
  redirectingToLogin = true
  const redirect = `${window.location.pathname}${window.location.search}${window.location.hash}`
  const query = new URLSearchParams({ redirect })
  if (reason) query.set('reason', reason)
  window.location.assign(`/login?${query.toString()}`)
}
 
function isAuthenticationError(code) {
  return code === AUTH_ERROR_CODE || code === SESSION_REPLACED_CODE
}
 
function handleUnauthorized(code) {
  const authStore = useAuthStore()
  authStore.clearSession()
  redirectToLogin(code === SESSION_REPLACED_CODE ? 'session-replaced' : 'expired')
}
 
// 请求拦截器:注入 Bearer token(调用时读取 store,保证 pinia 已激活)
http.interceptors.request.use((config) => {
  const authStore = useAuthStore()
  if (authStore.loginInfo.token) {
    config.headers.Authorization = `Bearer ${authStore.loginInfo.token}`
  }
  return config
})
 
// 响应拦截器:统一按 ApiResult 结构处理
http.interceptors.response.use(
  (response) => {
    const payload = response.data
 
    // 无 code 字段时按成功透传
    if (payload?.code === undefined) {
      return {
        success: true,
        code: 0,
        message: '',
        data: payload,
      }
    }
 
    payload.success = payload.code === 0
 
    if (payload.success) return response
 
    if (isAuthenticationError(payload.code)) handleUnauthorized(payload.code)
 
    const err = new Error(payload.message || '请求失败')
    err.code = payload.code
    err.data = payload.data
    err.success = payload.success
    throw err
  },
  (error) => {
    const status = error.response?.status
    const payload = error.response?.data
 
    // 仅鉴权类错误(401 / 40101)才清会话并跳登录页;
    // 网络异常(无 status)、服务器 5xx 等普通错误不应把用户登出
    const errorCode = payload?.code ?? status ?? 0
    if (isAuthenticationError(errorCode)) handleUnauthorized(errorCode)
 
    const message =
      payload?.message ||
      (error.code === 'ECONNABORTED' ? '请求超时,请稍后重试' : '网络异常,请检查服务是否正常')
 
    const requestError = new Error(message)
    requestError.code = payload?.code ?? status ?? -1
    requestError.data = payload?.data
    requestError.success = false
 
    // 服务端不可达(无 status)/ 5xx 属全局性问题,统一弹 Message 提示;
    // 4xx 等业务错误由调用方自行决定提示方式。
    // 打 toasted 标记:登录、上传/删除等已有自身 Message 的调用方据此去重,避免重复弹窗
    if (!status || status >= 500) {
      Message.error(message)
      requestError.toasted = true
    }
 
    throw requestError
  },
)
 
export function post(path, data, config) {
  return http.post(path, data, config).then((r) => r.data)
}
 
export default http