347 lines
8.6 KiB
TypeScript
347 lines
8.6 KiB
TypeScript
/**
|
|
* HTTP请求工具
|
|
* 基于Axios封装,支持请求拦截、响应拦截、错误处理等
|
|
*/
|
|
|
|
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import { envConfig } from '@/config/env'
|
|
import { STORAGE_KEYS, ERROR_CODES } from '@/config/constants'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import { useAppStore } from '@/stores/app'
|
|
import router from '@/router'
|
|
|
|
// 请求配置接口
|
|
interface RequestConfig extends AxiosRequestConfig {
|
|
skipAuth?: boolean
|
|
skipErrorHandler?: boolean
|
|
showLoading?: boolean
|
|
loadingText?: string
|
|
}
|
|
|
|
// 响应数据接口
|
|
interface ResponseData<T = any> {
|
|
code: number
|
|
message: string
|
|
data: T
|
|
success: boolean
|
|
timestamp: number
|
|
}
|
|
|
|
class RequestService {
|
|
private instance: AxiosInstance
|
|
private pendingRequests = new Map<string, AbortController>()
|
|
|
|
constructor() {
|
|
// 创建axios实例
|
|
this.instance = axios.create({
|
|
baseURL: envConfig.apiBaseUrl,
|
|
timeout: 30000,
|
|
headers: {
|
|
'Content-Type': 'application/json;charset=UTF-8'
|
|
}
|
|
})
|
|
|
|
// 设置请求拦截器
|
|
this.setupRequestInterceptor()
|
|
|
|
// 设置响应拦截器
|
|
this.setupResponseInterceptor()
|
|
}
|
|
|
|
/**
|
|
* 设置请求拦截器
|
|
*/
|
|
private setupRequestInterceptor() {
|
|
this.instance.interceptors.request.use(
|
|
(config: any) => {
|
|
const requestConfig = config as RequestConfig
|
|
|
|
// 生成请求ID用于追踪
|
|
const requestId = this.generateRequestId(config)
|
|
config.metadata = { requestId }
|
|
|
|
// 处理重复请求
|
|
this.handleDuplicateRequest(config, requestId)
|
|
|
|
// 添加认证头
|
|
if (!requestConfig.skipAuth) {
|
|
const token = this.getToken()
|
|
if (token) {
|
|
config.headers.Authorization = `Bearer ${token}`
|
|
}
|
|
}
|
|
|
|
// 显示加载状态
|
|
if (requestConfig.showLoading) {
|
|
const appStore = useAppStore()
|
|
appStore.setLoading(true, requestConfig.loadingText)
|
|
}
|
|
|
|
// 调试模式下打印请求信息
|
|
if (envConfig.debug) {
|
|
console.log('🚀 发送请求:', {
|
|
url: config.url,
|
|
method: config.method,
|
|
params: config.params,
|
|
data: config.data,
|
|
headers: config.headers
|
|
})
|
|
}
|
|
|
|
return config
|
|
},
|
|
(error) => {
|
|
console.error('❌ 请求拦截器错误:', error)
|
|
return Promise.reject(error)
|
|
}
|
|
)
|
|
}
|
|
|
|
/**
|
|
* 设置响应拦截器
|
|
*/
|
|
private setupResponseInterceptor() {
|
|
this.instance.interceptors.response.use(
|
|
(response: AxiosResponse<ResponseData>) => {
|
|
const config = response.config as RequestConfig
|
|
const requestId = config.metadata?.requestId
|
|
|
|
// 移除pending请求
|
|
if (requestId) {
|
|
this.pendingRequests.delete(requestId)
|
|
}
|
|
|
|
// 隐藏加载状态
|
|
if (config.showLoading) {
|
|
const appStore = useAppStore()
|
|
appStore.setLoading(false)
|
|
}
|
|
|
|
// 调试模式下打印响应信息
|
|
if (envConfig.debug) {
|
|
console.log('✅ 收到响应:', {
|
|
url: response.config.url,
|
|
status: response.status,
|
|
data: response.data
|
|
})
|
|
}
|
|
|
|
const { data } = response
|
|
|
|
// 处理业务状态码
|
|
if (data.code === 200 || data.success) {
|
|
return data.data
|
|
} else {
|
|
return this.handleBusinessError(data, config)
|
|
}
|
|
},
|
|
(error) => {
|
|
const config = error.config as RequestConfig
|
|
const requestId = config?.metadata?.requestId
|
|
|
|
// 移除pending请求
|
|
if (requestId) {
|
|
this.pendingRequests.delete(requestId)
|
|
}
|
|
|
|
// 隐藏加载状态
|
|
if (config?.showLoading) {
|
|
const appStore = useAppStore()
|
|
appStore.setLoading(false)
|
|
}
|
|
|
|
return this.handleRequestError(error, config)
|
|
}
|
|
)
|
|
}
|
|
|
|
/**
|
|
* 生成请求ID
|
|
*/
|
|
private generateRequestId(config: AxiosRequestConfig): string {
|
|
const { method, url, params, data } = config
|
|
return `${method}_${url}_${JSON.stringify(params)}_${JSON.stringify(data)}_${Date.now()}`
|
|
}
|
|
|
|
/**
|
|
* 处理重复请求
|
|
*/
|
|
private handleDuplicateRequest(config: AxiosRequestConfig, requestId: string) {
|
|
const duplicateKey = `${config.method}_${config.url}`
|
|
|
|
// 取消之前的相同请求
|
|
if (this.pendingRequests.has(duplicateKey)) {
|
|
const controller = this.pendingRequests.get(duplicateKey)
|
|
controller?.abort('请求被取消:发起了新的相同请求')
|
|
}
|
|
|
|
// 创建新的AbortController
|
|
const controller = new AbortController()
|
|
config.signal = controller.signal
|
|
this.pendingRequests.set(duplicateKey, controller)
|
|
}
|
|
|
|
/**
|
|
* 获取Token
|
|
*/
|
|
private getToken(): string | null {
|
|
return localStorage.getItem(STORAGE_KEYS.TOKEN)
|
|
}
|
|
|
|
/**
|
|
* 处理业务错误
|
|
*/
|
|
private handleBusinessError(data: ResponseData, config: RequestConfig) {
|
|
if (config.skipErrorHandler) {
|
|
return Promise.reject(data)
|
|
}
|
|
|
|
// 特殊错误码处理
|
|
switch (data.code) {
|
|
case ERROR_CODES.UNAUTHORIZED:
|
|
this.handleUnauthorized()
|
|
break
|
|
case ERROR_CODES.FORBIDDEN:
|
|
ElMessage.error('权限不足,无法访问该资源')
|
|
break
|
|
case ERROR_CODES.NOT_FOUND:
|
|
ElMessage.error('请求的资源不存在')
|
|
break
|
|
default:
|
|
ElMessage.error(data.message || '请求失败')
|
|
}
|
|
|
|
return Promise.reject(data)
|
|
}
|
|
|
|
/**
|
|
* 处理请求错误
|
|
*/
|
|
private handleRequestError(error: any, config: RequestConfig) {
|
|
if (config?.skipErrorHandler) {
|
|
return Promise.reject(error)
|
|
}
|
|
|
|
let message = '网络错误,请稍后重试'
|
|
|
|
if (error.code === 'ECONNABORTED') {
|
|
message = '请求超时,请稍后重试'
|
|
} else if (error.response) {
|
|
const { status } = error.response
|
|
switch (status) {
|
|
case ERROR_CODES.UNAUTHORIZED:
|
|
this.handleUnauthorized()
|
|
return Promise.reject(error)
|
|
case ERROR_CODES.FORBIDDEN:
|
|
message = '权限不足,无法访问该资源'
|
|
break
|
|
case ERROR_CODES.NOT_FOUND:
|
|
message = '请求的资源不存在'
|
|
break
|
|
case ERROR_CODES.INTERNAL_SERVER_ERROR:
|
|
message = '服务器内部错误'
|
|
break
|
|
default:
|
|
message = `请求失败 (${status})`
|
|
}
|
|
} else if (error.request) {
|
|
message = '网络连接失败,请检查网络设置'
|
|
}
|
|
|
|
ElMessage.error(message)
|
|
return Promise.reject(error)
|
|
}
|
|
|
|
/**
|
|
* 处理未授权错误
|
|
*/
|
|
private async handleUnauthorized() {
|
|
const authStore = useAuthStore()
|
|
|
|
try {
|
|
// 尝试刷新Token
|
|
await authStore.refreshToken()
|
|
} catch {
|
|
// 刷新失败,跳转到登录页
|
|
ElMessageBox.alert('登录已过期,请重新登录', '提示', {
|
|
confirmButtonText: '确定',
|
|
type: 'warning'
|
|
}).then(() => {
|
|
authStore.logout()
|
|
router.push('/login')
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET请求
|
|
*/
|
|
get<T = any>(url: string, params?: any, config?: RequestConfig): Promise<T> {
|
|
return this.instance.get(url, { params, ...config })
|
|
}
|
|
|
|
/**
|
|
* POST请求
|
|
*/
|
|
post<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
|
|
return this.instance.post(url, data, config)
|
|
}
|
|
|
|
/**
|
|
* PUT请求
|
|
*/
|
|
put<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
|
|
return this.instance.put(url, data, config)
|
|
}
|
|
|
|
/**
|
|
* DELETE请求
|
|
*/
|
|
delete<T = any>(url: string, config?: RequestConfig): Promise<T> {
|
|
return this.instance.delete(url, config)
|
|
}
|
|
|
|
/**
|
|
* 上传文件
|
|
*/
|
|
upload<T = any>(url: string, file: File, config?: RequestConfig): Promise<T> {
|
|
const formData = new FormData()
|
|
formData.append('file', file)
|
|
|
|
return this.instance.post(url, formData, {
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data'
|
|
},
|
|
...config
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 取消所有请求
|
|
*/
|
|
cancelAllRequests() {
|
|
this.pendingRequests.forEach((controller) => {
|
|
controller.abort('用户取消请求')
|
|
})
|
|
this.pendingRequests.clear()
|
|
}
|
|
|
|
/**
|
|
* 取消指定请求
|
|
*/
|
|
cancelRequest(requestId: string) {
|
|
const controller = this.pendingRequests.get(requestId)
|
|
if (controller) {
|
|
controller.abort('用户取消请求')
|
|
this.pendingRequests.delete(requestId)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 创建请求实例
|
|
const request = new RequestService()
|
|
|
|
export default request
|
|
export { type RequestConfig }
|