import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios' import { useUserStore } from '@/stores/user.ts' import { showToast } from '@/utils/toast' import router from '@/router' import { mockRequest, isMockEnabled } from '@/mock' // API 基础配置 const BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api' const TIMEOUT = 30000 let handlingUnauthorized = false const shouldRedirectToLogin = () => { const currentRoute = router.currentRoute.value return Boolean(currentRoute.meta?.requiresAuth) } const handleUnauthorized = (message = '登录已过期,请重新登录') => { const userStore = useUserStore() userStore.clearAuth() if (!handlingUnauthorized) { handlingUnauthorized = true if (shouldRedirectToLogin() && router.currentRoute.value.path !== '/login') { router.push('/login') showToast(message) return } } } // 创建 axios 实例 const service: AxiosInstance = axios.create({ baseURL: BASE_URL, timeout: TIMEOUT, headers: { 'Content-Type': 'application/json' } }) // 请求拦截器 service.interceptors.request.use( (config: InternalAxiosRequestConfig) => { const userStore = useUserStore() // 添加 Token if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}` } return config }, (error) => { console.error('请求错误:', error) return Promise.reject(error) } ) // 响应拦截器 service.interceptors.response.use( (response: AxiosResponse) => { const { data } = response // 业务状态码判断 if (data.code === 200) { handlingUnauthorized = false return data.data } // Token 过期 if (data.code === 401) { handleUnauthorized() return Promise.reject(new Error('Token expired')) } // 其他业务错误 showToast(data.message || '请求失败') return Promise.reject(new Error(data.message || '请求失败')) }, (error) => { console.error('响应错误:', error) // 网络错误 if (error.code === 'ECONNABORTED') { showToast('请求超时,请稍后重试') } else if (error.message.includes('Network Error')) { showToast('网络错误,请检查网络连接') } else if (error.response) { const status = error.response.status switch (status) { case 401: { handleUnauthorized() break } case 403: showToast('没有权限访问') break case 404: showToast('请求的资源不存在') break case 413: showToast('上传文件过大,请选择更小的文件') break case 500: showToast('服务器错误,请稍后重试') break default: showToast(`请求失败: ${status}`) } } return Promise.reject(error) } ) // Mock 模式下的请求处理 async function mockFetch(method: string, url: string, data?: any, params?: any): Promise { const result = await mockRequest(url, { method, body: data, params }) if (result.code === 200) { handlingUnauthorized = false return result.data } if (result.code === 401) { handleUnauthorized() throw new Error('Token expired') } showToast(result.message || '请求失败') throw new Error(result.message || '请求失败') } // 封装请求方法 const request = { async get(url: string, params?: object, config?: AxiosRequestConfig): Promise { // Mock 模式 if (isMockEnabled()) { return mockFetch('GET', url, undefined, params) } return service.get(url, { params, ...config }) }, async post(url: string, data?: object, config?: AxiosRequestConfig): Promise { // Mock 模式 if (isMockEnabled()) { return mockFetch('POST', url, data) } return service.post(url, data, config) }, async put(url: string, data?: object, config?: AxiosRequestConfig): Promise { // Mock 模式 if (isMockEnabled()) { return mockFetch('PUT', url, data) } return service.put(url, data, config) }, async delete(url: string, params?: object, config?: AxiosRequestConfig): Promise { // Mock 模式 if (isMockEnabled()) { return mockFetch('DELETE', url, undefined, params) } return service.delete(url, { params, ...config }) }, async upload(url: string, file: File, onProgress?: (percent: number) => void): Promise { // Mock 模式 if (isMockEnabled()) { return mockFetch('POST', url, { file }) } const formData = new FormData() formData.append('file', file) return service.post(url, formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (progressEvent) => { if (progressEvent.total && onProgress) { const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total) onProgress(percent) } } }) } } export default request export { service as axiosInstance }