import axios, { type AxiosResponse, type InternalAxiosRequestConfig } from 'axios' import { ElMessage } from 'element-plus' import { showToast } from 'vant' function showError(message: string) { const isMobile = window.location.pathname.startsWith('/m') if (isMobile) { showToast({ message, icon: 'cross' }) } else { ElMessage.error(message) } } declare module 'axios' { export interface AxiosRequestConfig { silent?: boolean } } import { clearAuthStorage, getAccessToken, getLoginPath, getRefreshToken, setAuthTokens, type AuthScope, } from '@/utils/authStorage' import type { ApiResponse } from './types' export const apiClient = axios.create({ baseURL: '/api', timeout: 10000, }) export async function unwrapData(request: Promise>>) { const { data } = await request return data.data } type RetryRequest = { resolve: (token: string) => void reject: (error: unknown) => void } type RefreshState = { refreshing: boolean pendingRequests: RetryRequest[] } type RetriableRequestConfig = InternalAxiosRequestConfig & { _retry?: boolean silent?: boolean } const refreshStates: Record = { user: { refreshing: false, pendingRequests: [], }, admin: { refreshing: false, pendingRequests: [], }, } function resolvePendingRequests(scope: AuthScope, token: string) { const pendingRequests = refreshStates[scope].pendingRequests.splice(0) pendingRequests.forEach(({ resolve }) => resolve(token)) } function rejectPendingRequests(scope: AuthScope, error: unknown) { const pendingRequests = refreshStates[scope].pendingRequests.splice(0) pendingRequests.forEach(({ reject }) => reject(error)) } async function refreshToken(scope: AuthScope): Promise { const refreshToken = getRefreshToken(scope) if (!refreshToken) throw new Error('no refresh token') const endpoint = scope === 'admin' ? '/api/admin/auth/refresh' : '/api/auth/refresh' const { data } = await axios.post(endpoint, { refresh_token: refreshToken }) const tokens = { access_token: data.data.access_token, refresh_token: data.data.refresh_token, } setAuthTokens(scope, tokens) return tokens.access_token } function getRequestScope(url = ''): AuthScope { return url.startsWith('/admin') ? 'admin' : 'user' } function redirectToLogin(scope: AuthScope) { clearAuthStorage(scope) const currentPath = window.location.pathname + window.location.search const loginPath = getLoginPath(scope, currentPath) if (currentPath.startsWith(loginPath)) return if (scope === 'admin') { window.location.assign(loginPath) return } window.location.assign(`${loginPath}?redirect=${encodeURIComponent(currentPath)}`) } function isRefreshRequest(url = '') { return url.endsWith('/auth/refresh') || url.endsWith('/admin/auth/refresh') } apiClient.interceptors.request.use((config) => { const scope = getRequestScope(config.url || '') const token = getAccessToken(scope) if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) apiClient.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config as RetriableRequestConfig | undefined if (!originalRequest || error?.response?.status !== 401 || originalRequest._retry) { if (originalRequest && !originalRequest.silent) { const msg = error.response?.data?.message || error.message || '网络连接异常,请稍后重试' showError(msg) } return Promise.reject(error) } const requestUrl = originalRequest.url || '' const scope = getRequestScope(requestUrl) if (isRefreshRequest(requestUrl)) { redirectToLogin(scope) if (originalRequest && !originalRequest.silent) { showError('登录已失效,请重新登录') } return Promise.reject(error) } const state = refreshStates[scope] if (state.refreshing) { return new Promise((resolve, reject) => { state.pendingRequests.push({ resolve, reject }) }).then((newToken) => { originalRequest._retry = true originalRequest.headers.Authorization = `Bearer ${newToken}` return apiClient(originalRequest) }) } state.refreshing = true try { const newToken = await refreshToken(scope) resolvePendingRequests(scope, newToken) originalRequest._retry = true originalRequest.headers.Authorization = `Bearer ${newToken}` return apiClient(originalRequest) } catch (refreshError) { rejectPendingRequests(scope, refreshError) redirectToLogin(scope) if (originalRequest && !originalRequest.silent) { showError('会话已过期,请重新登录') } return Promise.reject(refreshError) } finally { state.refreshing = false } }, )