348 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// HTTP 请求封装文件
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosRequestConfig, AxiosResponse } from 'axios'
import { useUserStore } from '@/stores/user'
// import router from '@/router'
import type { ApiResponse } from './types'
// 网络状态检测
const checkNetworkStatus = (): boolean => {
if (typeof navigator !== 'undefined' && 'onLine' in navigator) {
return navigator.onLine
}
return true // 默认认为网络可用
}
// 消息提示函数 - 使用window.alert作为fallback实际项目中应该使用UI库的消息组件
const showMessage = (message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info') => {
// 这里可以替换为你使用的UI库的消息组件
// 例如naive-ui的 useMessage()
console.log(`[${type.toUpperCase()}] ${message}`)
// 临时使用alert实际项目中应该替换为UI库的消息组件
if (type === 'error') {
// alert(`错误: ${message}`)
console.error(`错误: ${message}`)
}
}
// 创建axios实例
const request: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/jeecgboot',
timeout: 10000, // 减少到10秒避免长时间等待
headers: {
'Content-Type': 'application/json',
},
})
// 不需要token的接口列表
const NO_TOKEN_URLS = [
'/biz/course/category/list',
'/biz/course/subject/list',
'/biz/course/difficulty/list',
'/biz/course/list',
'/biz/course/detail',
// 可以在这里添加其他不需要token的接口
]
// 请求拦截器
request.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// 检查是否需要添加token
const needToken = !NO_TOKEN_URLS.some(url => config.url?.includes(url))
if (needToken) {
// 添加认证token直接传token不需要Bearer前缀
const userStore = useUserStore()
const token = userStore.token || localStorage.getItem('X-Access-Token') || ''
if (token) {
config.headers['X-Access-Token'] = token
}
}
// 添加请求时间戳
config.headers['X-Request-Time'] = Date.now().toString()
// 开发环境下打印请求信息
if (import.meta.env.DEV) {
console.log('🚀 Request:', {
url: config.url,
method: config.method,
params: config.params,
data: config.data,
})
}
return config
},
(error) => {
console.error('❌ Request Error:', error)
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
(response: AxiosResponse<any>) => {
const { data } = response
// 开发环境下打印响应信息
if (import.meta.env.DEV) {
console.log('✅ Response:', {
url: response.config.url,
status: response.status,
data: data,
})
}
// 如果是blob响应直接返回
if (response.config.responseType === 'blob') {
return response
}
// 处理不同的响应格式
let normalizedData: ApiResponse
// 如果响应已经是标准格式
if (data && typeof data === 'object' && 'code' in data && 'message' in data) {
normalizedData = data
} else {
// 如果响应不是标准格式,包装成标准格式
normalizedData = {
code: 200,
message: '请求成功',
data: data
}
}
// 检查业务状态码
if (normalizedData.code === 200 || normalizedData.code === 0) {
// 返回标准化后的响应
response.data = normalizedData
return response
}
// 处理业务错误
const errorMessage = normalizedData.message || '请求失败'
// 不在这里显示错误消息,让组件自己处理
// showMessage(errorMessage, 'error')
// 创建一个包含完整响应信息的错误对象
const error = new Error(errorMessage)
;(error as any).response = {
data: normalizedData,
status: 200 // HTTP状态码是200但业务状态码不是成功
}
return Promise.reject(error)
},
(error) => {
console.error('❌ Response Error:', error)
// 处理HTTP状态码错误
const { response } = error
let errorMessage = '网络错误,请稍后重试'
if (response) {
switch (response.status) {
case 400:
errorMessage = '请求参数错误'
break
case 401:
errorMessage = '登录已过期,请重新登录'
// 清除用户信息,不跳转页面(使用模态框登录)
const userStore = useUserStore()
userStore.logout()
break
case 403:
errorMessage = '没有权限访问'
break
case 404:
// 对于登出接口的404错误不显示错误消息
if (error.config?.url?.includes('/logout')) {
console.log('登出接口不存在,这是正常的')
return Promise.resolve({
data: { code: 200, message: '登出成功', data: null }
})
}
errorMessage = '请求的资源不存在'
break
case 422:
errorMessage = '数据验证失败'
break
case 429:
errorMessage = '请求过于频繁,请稍后重试'
break
case 500:
errorMessage = '服务器内部错误'
break
case 502:
errorMessage = '网关错误'
break
case 503:
errorMessage = '服务暂时不可用'
break
case 504:
errorMessage = '网关超时'
break
default:
errorMessage = `请求失败 (${response.status})`
}
} else if (error.code === 'ECONNABORTED') {
errorMessage = '请求超时,请检查网络连接'
} else if (error.message === 'Network Error') {
errorMessage = '网络连接失败,请检查网络设置'
}
showMessage(errorMessage, 'error')
return Promise.reject(error)
}
)
// 重试机制
const retryRequest = async <T = any>(
requestFn: () => Promise<T>,
maxRetries: number = 2,
delay: number = 1000
): Promise<T> => {
let lastError: any
for (let i = 0; i <= maxRetries; i++) {
try {
// 检查网络状态
if (!checkNetworkStatus()) {
throw new Error('网络连接不可用,请检查网络设置')
}
return await requestFn()
} catch (error: any) {
lastError = error
// 如果是最后一次重试,或者不是网络错误,直接抛出
if (i === maxRetries || (error.code !== 'ECONNABORTED' && error.message !== 'Network Error' && !error.message?.includes('网络'))) {
throw error
}
console.log(`请求失败,${delay}ms后进行第${i + 1}次重试...`)
console.log('错误详情:', error.message)
await new Promise(resolve => setTimeout(resolve, delay))
delay *= 2 // 指数退避
}
}
throw lastError
}
// 请求方法封装
export class ApiRequest {
// GET 请求
static async get<T = any>(
url: string,
params?: any,
config?: AxiosRequestConfig
): Promise<ApiResponse<T>> {
return await retryRequest(() => request.get(url, { params, ...config }))
}
// POST 请求
static async post<T = any>(
url: string,
data?: any,
config?: AxiosRequestConfig
): Promise<ApiResponse<T>> {
return await retryRequest(() => request.post(url, data, config))
}
// PUT 请求
static put<T = any>(
url: string,
data?: any,
config?: AxiosRequestConfig
): Promise<ApiResponse<T>> {
return request.put(url, data, config)
}
// PATCH 请求
static patch<T = any>(
url: string,
data?: any,
config?: AxiosRequestConfig
): Promise<ApiResponse<T>> {
return request.patch(url, data, config)
}
// DELETE 请求
static delete<T = any>(
url: string,
params?: any,
config?: AxiosRequestConfig
): Promise<ApiResponse<T>> {
return request.delete(url, { params, ...config })
}
// 文件上传
static upload<T = any>(
url: string,
file: File,
onProgress?: (progress: number) => void,
config?: AxiosRequestConfig
): Promise<ApiResponse<T>> {
const formData = new FormData()
formData.append('file', file)
return request.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
if (onProgress && progressEvent.total) {
const progress = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
)
onProgress(progress)
}
},
...config,
})
}
// 文件下载
static download(
url: string,
filename?: string,
params?: any,
config?: AxiosRequestConfig
): Promise<void> {
return request
.get(url, {
params,
responseType: 'blob',
...config,
})
.then((response: any) => {
const blob = new Blob([response.data])
const downloadUrl = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = downloadUrl
link.download = filename || 'download'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(downloadUrl)
})
}
// API健康检查
static async healthCheck(): Promise<boolean> {
try {
const response = await request.get('/health', { timeout: 5000 })
return response.status === 200
} catch (error) {
console.warn('API健康检查失败:', error)
return false
}
}
}
export default request