Files
happy-life-star/web-new/src/utils/request.ts
T

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 }