348 lines
9.4 KiB
TypeScript
348 lines
9.4 KiB
TypeScript
// 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
|